we (web engine): Experimental web browser project to understand the limits of Claude
at test262-harness 1347 lines 44 kB view raw
1//! CSS value parsing: convert raw component values into typed property values. 2//! 3//! Provides `CssValue` enum and parsing from `ComponentValue` lists. 4 5use crate::parser::ComponentValue; 6 7// --------------------------------------------------------------------------- 8// Core value types 9// --------------------------------------------------------------------------- 10 11/// A fully parsed, typed CSS value. 12#[derive(Debug, Clone, PartialEq)] 13pub enum CssValue { 14 /// A length with resolved unit. 15 Length(f64, LengthUnit), 16 /// A percentage value. 17 Percentage(f64), 18 /// A color value (r, g, b, a in 0–255 range, alpha 0–255). 19 Color(Color), 20 /// A numeric value (unitless). 21 Number(f64), 22 /// A string value. 23 String(String), 24 /// A keyword (ident). 25 Keyword(String), 26 /// The `auto` keyword. 27 Auto, 28 /// The `inherit` keyword. 29 Inherit, 30 /// The `initial` keyword. 31 Initial, 32 /// The `unset` keyword. 33 Unset, 34 /// The `currentColor` keyword. 35 CurrentColor, 36 /// The `none` keyword (for display, background, etc.). 37 None, 38 /// The `transparent` keyword. 39 Transparent, 40 /// Zero (unitless). 41 Zero, 42 /// A list of values (for multi-value properties like margin shorthand). 43 List(Vec<CssValue>), 44} 45 46/// CSS length unit. 47#[derive(Debug, Clone, Copy, PartialEq, Eq)] 48pub enum LengthUnit { 49 // Absolute 50 Px, 51 Pt, 52 Cm, 53 Mm, 54 In, 55 Pc, 56 // Font-relative 57 Em, 58 Rem, 59 // Viewport 60 Vw, 61 Vh, 62 Vmin, 63 Vmax, 64} 65 66/// A CSS color in RGBA format. 67#[derive(Debug, Clone, Copy, PartialEq, Eq)] 68pub struct Color { 69 pub r: u8, 70 pub g: u8, 71 pub b: u8, 72 pub a: u8, 73} 74 75impl Color { 76 pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { 77 Self { r, g, b, a } 78 } 79 80 pub fn rgb(r: u8, g: u8, b: u8) -> Self { 81 Self { r, g, b, a: 255 } 82 } 83} 84 85// --------------------------------------------------------------------------- 86// Shorthand expansion result 87// --------------------------------------------------------------------------- 88 89/// A longhand property-value pair produced by shorthand expansion. 90#[derive(Debug, Clone, PartialEq)] 91pub struct LonghandDeclaration { 92 pub property: String, 93 pub value: CssValue, 94 pub important: bool, 95} 96 97// --------------------------------------------------------------------------- 98// Value parsing 99// --------------------------------------------------------------------------- 100 101/// Parse a single `CssValue` from a list of component values. 102pub fn parse_value(values: &[ComponentValue]) -> CssValue { 103 // Filter out whitespace for easier matching 104 let non_ws: Vec<&ComponentValue> = values 105 .iter() 106 .filter(|v| !matches!(v, ComponentValue::Whitespace)) 107 .collect(); 108 109 if non_ws.is_empty() { 110 return CssValue::Keyword(String::new()); 111 } 112 113 // Single-value case 114 if non_ws.len() == 1 { 115 return parse_single_value(non_ws[0]); 116 } 117 118 // Multi-value: parse each non-whitespace value 119 let parsed: Vec<CssValue> = non_ws.iter().copied().map(parse_single_value).collect(); 120 CssValue::List(parsed) 121} 122 123/// Parse a single component value into a `CssValue`. 124pub fn parse_single_value(cv: &ComponentValue) -> CssValue { 125 match cv { 126 ComponentValue::Ident(s) => parse_keyword(s), 127 ComponentValue::String(s) => CssValue::String(s.clone()), 128 ComponentValue::Number(n, _) => { 129 if *n == 0.0 { 130 CssValue::Zero 131 } else { 132 CssValue::Number(*n) 133 } 134 } 135 ComponentValue::Percentage(n) => CssValue::Percentage(*n), 136 ComponentValue::Dimension(n, _, unit) => parse_dimension(*n, unit), 137 ComponentValue::Hash(s, _) => parse_hex_color(s), 138 ComponentValue::Function(name, args) => parse_function(name, args), 139 ComponentValue::Comma => CssValue::Keyword(",".to_string()), 140 ComponentValue::Delim(c) => CssValue::Keyword(c.to_string()), 141 ComponentValue::Whitespace => CssValue::Keyword(" ".to_string()), 142 } 143} 144 145fn parse_keyword(s: &str) -> CssValue { 146 match s.to_ascii_lowercase().as_str() { 147 "auto" => CssValue::Auto, 148 "inherit" => CssValue::Inherit, 149 "initial" => CssValue::Initial, 150 "unset" => CssValue::Unset, 151 "none" => CssValue::None, 152 "transparent" => CssValue::Transparent, 153 "currentcolor" => CssValue::CurrentColor, 154 // Named colors 155 name => { 156 if let Some(color) = named_color(name) { 157 CssValue::Color(color) 158 } else { 159 CssValue::Keyword(s.to_ascii_lowercase()) 160 } 161 } 162 } 163} 164 165fn parse_dimension(n: f64, unit: &str) -> CssValue { 166 let u = unit.to_ascii_lowercase(); 167 match u.as_str() { 168 "px" => CssValue::Length(n, LengthUnit::Px), 169 "pt" => CssValue::Length(n, LengthUnit::Pt), 170 "cm" => CssValue::Length(n, LengthUnit::Cm), 171 "mm" => CssValue::Length(n, LengthUnit::Mm), 172 "in" => CssValue::Length(n, LengthUnit::In), 173 "pc" => CssValue::Length(n, LengthUnit::Pc), 174 "em" => CssValue::Length(n, LengthUnit::Em), 175 "rem" => CssValue::Length(n, LengthUnit::Rem), 176 "vw" => CssValue::Length(n, LengthUnit::Vw), 177 "vh" => CssValue::Length(n, LengthUnit::Vh), 178 "vmin" => CssValue::Length(n, LengthUnit::Vmin), 179 "vmax" => CssValue::Length(n, LengthUnit::Vmax), 180 _ => CssValue::Keyword(format!("{n}{u}")), 181 } 182} 183 184// --------------------------------------------------------------------------- 185// Color parsing 186// --------------------------------------------------------------------------- 187 188fn parse_hex_color(hex: &str) -> CssValue { 189 let chars: Vec<char> = hex.chars().collect(); 190 match chars.len() { 191 // #rgb 192 3 => { 193 let r = hex_digit(chars[0]) * 17; 194 let g = hex_digit(chars[1]) * 17; 195 let b = hex_digit(chars[2]) * 17; 196 CssValue::Color(Color::rgb(r, g, b)) 197 } 198 // #rgba 199 4 => { 200 let r = hex_digit(chars[0]) * 17; 201 let g = hex_digit(chars[1]) * 17; 202 let b = hex_digit(chars[2]) * 17; 203 let a = hex_digit(chars[3]) * 17; 204 CssValue::Color(Color::new(r, g, b, a)) 205 } 206 // #rrggbb 207 6 => { 208 let r = hex_byte(chars[0], chars[1]); 209 let g = hex_byte(chars[2], chars[3]); 210 let b = hex_byte(chars[4], chars[5]); 211 CssValue::Color(Color::rgb(r, g, b)) 212 } 213 // #rrggbbaa 214 8 => { 215 let r = hex_byte(chars[0], chars[1]); 216 let g = hex_byte(chars[2], chars[3]); 217 let b = hex_byte(chars[4], chars[5]); 218 let a = hex_byte(chars[6], chars[7]); 219 CssValue::Color(Color::new(r, g, b, a)) 220 } 221 _ => CssValue::Keyword(format!("#{hex}")), 222 } 223} 224 225fn hex_digit(c: char) -> u8 { 226 match c { 227 '0'..='9' => c as u8 - b'0', 228 'a'..='f' => c as u8 - b'a' + 10, 229 'A'..='F' => c as u8 - b'A' + 10, 230 _ => 0, 231 } 232} 233 234fn hex_byte(hi: char, lo: char) -> u8 { 235 hex_digit(hi) * 16 + hex_digit(lo) 236} 237 238fn parse_function(name: &str, args: &[ComponentValue]) -> CssValue { 239 match name.to_ascii_lowercase().as_str() { 240 "rgb" => parse_rgb(args, false), 241 "rgba" => parse_rgb(args, true), 242 _ => CssValue::Keyword(format!("{name}()")), 243 } 244} 245 246fn parse_rgb(args: &[ComponentValue], _has_alpha: bool) -> CssValue { 247 let nums: Vec<f64> = args 248 .iter() 249 .filter_map(|cv| match cv { 250 ComponentValue::Number(n, _) => Some(*n), 251 ComponentValue::Percentage(n) => Some(*n * 2.55), 252 _ => Option::None, 253 }) 254 .collect(); 255 256 match nums.len() { 257 3 => CssValue::Color(Color::rgb( 258 clamp_u8(nums[0]), 259 clamp_u8(nums[1]), 260 clamp_u8(nums[2]), 261 )), 262 4 => { 263 let a = if args 264 .iter() 265 .any(|cv| matches!(cv, ComponentValue::Percentage(_))) 266 { 267 // If any arg is a percentage, treat alpha as 0-1 float 268 clamp_u8(nums[3] / 2.55 * 255.0) 269 } else { 270 // Check if alpha looks like a 0-1 range 271 if nums[3] <= 1.0 { 272 clamp_u8(nums[3] * 255.0) 273 } else { 274 clamp_u8(nums[3]) 275 } 276 }; 277 CssValue::Color(Color::new( 278 clamp_u8(nums[0]), 279 clamp_u8(nums[1]), 280 clamp_u8(nums[2]), 281 a, 282 )) 283 } 284 _ => CssValue::Keyword("rgb()".to_string()), 285 } 286} 287 288fn clamp_u8(n: f64) -> u8 { 289 n.round().clamp(0.0, 255.0) as u8 290} 291 292// --------------------------------------------------------------------------- 293// Named colors (CSS Level 1 + transparent) 294// --------------------------------------------------------------------------- 295 296fn named_color(name: &str) -> Option<Color> { 297 Some(match name { 298 "black" => Color::rgb(0, 0, 0), 299 "silver" => Color::rgb(192, 192, 192), 300 "gray" | "grey" => Color::rgb(128, 128, 128), 301 "white" => Color::rgb(255, 255, 255), 302 "maroon" => Color::rgb(128, 0, 0), 303 "red" => Color::rgb(255, 0, 0), 304 "purple" => Color::rgb(128, 0, 128), 305 "fuchsia" | "magenta" => Color::rgb(255, 0, 255), 306 "green" => Color::rgb(0, 128, 0), 307 "lime" => Color::rgb(0, 255, 0), 308 "olive" => Color::rgb(128, 128, 0), 309 "yellow" => Color::rgb(255, 255, 0), 310 "navy" => Color::rgb(0, 0, 128), 311 "blue" => Color::rgb(0, 0, 255), 312 "teal" => Color::rgb(0, 128, 128), 313 "aqua" | "cyan" => Color::rgb(0, 255, 255), 314 "orange" => Color::rgb(255, 165, 0), 315 _ => return Option::None, 316 }) 317} 318 319// --------------------------------------------------------------------------- 320// Shorthand expansion 321// --------------------------------------------------------------------------- 322 323/// Expand a CSS declaration into longhand declarations. 324/// Returns `None` if the property is not a known shorthand. 325pub fn expand_shorthand( 326 property: &str, 327 values: &[ComponentValue], 328 important: bool, 329) -> Option<Vec<LonghandDeclaration>> { 330 match property { 331 "margin" => Some(expand_box_shorthand("margin", values, important)), 332 "padding" => Some(expand_box_shorthand("padding", values, important)), 333 "border" => Some(expand_border(values, important)), 334 "border-width" => Some( 335 expand_box_shorthand("border", values, important) 336 .into_iter() 337 .map(|mut d| { 338 d.property = format!("{}-width", d.property); 339 d 340 }) 341 .collect(), 342 ), 343 "border-style" => Some( 344 expand_box_shorthand("border", values, important) 345 .into_iter() 346 .map(|mut d| { 347 d.property = format!("{}-style", d.property); 348 d 349 }) 350 .collect(), 351 ), 352 "border-color" => Some( 353 expand_box_shorthand("border", values, important) 354 .into_iter() 355 .map(|mut d| { 356 d.property = format!("{}-color", d.property); 357 d 358 }) 359 .collect(), 360 ), 361 "background" => Some(expand_background(values, important)), 362 "flex" => Some(expand_flex(values, important)), 363 "flex-flow" => Some(expand_flex_flow(values, important)), 364 "gap" => Some(expand_gap(values, important)), 365 _ => Option::None, 366 } 367} 368 369/// Expand the `flex` shorthand into `flex-grow`, `flex-shrink`, `flex-basis`. 370fn expand_flex(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 371 let parsed: Vec<CssValue> = values 372 .iter() 373 .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) 374 .map(parse_single_value) 375 .collect(); 376 377 let (grow, shrink, basis) = match parsed.as_slice() { 378 [CssValue::None] => (CssValue::Number(0.0), CssValue::Number(0.0), CssValue::Auto), 379 [CssValue::Auto] => (CssValue::Number(1.0), CssValue::Number(1.0), CssValue::Auto), 380 [CssValue::Number(g)] => (CssValue::Number(*g), CssValue::Number(1.0), CssValue::Zero), 381 [CssValue::Zero] => (CssValue::Number(0.0), CssValue::Number(1.0), CssValue::Zero), 382 [CssValue::Number(g), CssValue::Number(s)] => { 383 (CssValue::Number(*g), CssValue::Number(*s), CssValue::Zero) 384 } 385 [CssValue::Number(g), CssValue::Number(s), basis] => { 386 (CssValue::Number(*g), CssValue::Number(*s), basis.clone()) 387 } 388 _ => (CssValue::Number(0.0), CssValue::Number(1.0), CssValue::Auto), 389 }; 390 391 vec![ 392 LonghandDeclaration { 393 property: "flex-grow".to_string(), 394 value: grow, 395 important, 396 }, 397 LonghandDeclaration { 398 property: "flex-shrink".to_string(), 399 value: shrink, 400 important, 401 }, 402 LonghandDeclaration { 403 property: "flex-basis".to_string(), 404 value: basis, 405 important, 406 }, 407 ] 408} 409 410/// Expand the `flex-flow` shorthand into `flex-direction` and `flex-wrap`. 411fn expand_flex_flow(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 412 let parsed: Vec<CssValue> = values 413 .iter() 414 .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) 415 .map(parse_single_value) 416 .collect(); 417 418 let mut direction = CssValue::Keyword("row".to_string()); 419 let mut wrap = CssValue::Keyword("nowrap".to_string()); 420 421 for val in &parsed { 422 if let CssValue::Keyword(k) = val { 423 match k.as_str() { 424 "row" | "row-reverse" | "column" | "column-reverse" => { 425 direction = val.clone(); 426 } 427 "nowrap" | "wrap" | "wrap-reverse" => { 428 wrap = val.clone(); 429 } 430 _ => {} 431 } 432 } 433 } 434 435 vec![ 436 LonghandDeclaration { 437 property: "flex-direction".to_string(), 438 value: direction, 439 important, 440 }, 441 LonghandDeclaration { 442 property: "flex-wrap".to_string(), 443 value: wrap, 444 important, 445 }, 446 ] 447} 448 449/// Expand the `gap` shorthand into `row-gap` and `column-gap`. 450fn expand_gap(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 451 let parsed: Vec<CssValue> = values 452 .iter() 453 .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) 454 .map(parse_single_value) 455 .collect(); 456 457 let (row, col) = match parsed.as_slice() { 458 [single] => (single.clone(), single.clone()), 459 [r, c] => (r.clone(), c.clone()), 460 _ => (CssValue::Zero, CssValue::Zero), 461 }; 462 463 vec![ 464 LonghandDeclaration { 465 property: "row-gap".to_string(), 466 value: row, 467 important, 468 }, 469 LonghandDeclaration { 470 property: "column-gap".to_string(), 471 value: col, 472 important, 473 }, 474 ] 475} 476 477/// Expand a box-model shorthand (margin, padding) using the 1-to-4 value pattern. 478fn expand_box_shorthand( 479 prefix: &str, 480 values: &[ComponentValue], 481 important: bool, 482) -> Vec<LonghandDeclaration> { 483 let parsed: Vec<CssValue> = values 484 .iter() 485 .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) 486 .map(parse_single_value) 487 .collect(); 488 489 let (top, right, bottom, left) = match parsed.len() { 490 1 => ( 491 parsed[0].clone(), 492 parsed[0].clone(), 493 parsed[0].clone(), 494 parsed[0].clone(), 495 ), 496 2 => ( 497 parsed[0].clone(), 498 parsed[1].clone(), 499 parsed[0].clone(), 500 parsed[1].clone(), 501 ), 502 3 => ( 503 parsed[0].clone(), 504 parsed[1].clone(), 505 parsed[2].clone(), 506 parsed[1].clone(), 507 ), 508 4 => ( 509 parsed[0].clone(), 510 parsed[1].clone(), 511 parsed[2].clone(), 512 parsed[3].clone(), 513 ), 514 _ => { 515 let fallback = if parsed.is_empty() { 516 CssValue::Zero 517 } else { 518 parsed[0].clone() 519 }; 520 ( 521 fallback.clone(), 522 fallback.clone(), 523 fallback.clone(), 524 fallback, 525 ) 526 } 527 }; 528 529 vec![ 530 LonghandDeclaration { 531 property: format!("{prefix}-top"), 532 value: top, 533 important, 534 }, 535 LonghandDeclaration { 536 property: format!("{prefix}-right"), 537 value: right, 538 important, 539 }, 540 LonghandDeclaration { 541 property: format!("{prefix}-bottom"), 542 value: bottom, 543 important, 544 }, 545 LonghandDeclaration { 546 property: format!("{prefix}-left"), 547 value: left, 548 important, 549 }, 550 ] 551} 552 553/// Expand `border` shorthand into border-width, border-style, border-color. 554fn expand_border(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 555 let parsed: Vec<CssValue> = values 556 .iter() 557 .filter(|v| !matches!(v, ComponentValue::Whitespace)) 558 .map(parse_single_value) 559 .collect(); 560 561 let mut width = CssValue::Keyword("medium".to_string()); 562 let mut style = CssValue::None; 563 let mut color = CssValue::CurrentColor; 564 565 for val in &parsed { 566 match val { 567 CssValue::Length(_, _) | CssValue::Zero => width = val.clone(), 568 CssValue::Color(_) | CssValue::CurrentColor | CssValue::Transparent => { 569 color = val.clone() 570 } 571 CssValue::Keyword(kw) => match kw.as_str() { 572 "thin" | "medium" | "thick" => width = val.clone(), 573 "none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" 574 | "ridge" | "inset" | "outset" => style = val.clone(), 575 _ => { 576 // Could be a named color 577 if let Some(c) = named_color(kw) { 578 color = CssValue::Color(c); 579 } 580 } 581 }, 582 _ => {} 583 } 584 } 585 586 vec![ 587 LonghandDeclaration { 588 property: "border-width".to_string(), 589 value: width, 590 important, 591 }, 592 LonghandDeclaration { 593 property: "border-style".to_string(), 594 value: style, 595 important, 596 }, 597 LonghandDeclaration { 598 property: "border-color".to_string(), 599 value: color, 600 important, 601 }, 602 ] 603} 604 605/// Expand `background` shorthand (basic: color only for now). 606fn expand_background(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 607 let parsed: Vec<CssValue> = values 608 .iter() 609 .filter(|v| !matches!(v, ComponentValue::Whitespace)) 610 .map(parse_single_value) 611 .collect(); 612 613 let mut bg_color = CssValue::Transparent; 614 615 for val in &parsed { 616 match val { 617 CssValue::Color(_) | CssValue::CurrentColor | CssValue::Transparent => { 618 bg_color = val.clone() 619 } 620 CssValue::Keyword(kw) => { 621 if let Some(c) = named_color(kw) { 622 bg_color = CssValue::Color(c); 623 } else { 624 match kw.as_str() { 625 "none" => {} // background-image: none 626 _ => bg_color = val.clone(), 627 } 628 } 629 } 630 _ => {} 631 } 632 } 633 634 vec![LonghandDeclaration { 635 property: "background-color".to_string(), 636 value: bg_color, 637 important, 638 }] 639} 640 641// --------------------------------------------------------------------------- 642// Tests 643// --------------------------------------------------------------------------- 644 645#[cfg(test)] 646mod tests { 647 use super::*; 648 use crate::tokenizer::{HashType, NumericType}; 649 650 // -- Length tests -------------------------------------------------------- 651 652 #[test] 653 fn test_parse_px() { 654 let cv = ComponentValue::Dimension(16.0, NumericType::Integer, "px".to_string()); 655 assert_eq!( 656 parse_single_value(&cv), 657 CssValue::Length(16.0, LengthUnit::Px) 658 ); 659 } 660 661 #[test] 662 fn test_parse_em() { 663 let cv = ComponentValue::Dimension(1.5, NumericType::Number, "em".to_string()); 664 assert_eq!( 665 parse_single_value(&cv), 666 CssValue::Length(1.5, LengthUnit::Em) 667 ); 668 } 669 670 #[test] 671 fn test_parse_rem() { 672 let cv = ComponentValue::Dimension(2.0, NumericType::Number, "rem".to_string()); 673 assert_eq!( 674 parse_single_value(&cv), 675 CssValue::Length(2.0, LengthUnit::Rem) 676 ); 677 } 678 679 #[test] 680 fn test_parse_pt() { 681 let cv = ComponentValue::Dimension(12.0, NumericType::Integer, "pt".to_string()); 682 assert_eq!( 683 parse_single_value(&cv), 684 CssValue::Length(12.0, LengthUnit::Pt) 685 ); 686 } 687 688 #[test] 689 fn test_parse_cm() { 690 let cv = ComponentValue::Dimension(2.54, NumericType::Number, "cm".to_string()); 691 assert_eq!( 692 parse_single_value(&cv), 693 CssValue::Length(2.54, LengthUnit::Cm) 694 ); 695 } 696 697 #[test] 698 fn test_parse_mm() { 699 let cv = ComponentValue::Dimension(10.0, NumericType::Integer, "mm".to_string()); 700 assert_eq!( 701 parse_single_value(&cv), 702 CssValue::Length(10.0, LengthUnit::Mm) 703 ); 704 } 705 706 #[test] 707 fn test_parse_in() { 708 let cv = ComponentValue::Dimension(1.0, NumericType::Integer, "in".to_string()); 709 assert_eq!( 710 parse_single_value(&cv), 711 CssValue::Length(1.0, LengthUnit::In) 712 ); 713 } 714 715 #[test] 716 fn test_parse_pc() { 717 let cv = ComponentValue::Dimension(6.0, NumericType::Integer, "pc".to_string()); 718 assert_eq!( 719 parse_single_value(&cv), 720 CssValue::Length(6.0, LengthUnit::Pc) 721 ); 722 } 723 724 #[test] 725 fn test_parse_vw() { 726 let cv = ComponentValue::Dimension(50.0, NumericType::Integer, "vw".to_string()); 727 assert_eq!( 728 parse_single_value(&cv), 729 CssValue::Length(50.0, LengthUnit::Vw) 730 ); 731 } 732 733 #[test] 734 fn test_parse_vh() { 735 let cv = ComponentValue::Dimension(100.0, NumericType::Integer, "vh".to_string()); 736 assert_eq!( 737 parse_single_value(&cv), 738 CssValue::Length(100.0, LengthUnit::Vh) 739 ); 740 } 741 742 #[test] 743 fn test_parse_vmin() { 744 let cv = ComponentValue::Dimension(50.0, NumericType::Integer, "vmin".to_string()); 745 assert_eq!( 746 parse_single_value(&cv), 747 CssValue::Length(50.0, LengthUnit::Vmin) 748 ); 749 } 750 751 #[test] 752 fn test_parse_vmax() { 753 let cv = ComponentValue::Dimension(50.0, NumericType::Integer, "vmax".to_string()); 754 assert_eq!( 755 parse_single_value(&cv), 756 CssValue::Length(50.0, LengthUnit::Vmax) 757 ); 758 } 759 760 #[test] 761 fn test_parse_percentage() { 762 let cv = ComponentValue::Percentage(50.0); 763 assert_eq!(parse_single_value(&cv), CssValue::Percentage(50.0)); 764 } 765 766 #[test] 767 fn test_parse_zero() { 768 let cv = ComponentValue::Number(0.0, NumericType::Integer); 769 assert_eq!(parse_single_value(&cv), CssValue::Zero); 770 } 771 772 #[test] 773 fn test_parse_number() { 774 let cv = ComponentValue::Number(42.0, NumericType::Integer); 775 assert_eq!(parse_single_value(&cv), CssValue::Number(42.0)); 776 } 777 778 // -- Color tests -------------------------------------------------------- 779 780 #[test] 781 fn test_hex_color_3() { 782 let cv = ComponentValue::Hash("f00".to_string(), HashType::Id); 783 assert_eq!( 784 parse_single_value(&cv), 785 CssValue::Color(Color::rgb(255, 0, 0)) 786 ); 787 } 788 789 #[test] 790 fn test_hex_color_4() { 791 let cv = ComponentValue::Hash("f00a".to_string(), HashType::Id); 792 assert_eq!( 793 parse_single_value(&cv), 794 CssValue::Color(Color::new(255, 0, 0, 170)) 795 ); 796 } 797 798 #[test] 799 fn test_hex_color_6() { 800 let cv = ComponentValue::Hash("ff8800".to_string(), HashType::Id); 801 assert_eq!( 802 parse_single_value(&cv), 803 CssValue::Color(Color::rgb(255, 136, 0)) 804 ); 805 } 806 807 #[test] 808 fn test_hex_color_8() { 809 let cv = ComponentValue::Hash("ff880080".to_string(), HashType::Id); 810 assert_eq!( 811 parse_single_value(&cv), 812 CssValue::Color(Color::new(255, 136, 0, 128)) 813 ); 814 } 815 816 #[test] 817 fn test_named_color_red() { 818 let cv = ComponentValue::Ident("red".to_string()); 819 assert_eq!( 820 parse_single_value(&cv), 821 CssValue::Color(Color::rgb(255, 0, 0)) 822 ); 823 } 824 825 #[test] 826 fn test_named_color_blue() { 827 let cv = ComponentValue::Ident("blue".to_string()); 828 assert_eq!( 829 parse_single_value(&cv), 830 CssValue::Color(Color::rgb(0, 0, 255)) 831 ); 832 } 833 834 #[test] 835 fn test_named_color_black() { 836 let cv = ComponentValue::Ident("black".to_string()); 837 assert_eq!( 838 parse_single_value(&cv), 839 CssValue::Color(Color::rgb(0, 0, 0)) 840 ); 841 } 842 843 #[test] 844 fn test_named_color_white() { 845 let cv = ComponentValue::Ident("white".to_string()); 846 assert_eq!( 847 parse_single_value(&cv), 848 CssValue::Color(Color::rgb(255, 255, 255)) 849 ); 850 } 851 852 #[test] 853 fn test_transparent() { 854 let cv = ComponentValue::Ident("transparent".to_string()); 855 assert_eq!(parse_single_value(&cv), CssValue::Transparent); 856 } 857 858 #[test] 859 fn test_current_color() { 860 let cv = ComponentValue::Ident("currentColor".to_string()); 861 assert_eq!(parse_single_value(&cv), CssValue::CurrentColor); 862 } 863 864 #[test] 865 fn test_rgb_function() { 866 let args = vec![ 867 ComponentValue::Number(255.0, NumericType::Integer), 868 ComponentValue::Comma, 869 ComponentValue::Whitespace, 870 ComponentValue::Number(128.0, NumericType::Integer), 871 ComponentValue::Comma, 872 ComponentValue::Whitespace, 873 ComponentValue::Number(0.0, NumericType::Integer), 874 ]; 875 let cv = ComponentValue::Function("rgb".to_string(), args); 876 assert_eq!( 877 parse_single_value(&cv), 878 CssValue::Color(Color::rgb(255, 128, 0)) 879 ); 880 } 881 882 #[test] 883 fn test_rgba_function() { 884 let args = vec![ 885 ComponentValue::Number(255.0, NumericType::Integer), 886 ComponentValue::Comma, 887 ComponentValue::Whitespace, 888 ComponentValue::Number(0.0, NumericType::Integer), 889 ComponentValue::Comma, 890 ComponentValue::Whitespace, 891 ComponentValue::Number(0.0, NumericType::Integer), 892 ComponentValue::Comma, 893 ComponentValue::Whitespace, 894 ComponentValue::Number(0.5, NumericType::Number), 895 ]; 896 let cv = ComponentValue::Function("rgba".to_string(), args); 897 assert_eq!( 898 parse_single_value(&cv), 899 CssValue::Color(Color::new(255, 0, 0, 128)) 900 ); 901 } 902 903 // -- Keyword tests ------------------------------------------------------ 904 905 #[test] 906 fn test_keyword_auto() { 907 let cv = ComponentValue::Ident("auto".to_string()); 908 assert_eq!(parse_single_value(&cv), CssValue::Auto); 909 } 910 911 #[test] 912 fn test_keyword_inherit() { 913 let cv = ComponentValue::Ident("inherit".to_string()); 914 assert_eq!(parse_single_value(&cv), CssValue::Inherit); 915 } 916 917 #[test] 918 fn test_keyword_initial() { 919 let cv = ComponentValue::Ident("initial".to_string()); 920 assert_eq!(parse_single_value(&cv), CssValue::Initial); 921 } 922 923 #[test] 924 fn test_keyword_unset() { 925 let cv = ComponentValue::Ident("unset".to_string()); 926 assert_eq!(parse_single_value(&cv), CssValue::Unset); 927 } 928 929 #[test] 930 fn test_keyword_none() { 931 let cv = ComponentValue::Ident("none".to_string()); 932 assert_eq!(parse_single_value(&cv), CssValue::None); 933 } 934 935 #[test] 936 fn test_keyword_display_block() { 937 let cv = ComponentValue::Ident("block".to_string()); 938 assert_eq!( 939 parse_single_value(&cv), 940 CssValue::Keyword("block".to_string()) 941 ); 942 } 943 944 #[test] 945 fn test_keyword_display_inline() { 946 let cv = ComponentValue::Ident("inline".to_string()); 947 assert_eq!( 948 parse_single_value(&cv), 949 CssValue::Keyword("inline".to_string()) 950 ); 951 } 952 953 #[test] 954 fn test_keyword_display_flex() { 955 let cv = ComponentValue::Ident("flex".to_string()); 956 assert_eq!( 957 parse_single_value(&cv), 958 CssValue::Keyword("flex".to_string()) 959 ); 960 } 961 962 #[test] 963 fn test_keyword_position() { 964 for kw in &["static", "relative", "absolute", "fixed"] { 965 let cv = ComponentValue::Ident(kw.to_string()); 966 assert_eq!(parse_single_value(&cv), CssValue::Keyword(kw.to_string())); 967 } 968 } 969 970 #[test] 971 fn test_keyword_text_align() { 972 for kw in &["left", "center", "right", "justify"] { 973 let cv = ComponentValue::Ident(kw.to_string()); 974 assert_eq!(parse_single_value(&cv), CssValue::Keyword(kw.to_string())); 975 } 976 } 977 978 #[test] 979 fn test_keyword_font_weight() { 980 let cv = ComponentValue::Ident("bold".to_string()); 981 assert_eq!( 982 parse_single_value(&cv), 983 CssValue::Keyword("bold".to_string()) 984 ); 985 let cv = ComponentValue::Ident("normal".to_string()); 986 assert_eq!( 987 parse_single_value(&cv), 988 CssValue::Keyword("normal".to_string()) 989 ); 990 // Numeric font-weight 991 let cv = ComponentValue::Number(700.0, NumericType::Integer); 992 assert_eq!(parse_single_value(&cv), CssValue::Number(700.0)); 993 } 994 995 #[test] 996 fn test_keyword_overflow() { 997 for kw in &["visible", "hidden", "scroll", "auto"] { 998 let cv = ComponentValue::Ident(kw.to_string()); 999 let expected = match *kw { 1000 "auto" => CssValue::Auto, 1001 _ => CssValue::Keyword(kw.to_string()), 1002 }; 1003 assert_eq!(parse_single_value(&cv), expected); 1004 } 1005 } 1006 1007 // -- parse_value (multi-value) ------------------------------------------ 1008 1009 #[test] 1010 fn test_parse_value_single() { 1011 let values = vec![ComponentValue::Ident("red".to_string())]; 1012 assert_eq!(parse_value(&values), CssValue::Color(Color::rgb(255, 0, 0))); 1013 } 1014 1015 #[test] 1016 fn test_parse_value_multi() { 1017 let values = vec![ 1018 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1019 ComponentValue::Whitespace, 1020 ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), 1021 ]; 1022 assert_eq!( 1023 parse_value(&values), 1024 CssValue::List(vec![ 1025 CssValue::Length(10.0, LengthUnit::Px), 1026 CssValue::Length(20.0, LengthUnit::Px), 1027 ]) 1028 ); 1029 } 1030 1031 // -- Shorthand expansion tests ------------------------------------------ 1032 1033 #[test] 1034 fn test_margin_one_value() { 1035 let values = vec![ComponentValue::Dimension( 1036 10.0, 1037 NumericType::Integer, 1038 "px".to_string(), 1039 )]; 1040 let result = expand_shorthand("margin", &values, false).unwrap(); 1041 assert_eq!(result.len(), 4); 1042 for decl in &result { 1043 assert_eq!(decl.value, CssValue::Length(10.0, LengthUnit::Px)); 1044 } 1045 assert_eq!(result[0].property, "margin-top"); 1046 assert_eq!(result[1].property, "margin-right"); 1047 assert_eq!(result[2].property, "margin-bottom"); 1048 assert_eq!(result[3].property, "margin-left"); 1049 } 1050 1051 #[test] 1052 fn test_margin_two_values() { 1053 let values = vec![ 1054 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1055 ComponentValue::Whitespace, 1056 ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), 1057 ]; 1058 let result = expand_shorthand("margin", &values, false).unwrap(); 1059 assert_eq!(result[0].value, CssValue::Length(10.0, LengthUnit::Px)); // top 1060 assert_eq!(result[1].value, CssValue::Length(20.0, LengthUnit::Px)); // right 1061 assert_eq!(result[2].value, CssValue::Length(10.0, LengthUnit::Px)); // bottom 1062 assert_eq!(result[3].value, CssValue::Length(20.0, LengthUnit::Px)); // left 1063 } 1064 1065 #[test] 1066 fn test_margin_three_values() { 1067 let values = vec![ 1068 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1069 ComponentValue::Whitespace, 1070 ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), 1071 ComponentValue::Whitespace, 1072 ComponentValue::Dimension(30.0, NumericType::Integer, "px".to_string()), 1073 ]; 1074 let result = expand_shorthand("margin", &values, false).unwrap(); 1075 assert_eq!(result[0].value, CssValue::Length(10.0, LengthUnit::Px)); // top 1076 assert_eq!(result[1].value, CssValue::Length(20.0, LengthUnit::Px)); // right 1077 assert_eq!(result[2].value, CssValue::Length(30.0, LengthUnit::Px)); // bottom 1078 assert_eq!(result[3].value, CssValue::Length(20.0, LengthUnit::Px)); // left 1079 } 1080 1081 #[test] 1082 fn test_margin_four_values() { 1083 let values = vec![ 1084 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1085 ComponentValue::Whitespace, 1086 ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), 1087 ComponentValue::Whitespace, 1088 ComponentValue::Dimension(30.0, NumericType::Integer, "px".to_string()), 1089 ComponentValue::Whitespace, 1090 ComponentValue::Dimension(40.0, NumericType::Integer, "px".to_string()), 1091 ]; 1092 let result = expand_shorthand("margin", &values, false).unwrap(); 1093 assert_eq!(result[0].value, CssValue::Length(10.0, LengthUnit::Px)); // top 1094 assert_eq!(result[1].value, CssValue::Length(20.0, LengthUnit::Px)); // right 1095 assert_eq!(result[2].value, CssValue::Length(30.0, LengthUnit::Px)); // bottom 1096 assert_eq!(result[3].value, CssValue::Length(40.0, LengthUnit::Px)); // left 1097 } 1098 1099 #[test] 1100 fn test_margin_auto() { 1101 let values = vec![ 1102 ComponentValue::Number(0.0, NumericType::Integer), 1103 ComponentValue::Whitespace, 1104 ComponentValue::Ident("auto".to_string()), 1105 ]; 1106 let result = expand_shorthand("margin", &values, false).unwrap(); 1107 assert_eq!(result[0].value, CssValue::Zero); // top 1108 assert_eq!(result[1].value, CssValue::Auto); // right 1109 assert_eq!(result[2].value, CssValue::Zero); // bottom 1110 assert_eq!(result[3].value, CssValue::Auto); // left 1111 } 1112 1113 #[test] 1114 fn test_padding_shorthand() { 1115 let values = vec![ComponentValue::Dimension( 1116 5.0, 1117 NumericType::Integer, 1118 "px".to_string(), 1119 )]; 1120 let result = expand_shorthand("padding", &values, false).unwrap(); 1121 assert_eq!(result.len(), 4); 1122 assert_eq!(result[0].property, "padding-top"); 1123 assert_eq!(result[1].property, "padding-right"); 1124 assert_eq!(result[2].property, "padding-bottom"); 1125 assert_eq!(result[3].property, "padding-left"); 1126 } 1127 1128 #[test] 1129 fn test_border_shorthand() { 1130 let values = vec![ 1131 ComponentValue::Dimension(1.0, NumericType::Integer, "px".to_string()), 1132 ComponentValue::Whitespace, 1133 ComponentValue::Ident("solid".to_string()), 1134 ComponentValue::Whitespace, 1135 ComponentValue::Ident("red".to_string()), 1136 ]; 1137 let result = expand_shorthand("border", &values, false).unwrap(); 1138 assert_eq!(result.len(), 3); 1139 assert_eq!(result[0].property, "border-width"); 1140 assert_eq!(result[0].value, CssValue::Length(1.0, LengthUnit::Px)); 1141 assert_eq!(result[1].property, "border-style"); 1142 assert_eq!(result[1].value, CssValue::Keyword("solid".to_string())); 1143 assert_eq!(result[2].property, "border-color"); 1144 assert_eq!(result[2].value, CssValue::Color(Color::rgb(255, 0, 0))); 1145 } 1146 1147 #[test] 1148 fn test_border_shorthand_defaults() { 1149 // Just a width 1150 let values = vec![ComponentValue::Dimension( 1151 2.0, 1152 NumericType::Integer, 1153 "px".to_string(), 1154 )]; 1155 let result = expand_shorthand("border", &values, false).unwrap(); 1156 assert_eq!(result[0].value, CssValue::Length(2.0, LengthUnit::Px)); 1157 assert_eq!(result[1].value, CssValue::None); // default style 1158 assert_eq!(result[2].value, CssValue::CurrentColor); // default color 1159 } 1160 1161 #[test] 1162 fn test_background_shorthand_color() { 1163 let values = vec![ComponentValue::Hash("ff0000".to_string(), HashType::Id)]; 1164 let result = expand_shorthand("background", &values, false).unwrap(); 1165 assert_eq!(result.len(), 1); 1166 assert_eq!(result[0].property, "background-color"); 1167 assert_eq!(result[0].value, CssValue::Color(Color::rgb(255, 0, 0))); 1168 } 1169 1170 #[test] 1171 fn test_non_shorthand_returns_none() { 1172 let values = vec![ComponentValue::Ident("red".to_string())]; 1173 assert!(expand_shorthand("color", &values, false).is_none()); 1174 } 1175 1176 #[test] 1177 fn test_important_propagated() { 1178 let values = vec![ComponentValue::Dimension( 1179 10.0, 1180 NumericType::Integer, 1181 "px".to_string(), 1182 )]; 1183 let result = expand_shorthand("margin", &values, true).unwrap(); 1184 for decl in &result { 1185 assert!(decl.important); 1186 } 1187 } 1188 1189 // -- Integration: parse from CSS text ----------------------------------- 1190 1191 #[test] 1192 fn test_parse_from_css_text() { 1193 use crate::parser::Parser; 1194 1195 let ss = Parser::parse("p { color: red; margin: 10px 20px; }"); 1196 let rule = match &ss.rules[0] { 1197 crate::parser::Rule::Style(r) => r, 1198 _ => panic!("expected style rule"), 1199 }; 1200 1201 // color: red 1202 let color_val = parse_value(&rule.declarations[0].value); 1203 assert_eq!(color_val, CssValue::Color(Color::rgb(255, 0, 0))); 1204 1205 // margin: 10px 20px (multi-value) 1206 let margin_val = parse_value(&rule.declarations[1].value); 1207 assert_eq!( 1208 margin_val, 1209 CssValue::List(vec![ 1210 CssValue::Length(10.0, LengthUnit::Px), 1211 CssValue::Length(20.0, LengthUnit::Px), 1212 ]) 1213 ); 1214 } 1215 1216 #[test] 1217 fn test_shorthand_from_css_text() { 1218 use crate::parser::Parser; 1219 1220 let ss = Parser::parse("div { margin: 10px 20px 30px 40px; }"); 1221 let rule = match &ss.rules[0] { 1222 crate::parser::Rule::Style(r) => r, 1223 _ => panic!("expected style rule"), 1224 }; 1225 1226 let longhands = expand_shorthand( 1227 &rule.declarations[0].property, 1228 &rule.declarations[0].value, 1229 rule.declarations[0].important, 1230 ) 1231 .unwrap(); 1232 1233 assert_eq!(longhands[0].property, "margin-top"); 1234 assert_eq!(longhands[0].value, CssValue::Length(10.0, LengthUnit::Px)); 1235 assert_eq!(longhands[1].property, "margin-right"); 1236 assert_eq!(longhands[1].value, CssValue::Length(20.0, LengthUnit::Px)); 1237 assert_eq!(longhands[2].property, "margin-bottom"); 1238 assert_eq!(longhands[2].value, CssValue::Length(30.0, LengthUnit::Px)); 1239 assert_eq!(longhands[3].property, "margin-left"); 1240 assert_eq!(longhands[3].value, CssValue::Length(40.0, LengthUnit::Px)); 1241 } 1242 1243 #[test] 1244 fn test_case_insensitive_units() { 1245 let cv = ComponentValue::Dimension(16.0, NumericType::Integer, "PX".to_string()); 1246 assert_eq!( 1247 parse_single_value(&cv), 1248 CssValue::Length(16.0, LengthUnit::Px) 1249 ); 1250 } 1251 1252 #[test] 1253 fn test_case_insensitive_color_name() { 1254 let cv = ComponentValue::Ident("RED".to_string()); 1255 assert_eq!( 1256 parse_single_value(&cv), 1257 CssValue::Color(Color::rgb(255, 0, 0)) 1258 ); 1259 } 1260 1261 #[test] 1262 fn test_case_insensitive_keywords() { 1263 let cv = ComponentValue::Ident("AUTO".to_string()); 1264 assert_eq!(parse_single_value(&cv), CssValue::Auto); 1265 1266 let cv = ComponentValue::Ident("INHERIT".to_string()); 1267 assert_eq!(parse_single_value(&cv), CssValue::Inherit); 1268 } 1269 1270 #[test] 1271 fn test_named_color_grey_alias() { 1272 let cv = ComponentValue::Ident("grey".to_string()); 1273 assert_eq!( 1274 parse_single_value(&cv), 1275 CssValue::Color(Color::rgb(128, 128, 128)) 1276 ); 1277 } 1278 1279 #[test] 1280 fn test_named_color_all_16_plus() { 1281 let colors = vec![ 1282 ("black", 0, 0, 0), 1283 ("silver", 192, 192, 192), 1284 ("gray", 128, 128, 128), 1285 ("white", 255, 255, 255), 1286 ("maroon", 128, 0, 0), 1287 ("red", 255, 0, 0), 1288 ("purple", 128, 0, 128), 1289 ("fuchsia", 255, 0, 255), 1290 ("green", 0, 128, 0), 1291 ("lime", 0, 255, 0), 1292 ("olive", 128, 128, 0), 1293 ("yellow", 255, 255, 0), 1294 ("navy", 0, 0, 128), 1295 ("blue", 0, 0, 255), 1296 ("teal", 0, 128, 128), 1297 ("aqua", 0, 255, 255), 1298 ("orange", 255, 165, 0), 1299 ]; 1300 for (name, r, g, b) in colors { 1301 let cv = ComponentValue::Ident(name.to_string()); 1302 assert_eq!( 1303 parse_single_value(&cv), 1304 CssValue::Color(Color::rgb(r, g, b)), 1305 "failed for {name}" 1306 ); 1307 } 1308 } 1309 1310 #[test] 1311 fn test_border_width_shorthand() { 1312 let values = vec![ 1313 ComponentValue::Dimension(1.0, NumericType::Integer, "px".to_string()), 1314 ComponentValue::Whitespace, 1315 ComponentValue::Dimension(2.0, NumericType::Integer, "px".to_string()), 1316 ]; 1317 let result = expand_shorthand("border-width", &values, false).unwrap(); 1318 assert_eq!(result.len(), 4); 1319 assert_eq!(result[0].property, "border-top-width"); 1320 assert_eq!(result[1].property, "border-right-width"); 1321 } 1322 1323 #[test] 1324 fn test_border_style_shorthand() { 1325 let values = vec![ComponentValue::Ident("solid".to_string())]; 1326 let result = expand_shorthand("border-style", &values, false).unwrap(); 1327 assert_eq!(result.len(), 4); 1328 assert_eq!(result[0].property, "border-top-style"); 1329 } 1330 1331 #[test] 1332 fn test_border_color_shorthand() { 1333 let values = vec![ComponentValue::Ident("red".to_string())]; 1334 let result = expand_shorthand("border-color", &values, false).unwrap(); 1335 assert_eq!(result.len(), 4); 1336 assert_eq!(result[0].property, "border-top-color"); 1337 } 1338 1339 #[test] 1340 fn test_string_value() { 1341 let cv = ComponentValue::String("hello".to_string()); 1342 assert_eq!( 1343 parse_single_value(&cv), 1344 CssValue::String("hello".to_string()) 1345 ); 1346 } 1347}