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 2244 lines 75 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 /// A CSS math function expression: calc(), min(), max(), clamp(). 45 Math(Box<MathExpr>), 46} 47 48// --------------------------------------------------------------------------- 49// CSS math expression AST (calc, min, max, clamp) 50// --------------------------------------------------------------------------- 51 52/// A node in a CSS math expression tree. 53#[derive(Debug, Clone, PartialEq)] 54pub enum MathExpr { 55 /// A literal length value. 56 Length(f64, LengthUnit), 57 /// A literal percentage value. 58 Percentage(f64), 59 /// A literal unitless number. 60 Number(f64), 61 /// Addition: `a + b`. 62 Add(Box<MathExpr>, Box<MathExpr>), 63 /// Subtraction: `a - b`. 64 Sub(Box<MathExpr>, Box<MathExpr>), 65 /// Multiplication: `a * b` (one operand should be a number). 66 Mul(Box<MathExpr>, Box<MathExpr>), 67 /// Division: `a / b` (divisor should be a number). 68 Div(Box<MathExpr>, Box<MathExpr>), 69 /// `min(a, b, ...)` — returns the smallest value. 70 Min(Vec<MathExpr>), 71 /// `max(a, b, ...)` — returns the largest value. 72 Max(Vec<MathExpr>), 73 /// `clamp(min, val, max)` — equivalent to `max(min, min(val, max))`. 74 Clamp(Box<MathExpr>, Box<MathExpr>, Box<MathExpr>), 75} 76 77/// CSS length unit. 78#[derive(Debug, Clone, Copy, PartialEq, Eq)] 79pub enum LengthUnit { 80 // Absolute 81 Px, 82 Pt, 83 Cm, 84 Mm, 85 In, 86 Pc, 87 // Font-relative 88 Em, 89 Rem, 90 // Viewport 91 Vw, 92 Vh, 93 Vmin, 94 Vmax, 95} 96 97/// A CSS color in RGBA format. 98#[derive(Debug, Clone, Copy, PartialEq, Eq)] 99pub struct Color { 100 pub r: u8, 101 pub g: u8, 102 pub b: u8, 103 pub a: u8, 104} 105 106impl Color { 107 pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { 108 Self { r, g, b, a } 109 } 110 111 pub fn rgb(r: u8, g: u8, b: u8) -> Self { 112 Self { r, g, b, a: 255 } 113 } 114} 115 116// --------------------------------------------------------------------------- 117// Shorthand expansion result 118// --------------------------------------------------------------------------- 119 120/// A longhand property-value pair produced by shorthand expansion. 121#[derive(Debug, Clone, PartialEq)] 122pub struct LonghandDeclaration { 123 pub property: String, 124 pub value: CssValue, 125 pub important: bool, 126} 127 128// --------------------------------------------------------------------------- 129// Value parsing 130// --------------------------------------------------------------------------- 131 132/// Parse a single `CssValue` from a list of component values. 133pub fn parse_value(values: &[ComponentValue]) -> CssValue { 134 // Filter out whitespace for easier matching 135 let non_ws: Vec<&ComponentValue> = values 136 .iter() 137 .filter(|v| !matches!(v, ComponentValue::Whitespace)) 138 .collect(); 139 140 if non_ws.is_empty() { 141 return CssValue::Keyword(String::new()); 142 } 143 144 // Single-value case 145 if non_ws.len() == 1 { 146 return parse_single_value(non_ws[0]); 147 } 148 149 // Multi-value: parse each non-whitespace value 150 let parsed: Vec<CssValue> = non_ws.iter().copied().map(parse_single_value).collect(); 151 CssValue::List(parsed) 152} 153 154/// Parse a single component value into a `CssValue`. 155pub fn parse_single_value(cv: &ComponentValue) -> CssValue { 156 match cv { 157 ComponentValue::Ident(s) => parse_keyword(s), 158 ComponentValue::String(s) => CssValue::String(s.clone()), 159 ComponentValue::Number(n, _) => { 160 if *n == 0.0 { 161 CssValue::Zero 162 } else { 163 CssValue::Number(*n) 164 } 165 } 166 ComponentValue::Percentage(n) => CssValue::Percentage(*n), 167 ComponentValue::Dimension(n, _, unit) => parse_dimension(*n, unit), 168 ComponentValue::Hash(s, _) => parse_hex_color(s), 169 ComponentValue::Function(name, args) => parse_function(name, args), 170 ComponentValue::Comma => CssValue::Keyword(",".to_string()), 171 ComponentValue::Delim(c) => CssValue::Keyword(c.to_string()), 172 ComponentValue::Whitespace => CssValue::Keyword(" ".to_string()), 173 } 174} 175 176fn parse_keyword(s: &str) -> CssValue { 177 match s.to_ascii_lowercase().as_str() { 178 "auto" => CssValue::Auto, 179 "inherit" => CssValue::Inherit, 180 "initial" => CssValue::Initial, 181 "unset" => CssValue::Unset, 182 "none" => CssValue::None, 183 "transparent" => CssValue::Transparent, 184 "currentcolor" => CssValue::CurrentColor, 185 // Named colors 186 name => { 187 if let Some(color) = named_color(name) { 188 CssValue::Color(color) 189 } else { 190 CssValue::Keyword(s.to_ascii_lowercase()) 191 } 192 } 193 } 194} 195 196fn parse_dimension(n: f64, unit: &str) -> CssValue { 197 let u = unit.to_ascii_lowercase(); 198 match u.as_str() { 199 "px" => CssValue::Length(n, LengthUnit::Px), 200 "pt" => CssValue::Length(n, LengthUnit::Pt), 201 "cm" => CssValue::Length(n, LengthUnit::Cm), 202 "mm" => CssValue::Length(n, LengthUnit::Mm), 203 "in" => CssValue::Length(n, LengthUnit::In), 204 "pc" => CssValue::Length(n, LengthUnit::Pc), 205 "em" => CssValue::Length(n, LengthUnit::Em), 206 "rem" => CssValue::Length(n, LengthUnit::Rem), 207 "vw" => CssValue::Length(n, LengthUnit::Vw), 208 "vh" => CssValue::Length(n, LengthUnit::Vh), 209 "vmin" => CssValue::Length(n, LengthUnit::Vmin), 210 "vmax" => CssValue::Length(n, LengthUnit::Vmax), 211 _ => CssValue::Keyword(format!("{n}{u}")), 212 } 213} 214 215// --------------------------------------------------------------------------- 216// Color parsing 217// --------------------------------------------------------------------------- 218 219fn parse_hex_color(hex: &str) -> CssValue { 220 let chars: Vec<char> = hex.chars().collect(); 221 match chars.len() { 222 // #rgb 223 3 => { 224 let r = hex_digit(chars[0]) * 17; 225 let g = hex_digit(chars[1]) * 17; 226 let b = hex_digit(chars[2]) * 17; 227 CssValue::Color(Color::rgb(r, g, b)) 228 } 229 // #rgba 230 4 => { 231 let r = hex_digit(chars[0]) * 17; 232 let g = hex_digit(chars[1]) * 17; 233 let b = hex_digit(chars[2]) * 17; 234 let a = hex_digit(chars[3]) * 17; 235 CssValue::Color(Color::new(r, g, b, a)) 236 } 237 // #rrggbb 238 6 => { 239 let r = hex_byte(chars[0], chars[1]); 240 let g = hex_byte(chars[2], chars[3]); 241 let b = hex_byte(chars[4], chars[5]); 242 CssValue::Color(Color::rgb(r, g, b)) 243 } 244 // #rrggbbaa 245 8 => { 246 let r = hex_byte(chars[0], chars[1]); 247 let g = hex_byte(chars[2], chars[3]); 248 let b = hex_byte(chars[4], chars[5]); 249 let a = hex_byte(chars[6], chars[7]); 250 CssValue::Color(Color::new(r, g, b, a)) 251 } 252 _ => CssValue::Keyword(format!("#{hex}")), 253 } 254} 255 256fn hex_digit(c: char) -> u8 { 257 match c { 258 '0'..='9' => c as u8 - b'0', 259 'a'..='f' => c as u8 - b'a' + 10, 260 'A'..='F' => c as u8 - b'A' + 10, 261 _ => 0, 262 } 263} 264 265fn hex_byte(hi: char, lo: char) -> u8 { 266 hex_digit(hi) * 16 + hex_digit(lo) 267} 268 269fn parse_function(name: &str, args: &[ComponentValue]) -> CssValue { 270 match name.to_ascii_lowercase().as_str() { 271 "rgb" => parse_rgb(args, false), 272 "rgba" => parse_rgb(args, true), 273 "calc" => parse_math_calc(args), 274 "min" => parse_math_min(args), 275 "max" => parse_math_max(args), 276 "clamp" => parse_math_clamp(args), 277 _ => CssValue::Keyword(format!("{name}()")), 278 } 279} 280 281// --------------------------------------------------------------------------- 282// CSS math function parsing (calc, min, max, clamp) 283// --------------------------------------------------------------------------- 284 285fn parse_math_calc(args: &[ComponentValue]) -> CssValue { 286 let tokens = filter_math_tokens(args); 287 match parse_sum(&tokens) { 288 Some((expr, [])) => CssValue::Math(Box::new(expr)), 289 _ => CssValue::Keyword("calc()".to_string()), 290 } 291} 292 293fn parse_math_min(args: &[ComponentValue]) -> CssValue { 294 let exprs = parse_comma_separated_exprs(args); 295 if exprs.is_empty() { 296 return CssValue::Keyword("min()".to_string()); 297 } 298 CssValue::Math(Box::new(MathExpr::Min(exprs))) 299} 300 301fn parse_math_max(args: &[ComponentValue]) -> CssValue { 302 let exprs = parse_comma_separated_exprs(args); 303 if exprs.is_empty() { 304 return CssValue::Keyword("max()".to_string()); 305 } 306 CssValue::Math(Box::new(MathExpr::Max(exprs))) 307} 308 309fn parse_math_clamp(args: &[ComponentValue]) -> CssValue { 310 let exprs = parse_comma_separated_exprs(args); 311 if exprs.len() != 3 { 312 return CssValue::Keyword("clamp()".to_string()); 313 } 314 let mut iter = exprs.into_iter(); 315 let min = iter.next().unwrap(); 316 let val = iter.next().unwrap(); 317 let max = iter.next().unwrap(); 318 CssValue::Math(Box::new(MathExpr::Clamp( 319 Box::new(min), 320 Box::new(val), 321 Box::new(max), 322 ))) 323} 324 325/// Intermediate token for math expression parsing. 326#[derive(Debug, Clone)] 327enum MathToken { 328 Number(f64), 329 Dimension(f64, LengthUnit), 330 Percentage(f64), 331 Plus, 332 Minus, 333 Star, 334 Slash, 335 /// A pre-parsed nested math expression (e.g., nested min/max/clamp/calc). 336 Nested(MathExpr), 337} 338 339/// Convert ComponentValues into MathTokens, filtering whitespace but preserving 340/// it around +/- operators (whitespace is required around + and - per spec). 341fn filter_math_tokens(args: &[ComponentValue]) -> Vec<MathToken> { 342 let mut tokens = Vec::new(); 343 let mut i = 0; 344 while i < args.len() { 345 match &args[i] { 346 ComponentValue::Number(n, _) => tokens.push(MathToken::Number(*n)), 347 ComponentValue::Percentage(n) => tokens.push(MathToken::Percentage(*n)), 348 ComponentValue::Dimension(n, _, unit) => { 349 if let Some(lu) = parse_length_unit(unit) { 350 tokens.push(MathToken::Dimension(*n, lu)); 351 } 352 } 353 ComponentValue::Delim('+') => tokens.push(MathToken::Plus), 354 ComponentValue::Delim('-') => tokens.push(MathToken::Minus), 355 ComponentValue::Delim('*') => tokens.push(MathToken::Star), 356 ComponentValue::Delim('/') => tokens.push(MathToken::Slash), 357 ComponentValue::Function(name, inner_args) => { 358 let lower = name.to_ascii_lowercase(); 359 let nested = match lower.as_str() { 360 "calc" => { 361 let inner_tokens = filter_math_tokens(inner_args); 362 parse_sum(&inner_tokens).and_then(|(e, r)| { 363 if r.is_empty() { 364 Some(e) 365 } else { 366 Option::None 367 } 368 }) 369 } 370 "min" => { 371 let exprs = parse_comma_separated_exprs(inner_args); 372 if exprs.is_empty() { 373 Option::None 374 } else { 375 Some(MathExpr::Min(exprs)) 376 } 377 } 378 "max" => { 379 let exprs = parse_comma_separated_exprs(inner_args); 380 if exprs.is_empty() { 381 Option::None 382 } else { 383 Some(MathExpr::Max(exprs)) 384 } 385 } 386 "clamp" => { 387 let exprs = parse_comma_separated_exprs(inner_args); 388 if exprs.len() == 3 { 389 let mut it = exprs.into_iter(); 390 Some(MathExpr::Clamp( 391 Box::new(it.next().unwrap()), 392 Box::new(it.next().unwrap()), 393 Box::new(it.next().unwrap()), 394 )) 395 } else { 396 Option::None 397 } 398 } 399 _ => Option::None, 400 }; 401 if let Some(expr) = nested { 402 tokens.push(MathToken::Nested(expr)); 403 } 404 } 405 ComponentValue::Whitespace | ComponentValue::Comma => {} 406 _ => {} 407 } 408 i += 1; 409 } 410 tokens 411} 412 413fn parse_length_unit(unit: &str) -> Option<LengthUnit> { 414 match unit.to_ascii_lowercase().as_str() { 415 "px" => Some(LengthUnit::Px), 416 "pt" => Some(LengthUnit::Pt), 417 "cm" => Some(LengthUnit::Cm), 418 "mm" => Some(LengthUnit::Mm), 419 "in" => Some(LengthUnit::In), 420 "pc" => Some(LengthUnit::Pc), 421 "em" => Some(LengthUnit::Em), 422 "rem" => Some(LengthUnit::Rem), 423 "vw" => Some(LengthUnit::Vw), 424 "vh" => Some(LengthUnit::Vh), 425 "vmin" => Some(LengthUnit::Vmin), 426 "vmax" => Some(LengthUnit::Vmax), 427 _ => Option::None, 428 } 429} 430 431/// Parse comma-separated math expressions (for min, max, clamp arguments). 432fn parse_comma_separated_exprs(args: &[ComponentValue]) -> Vec<MathExpr> { 433 // Split on commas at the top level, then parse each group as a sum expression. 434 let mut groups: Vec<Vec<&ComponentValue>> = vec![vec![]]; 435 for cv in args { 436 if matches!(cv, ComponentValue::Comma) { 437 groups.push(vec![]); 438 } else { 439 groups.last_mut().unwrap().push(cv); 440 } 441 } 442 443 let mut results = Vec::new(); 444 for group in groups { 445 let owned: Vec<ComponentValue> = group.into_iter().cloned().collect(); 446 let tokens = filter_math_tokens(&owned); 447 if let Some((expr, rest)) = parse_sum(&tokens) { 448 if rest.is_empty() { 449 results.push(expr); 450 } 451 } 452 } 453 results 454} 455 456/// Parse an additive expression: `product (('+' | '-') product)*`. 457/// Per spec, whitespace is required around `+` and `-`. 458fn parse_sum(tokens: &[MathToken]) -> Option<(MathExpr, &[MathToken])> { 459 let (mut left, mut rest) = parse_product(tokens)?; 460 while !rest.is_empty() { 461 match rest.first() { 462 Some(MathToken::Plus) => { 463 let (right, r) = parse_product(&rest[1..])?; 464 left = MathExpr::Add(Box::new(left), Box::new(right)); 465 rest = r; 466 } 467 Some(MathToken::Minus) => { 468 let (right, r) = parse_product(&rest[1..])?; 469 left = MathExpr::Sub(Box::new(left), Box::new(right)); 470 rest = r; 471 } 472 _ => break, 473 } 474 } 475 Some((left, rest)) 476} 477 478/// Parse a multiplicative expression: `atom (('*' | '/') atom)*`. 479fn parse_product(tokens: &[MathToken]) -> Option<(MathExpr, &[MathToken])> { 480 let (mut left, mut rest) = parse_atom(tokens)?; 481 while !rest.is_empty() { 482 match rest.first() { 483 Some(MathToken::Star) => { 484 let (right, r) = parse_atom(&rest[1..])?; 485 left = MathExpr::Mul(Box::new(left), Box::new(right)); 486 rest = r; 487 } 488 Some(MathToken::Slash) => { 489 let (right, r) = parse_atom(&rest[1..])?; 490 left = MathExpr::Div(Box::new(left), Box::new(right)); 491 rest = r; 492 } 493 _ => break, 494 } 495 } 496 Some((left, rest)) 497} 498 499/// Parse an atom: a number, dimension, percentage, parenthesized expression, 500/// or nested math function. 501fn parse_atom(tokens: &[MathToken]) -> Option<(MathExpr, &[MathToken])> { 502 let first = tokens.first()?; 503 match first { 504 MathToken::Number(n) => Some((MathExpr::Number(*n), &tokens[1..])), 505 MathToken::Dimension(n, unit) => Some((MathExpr::Length(*n, *unit), &tokens[1..])), 506 MathToken::Percentage(n) => Some((MathExpr::Percentage(*n), &tokens[1..])), 507 MathToken::Nested(expr) => Some((expr.clone(), &tokens[1..])), 508 // Handle unary minus: e.g. `-20px` tokenized as Minus followed by value. 509 MathToken::Minus => { 510 let (inner, rest) = parse_atom(&tokens[1..])?; 511 let negated = MathExpr::Mul(Box::new(MathExpr::Number(-1.0)), Box::new(inner)); 512 Some((negated, rest)) 513 } 514 _ => Option::None, 515 } 516} 517 518fn parse_rgb(args: &[ComponentValue], _has_alpha: bool) -> CssValue { 519 let nums: Vec<f64> = args 520 .iter() 521 .filter_map(|cv| match cv { 522 ComponentValue::Number(n, _) => Some(*n), 523 ComponentValue::Percentage(n) => Some(*n * 2.55), 524 _ => Option::None, 525 }) 526 .collect(); 527 528 match nums.len() { 529 3 => CssValue::Color(Color::rgb( 530 clamp_u8(nums[0]), 531 clamp_u8(nums[1]), 532 clamp_u8(nums[2]), 533 )), 534 4 => { 535 let a = if args 536 .iter() 537 .any(|cv| matches!(cv, ComponentValue::Percentage(_))) 538 { 539 // If any arg is a percentage, treat alpha as 0-1 float 540 clamp_u8(nums[3] / 2.55 * 255.0) 541 } else { 542 // Check if alpha looks like a 0-1 range 543 if nums[3] <= 1.0 { 544 clamp_u8(nums[3] * 255.0) 545 } else { 546 clamp_u8(nums[3]) 547 } 548 }; 549 CssValue::Color(Color::new( 550 clamp_u8(nums[0]), 551 clamp_u8(nums[1]), 552 clamp_u8(nums[2]), 553 a, 554 )) 555 } 556 _ => CssValue::Keyword("rgb()".to_string()), 557 } 558} 559 560fn clamp_u8(n: f64) -> u8 { 561 n.round().clamp(0.0, 255.0) as u8 562} 563 564// --------------------------------------------------------------------------- 565// Named colors (CSS Level 1 + transparent) 566// --------------------------------------------------------------------------- 567 568pub fn named_color(name: &str) -> Option<Color> { 569 Some(match name { 570 "black" => Color::rgb(0, 0, 0), 571 "silver" => Color::rgb(192, 192, 192), 572 "gray" | "grey" => Color::rgb(128, 128, 128), 573 "white" => Color::rgb(255, 255, 255), 574 "maroon" => Color::rgb(128, 0, 0), 575 "red" => Color::rgb(255, 0, 0), 576 "purple" => Color::rgb(128, 0, 128), 577 "fuchsia" | "magenta" => Color::rgb(255, 0, 255), 578 "green" => Color::rgb(0, 128, 0), 579 "lime" => Color::rgb(0, 255, 0), 580 "olive" => Color::rgb(128, 128, 0), 581 "yellow" => Color::rgb(255, 255, 0), 582 "navy" => Color::rgb(0, 0, 128), 583 "blue" => Color::rgb(0, 0, 255), 584 "teal" => Color::rgb(0, 128, 128), 585 "aqua" | "cyan" => Color::rgb(0, 255, 255), 586 "orange" => Color::rgb(255, 165, 0), 587 _ => return Option::None, 588 }) 589} 590 591// --------------------------------------------------------------------------- 592// Canvas color string parsing 593// --------------------------------------------------------------------------- 594 595/// Parse a CSS color string as used by the Canvas 2D API (`fillStyle`, 596/// `strokeStyle`). Supports `#rgb`, `#rrggbb`, `#rgba`, `#rrggbbaa`, 597/// `rgb()`, `rgba()`, named colors, and `transparent`. 598/// 599/// Returns `None` for invalid / unrecognised strings (per spec, the previous 600/// value should be kept). 601pub fn parse_color_string(s: &str) -> Option<Color> { 602 let s = s.trim(); 603 if s.is_empty() { 604 return None; 605 } 606 607 // "transparent" 608 if s.eq_ignore_ascii_case("transparent") { 609 return Some(Color::new(0, 0, 0, 0)); 610 } 611 612 // Hex: #rgb, #rgba, #rrggbb, #rrggbbaa 613 if let Some(hex) = s.strip_prefix('#') { 614 // Validate all characters are hex digits before parsing. 615 if !hex.chars().all(|c| c.is_ascii_hexdigit()) { 616 return None; 617 } 618 return match parse_hex_color(hex) { 619 CssValue::Color(c) => Some(c), 620 _ => None, 621 }; 622 } 623 624 // rgb(...) / rgba(...) 625 let lower = s.to_ascii_lowercase(); 626 if lower.starts_with("rgb") { 627 // Extract contents between parens 628 if let Some(start) = s.find('(') { 629 if let Some(end) = s.rfind(')') { 630 if start < end { 631 let inner = &s[start + 1..end]; 632 let args = tokenize_color_args(inner); 633 let result = parse_rgb(&args, lower.starts_with("rgba")); 634 return match result { 635 CssValue::Color(c) => Some(c), 636 _ => None, 637 }; 638 } 639 } 640 } 641 return None; 642 } 643 644 // Named colors 645 named_color(&lower) 646} 647 648/// Tokenize the inner arguments of `rgb()` / `rgba()` into `ComponentValue`s 649/// that `parse_rgb` expects. 650fn tokenize_color_args(s: &str) -> Vec<ComponentValue> { 651 use crate::tokenizer::NumericType; 652 let mut result = Vec::new(); 653 for part in s.split(|c: char| c == ',' || c.is_ascii_whitespace()) { 654 let part = part.trim(); 655 if part.is_empty() { 656 continue; 657 } 658 if let Some(pct) = part.strip_suffix('%') { 659 if let Ok(n) = pct.parse::<f64>() { 660 result.push(ComponentValue::Percentage(n)); 661 continue; 662 } 663 } 664 if let Ok(n) = part.parse::<f64>() { 665 let nt = if part.contains('.') { 666 NumericType::Number 667 } else { 668 NumericType::Integer 669 }; 670 result.push(ComponentValue::Number(n, nt)); 671 continue; 672 } 673 } 674 result 675} 676 677// --------------------------------------------------------------------------- 678// Shorthand expansion 679// --------------------------------------------------------------------------- 680 681/// Expand a CSS declaration into longhand declarations. 682/// Returns `None` if the property is not a known shorthand. 683pub fn expand_shorthand( 684 property: &str, 685 values: &[ComponentValue], 686 important: bool, 687) -> Option<Vec<LonghandDeclaration>> { 688 match property { 689 "margin" => Some(expand_box_shorthand("margin", values, important)), 690 "padding" => Some(expand_box_shorthand("padding", values, important)), 691 "border" => Some(expand_border(values, important)), 692 "border-width" => Some( 693 expand_box_shorthand("border", values, important) 694 .into_iter() 695 .map(|mut d| { 696 d.property = format!("{}-width", d.property); 697 d 698 }) 699 .collect(), 700 ), 701 "border-style" => Some( 702 expand_box_shorthand("border", values, important) 703 .into_iter() 704 .map(|mut d| { 705 d.property = format!("{}-style", d.property); 706 d 707 }) 708 .collect(), 709 ), 710 "border-color" => Some( 711 expand_box_shorthand("border", values, important) 712 .into_iter() 713 .map(|mut d| { 714 d.property = format!("{}-color", d.property); 715 d 716 }) 717 .collect(), 718 ), 719 "background" => Some(expand_background(values, important)), 720 "flex" => Some(expand_flex(values, important)), 721 "flex-flow" => Some(expand_flex_flow(values, important)), 722 "gap" => Some(expand_gap(values, important)), 723 "grid-column" => Some(expand_grid_line("grid-column", values, important)), 724 "grid-row" => Some(expand_grid_line("grid-row", values, important)), 725 "grid-area" => Some(expand_grid_area(values, important)), 726 _ => Option::None, 727 } 728} 729 730/// Expand the `flex` shorthand into `flex-grow`, `flex-shrink`, `flex-basis`. 731fn expand_flex(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 732 let parsed: Vec<CssValue> = values 733 .iter() 734 .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) 735 .map(parse_single_value) 736 .collect(); 737 738 let (grow, shrink, basis) = match parsed.as_slice() { 739 [CssValue::None] => (CssValue::Number(0.0), CssValue::Number(0.0), CssValue::Auto), 740 [CssValue::Auto] => (CssValue::Number(1.0), CssValue::Number(1.0), CssValue::Auto), 741 [CssValue::Number(g)] => (CssValue::Number(*g), CssValue::Number(1.0), CssValue::Zero), 742 [CssValue::Zero] => (CssValue::Number(0.0), CssValue::Number(1.0), CssValue::Zero), 743 [CssValue::Number(g), CssValue::Number(s)] => { 744 (CssValue::Number(*g), CssValue::Number(*s), CssValue::Zero) 745 } 746 [CssValue::Number(g), CssValue::Number(s), basis] => { 747 (CssValue::Number(*g), CssValue::Number(*s), basis.clone()) 748 } 749 _ => (CssValue::Number(0.0), CssValue::Number(1.0), CssValue::Auto), 750 }; 751 752 vec![ 753 LonghandDeclaration { 754 property: "flex-grow".to_string(), 755 value: grow, 756 important, 757 }, 758 LonghandDeclaration { 759 property: "flex-shrink".to_string(), 760 value: shrink, 761 important, 762 }, 763 LonghandDeclaration { 764 property: "flex-basis".to_string(), 765 value: basis, 766 important, 767 }, 768 ] 769} 770 771/// Expand the `flex-flow` shorthand into `flex-direction` and `flex-wrap`. 772fn expand_flex_flow(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 773 let parsed: Vec<CssValue> = values 774 .iter() 775 .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) 776 .map(parse_single_value) 777 .collect(); 778 779 let mut direction = CssValue::Keyword("row".to_string()); 780 let mut wrap = CssValue::Keyword("nowrap".to_string()); 781 782 for val in &parsed { 783 if let CssValue::Keyword(k) = val { 784 match k.as_str() { 785 "row" | "row-reverse" | "column" | "column-reverse" => { 786 direction = val.clone(); 787 } 788 "nowrap" | "wrap" | "wrap-reverse" => { 789 wrap = val.clone(); 790 } 791 _ => {} 792 } 793 } 794 } 795 796 vec![ 797 LonghandDeclaration { 798 property: "flex-direction".to_string(), 799 value: direction, 800 important, 801 }, 802 LonghandDeclaration { 803 property: "flex-wrap".to_string(), 804 value: wrap, 805 important, 806 }, 807 ] 808} 809 810/// Expand the `gap` shorthand into `row-gap` and `column-gap`. 811fn expand_gap(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 812 let parsed: Vec<CssValue> = values 813 .iter() 814 .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) 815 .map(parse_single_value) 816 .collect(); 817 818 let (row, col) = match parsed.as_slice() { 819 [single] => (single.clone(), single.clone()), 820 [r, c] => (r.clone(), c.clone()), 821 _ => (CssValue::Zero, CssValue::Zero), 822 }; 823 824 vec![ 825 LonghandDeclaration { 826 property: "row-gap".to_string(), 827 value: row, 828 important, 829 }, 830 LonghandDeclaration { 831 property: "column-gap".to_string(), 832 value: col, 833 important, 834 }, 835 ] 836} 837 838/// Expand `grid-column` or `grid-row` shorthand into start/end longhands. 839/// 840/// `grid-column: 1 / span 2` → grid-column-start: 1; grid-column-end: span 2; 841/// `grid-column: 2` → grid-column-start: 2; grid-column-end: auto; 842fn expand_grid_line( 843 property: &str, 844 values: &[ComponentValue], 845 important: bool, 846) -> Vec<LonghandDeclaration> { 847 let (start_prop, end_prop) = if property == "grid-column" { 848 ("grid-column-start", "grid-column-end") 849 } else { 850 ("grid-row-start", "grid-row-end") 851 }; 852 853 let groups = split_at_slash(values); 854 let start_value = parse_grid_line_group(&groups[0]); 855 let end_value = if groups.len() > 1 { 856 parse_grid_line_group(&groups[1]) 857 } else { 858 CssValue::Auto 859 }; 860 861 vec![ 862 LonghandDeclaration { 863 property: start_prop.to_string(), 864 value: start_value, 865 important, 866 }, 867 LonghandDeclaration { 868 property: end_prop.to_string(), 869 value: end_value, 870 important, 871 }, 872 ] 873} 874 875/// Expand `grid-area` shorthand into row-start, column-start, row-end, column-end. 876/// 877/// `grid-area: 1 / 2 / 3 / 4` → all four longhands. 878/// Missing values default to `auto`. 879fn expand_grid_area(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 880 let groups = split_at_slash(values); 881 let row_start = parse_grid_line_group(&groups[0]); 882 let col_start = if groups.len() > 1 { 883 parse_grid_line_group(&groups[1]) 884 } else { 885 CssValue::Auto 886 }; 887 let row_end = if groups.len() > 2 { 888 parse_grid_line_group(&groups[2]) 889 } else { 890 CssValue::Auto 891 }; 892 let col_end = if groups.len() > 3 { 893 parse_grid_line_group(&groups[3]) 894 } else { 895 CssValue::Auto 896 }; 897 898 vec![ 899 LonghandDeclaration { 900 property: "grid-row-start".to_string(), 901 value: row_start, 902 important, 903 }, 904 LonghandDeclaration { 905 property: "grid-column-start".to_string(), 906 value: col_start, 907 important, 908 }, 909 LonghandDeclaration { 910 property: "grid-row-end".to_string(), 911 value: row_end, 912 important, 913 }, 914 LonghandDeclaration { 915 property: "grid-column-end".to_string(), 916 value: col_end, 917 important, 918 }, 919 ] 920} 921 922/// Split component values at `/` delimiters into groups. 923fn split_at_slash(values: &[ComponentValue]) -> Vec<Vec<&ComponentValue>> { 924 let mut groups: Vec<Vec<&ComponentValue>> = vec![Vec::new()]; 925 for cv in values { 926 if matches!(cv, ComponentValue::Delim('/')) { 927 groups.push(Vec::new()); 928 } else { 929 groups.last_mut().unwrap().push(cv); 930 } 931 } 932 groups 933} 934 935/// Parse a single grid line group (between slashes) into a CssValue. 936fn parse_grid_line_group(values: &[&ComponentValue]) -> CssValue { 937 let non_ws: Vec<&&ComponentValue> = values 938 .iter() 939 .filter(|v| !matches!(v, ComponentValue::Whitespace)) 940 .collect(); 941 942 if non_ws.is_empty() { 943 return CssValue::Auto; 944 } 945 if non_ws.len() == 1 { 946 return parse_single_value(non_ws[0]); 947 } 948 let parsed: Vec<CssValue> = non_ws.iter().map(|v| parse_single_value(v)).collect(); 949 CssValue::List(parsed) 950} 951 952/// Expand a box-model shorthand (margin, padding) using the 1-to-4 value pattern. 953fn expand_box_shorthand( 954 prefix: &str, 955 values: &[ComponentValue], 956 important: bool, 957) -> Vec<LonghandDeclaration> { 958 let parsed: Vec<CssValue> = values 959 .iter() 960 .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) 961 .map(parse_single_value) 962 .collect(); 963 964 let (top, right, bottom, left) = match parsed.len() { 965 1 => ( 966 parsed[0].clone(), 967 parsed[0].clone(), 968 parsed[0].clone(), 969 parsed[0].clone(), 970 ), 971 2 => ( 972 parsed[0].clone(), 973 parsed[1].clone(), 974 parsed[0].clone(), 975 parsed[1].clone(), 976 ), 977 3 => ( 978 parsed[0].clone(), 979 parsed[1].clone(), 980 parsed[2].clone(), 981 parsed[1].clone(), 982 ), 983 4 => ( 984 parsed[0].clone(), 985 parsed[1].clone(), 986 parsed[2].clone(), 987 parsed[3].clone(), 988 ), 989 _ => { 990 let fallback = if parsed.is_empty() { 991 CssValue::Zero 992 } else { 993 parsed[0].clone() 994 }; 995 ( 996 fallback.clone(), 997 fallback.clone(), 998 fallback.clone(), 999 fallback, 1000 ) 1001 } 1002 }; 1003 1004 vec![ 1005 LonghandDeclaration { 1006 property: format!("{prefix}-top"), 1007 value: top, 1008 important, 1009 }, 1010 LonghandDeclaration { 1011 property: format!("{prefix}-right"), 1012 value: right, 1013 important, 1014 }, 1015 LonghandDeclaration { 1016 property: format!("{prefix}-bottom"), 1017 value: bottom, 1018 important, 1019 }, 1020 LonghandDeclaration { 1021 property: format!("{prefix}-left"), 1022 value: left, 1023 important, 1024 }, 1025 ] 1026} 1027 1028/// Expand `border` shorthand into border-width, border-style, border-color. 1029fn expand_border(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 1030 let parsed: Vec<CssValue> = values 1031 .iter() 1032 .filter(|v| !matches!(v, ComponentValue::Whitespace)) 1033 .map(parse_single_value) 1034 .collect(); 1035 1036 let mut width = CssValue::Keyword("medium".to_string()); 1037 let mut style = CssValue::None; 1038 let mut color = CssValue::CurrentColor; 1039 1040 for val in &parsed { 1041 match val { 1042 CssValue::Length(_, _) | CssValue::Zero => width = val.clone(), 1043 CssValue::Color(_) | CssValue::CurrentColor | CssValue::Transparent => { 1044 color = val.clone() 1045 } 1046 CssValue::Keyword(kw) => match kw.as_str() { 1047 "thin" | "medium" | "thick" => width = val.clone(), 1048 "none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" 1049 | "ridge" | "inset" | "outset" => style = val.clone(), 1050 _ => { 1051 // Could be a named color 1052 if let Some(c) = named_color(kw) { 1053 color = CssValue::Color(c); 1054 } 1055 } 1056 }, 1057 _ => {} 1058 } 1059 } 1060 1061 vec![ 1062 LonghandDeclaration { 1063 property: "border-width".to_string(), 1064 value: width, 1065 important, 1066 }, 1067 LonghandDeclaration { 1068 property: "border-style".to_string(), 1069 value: style, 1070 important, 1071 }, 1072 LonghandDeclaration { 1073 property: "border-color".to_string(), 1074 value: color, 1075 important, 1076 }, 1077 ] 1078} 1079 1080/// Expand `background` shorthand (basic: color only for now). 1081fn expand_background(values: &[ComponentValue], important: bool) -> Vec<LonghandDeclaration> { 1082 let parsed: Vec<CssValue> = values 1083 .iter() 1084 .filter(|v| !matches!(v, ComponentValue::Whitespace)) 1085 .map(parse_single_value) 1086 .collect(); 1087 1088 let mut bg_color = CssValue::Transparent; 1089 1090 for val in &parsed { 1091 match val { 1092 CssValue::Color(_) | CssValue::CurrentColor | CssValue::Transparent => { 1093 bg_color = val.clone() 1094 } 1095 CssValue::Keyword(kw) => { 1096 if let Some(c) = named_color(kw) { 1097 bg_color = CssValue::Color(c); 1098 } else { 1099 match kw.as_str() { 1100 "none" => {} // background-image: none 1101 _ => bg_color = val.clone(), 1102 } 1103 } 1104 } 1105 _ => {} 1106 } 1107 } 1108 1109 vec![LonghandDeclaration { 1110 property: "background-color".to_string(), 1111 value: bg_color, 1112 important, 1113 }] 1114} 1115 1116// --------------------------------------------------------------------------- 1117// Tests 1118// --------------------------------------------------------------------------- 1119 1120#[cfg(test)] 1121mod tests { 1122 use super::*; 1123 use crate::tokenizer::{HashType, NumericType}; 1124 1125 // -- Length tests -------------------------------------------------------- 1126 1127 #[test] 1128 fn test_parse_px() { 1129 let cv = ComponentValue::Dimension(16.0, NumericType::Integer, "px".to_string()); 1130 assert_eq!( 1131 parse_single_value(&cv), 1132 CssValue::Length(16.0, LengthUnit::Px) 1133 ); 1134 } 1135 1136 #[test] 1137 fn test_parse_em() { 1138 let cv = ComponentValue::Dimension(1.5, NumericType::Number, "em".to_string()); 1139 assert_eq!( 1140 parse_single_value(&cv), 1141 CssValue::Length(1.5, LengthUnit::Em) 1142 ); 1143 } 1144 1145 #[test] 1146 fn test_parse_rem() { 1147 let cv = ComponentValue::Dimension(2.0, NumericType::Number, "rem".to_string()); 1148 assert_eq!( 1149 parse_single_value(&cv), 1150 CssValue::Length(2.0, LengthUnit::Rem) 1151 ); 1152 } 1153 1154 #[test] 1155 fn test_parse_pt() { 1156 let cv = ComponentValue::Dimension(12.0, NumericType::Integer, "pt".to_string()); 1157 assert_eq!( 1158 parse_single_value(&cv), 1159 CssValue::Length(12.0, LengthUnit::Pt) 1160 ); 1161 } 1162 1163 #[test] 1164 fn test_parse_cm() { 1165 let cv = ComponentValue::Dimension(2.54, NumericType::Number, "cm".to_string()); 1166 assert_eq!( 1167 parse_single_value(&cv), 1168 CssValue::Length(2.54, LengthUnit::Cm) 1169 ); 1170 } 1171 1172 #[test] 1173 fn test_parse_mm() { 1174 let cv = ComponentValue::Dimension(10.0, NumericType::Integer, "mm".to_string()); 1175 assert_eq!( 1176 parse_single_value(&cv), 1177 CssValue::Length(10.0, LengthUnit::Mm) 1178 ); 1179 } 1180 1181 #[test] 1182 fn test_parse_in() { 1183 let cv = ComponentValue::Dimension(1.0, NumericType::Integer, "in".to_string()); 1184 assert_eq!( 1185 parse_single_value(&cv), 1186 CssValue::Length(1.0, LengthUnit::In) 1187 ); 1188 } 1189 1190 #[test] 1191 fn test_parse_pc() { 1192 let cv = ComponentValue::Dimension(6.0, NumericType::Integer, "pc".to_string()); 1193 assert_eq!( 1194 parse_single_value(&cv), 1195 CssValue::Length(6.0, LengthUnit::Pc) 1196 ); 1197 } 1198 1199 #[test] 1200 fn test_parse_vw() { 1201 let cv = ComponentValue::Dimension(50.0, NumericType::Integer, "vw".to_string()); 1202 assert_eq!( 1203 parse_single_value(&cv), 1204 CssValue::Length(50.0, LengthUnit::Vw) 1205 ); 1206 } 1207 1208 #[test] 1209 fn test_parse_vh() { 1210 let cv = ComponentValue::Dimension(100.0, NumericType::Integer, "vh".to_string()); 1211 assert_eq!( 1212 parse_single_value(&cv), 1213 CssValue::Length(100.0, LengthUnit::Vh) 1214 ); 1215 } 1216 1217 #[test] 1218 fn test_parse_vmin() { 1219 let cv = ComponentValue::Dimension(50.0, NumericType::Integer, "vmin".to_string()); 1220 assert_eq!( 1221 parse_single_value(&cv), 1222 CssValue::Length(50.0, LengthUnit::Vmin) 1223 ); 1224 } 1225 1226 #[test] 1227 fn test_parse_vmax() { 1228 let cv = ComponentValue::Dimension(50.0, NumericType::Integer, "vmax".to_string()); 1229 assert_eq!( 1230 parse_single_value(&cv), 1231 CssValue::Length(50.0, LengthUnit::Vmax) 1232 ); 1233 } 1234 1235 #[test] 1236 fn test_parse_percentage() { 1237 let cv = ComponentValue::Percentage(50.0); 1238 assert_eq!(parse_single_value(&cv), CssValue::Percentage(50.0)); 1239 } 1240 1241 #[test] 1242 fn test_parse_zero() { 1243 let cv = ComponentValue::Number(0.0, NumericType::Integer); 1244 assert_eq!(parse_single_value(&cv), CssValue::Zero); 1245 } 1246 1247 #[test] 1248 fn test_parse_number() { 1249 let cv = ComponentValue::Number(42.0, NumericType::Integer); 1250 assert_eq!(parse_single_value(&cv), CssValue::Number(42.0)); 1251 } 1252 1253 // -- Color tests -------------------------------------------------------- 1254 1255 #[test] 1256 fn test_hex_color_3() { 1257 let cv = ComponentValue::Hash("f00".to_string(), HashType::Id); 1258 assert_eq!( 1259 parse_single_value(&cv), 1260 CssValue::Color(Color::rgb(255, 0, 0)) 1261 ); 1262 } 1263 1264 #[test] 1265 fn test_hex_color_4() { 1266 let cv = ComponentValue::Hash("f00a".to_string(), HashType::Id); 1267 assert_eq!( 1268 parse_single_value(&cv), 1269 CssValue::Color(Color::new(255, 0, 0, 170)) 1270 ); 1271 } 1272 1273 #[test] 1274 fn test_hex_color_6() { 1275 let cv = ComponentValue::Hash("ff8800".to_string(), HashType::Id); 1276 assert_eq!( 1277 parse_single_value(&cv), 1278 CssValue::Color(Color::rgb(255, 136, 0)) 1279 ); 1280 } 1281 1282 #[test] 1283 fn test_hex_color_8() { 1284 let cv = ComponentValue::Hash("ff880080".to_string(), HashType::Id); 1285 assert_eq!( 1286 parse_single_value(&cv), 1287 CssValue::Color(Color::new(255, 136, 0, 128)) 1288 ); 1289 } 1290 1291 #[test] 1292 fn test_named_color_red() { 1293 let cv = ComponentValue::Ident("red".to_string()); 1294 assert_eq!( 1295 parse_single_value(&cv), 1296 CssValue::Color(Color::rgb(255, 0, 0)) 1297 ); 1298 } 1299 1300 #[test] 1301 fn test_named_color_blue() { 1302 let cv = ComponentValue::Ident("blue".to_string()); 1303 assert_eq!( 1304 parse_single_value(&cv), 1305 CssValue::Color(Color::rgb(0, 0, 255)) 1306 ); 1307 } 1308 1309 #[test] 1310 fn test_named_color_black() { 1311 let cv = ComponentValue::Ident("black".to_string()); 1312 assert_eq!( 1313 parse_single_value(&cv), 1314 CssValue::Color(Color::rgb(0, 0, 0)) 1315 ); 1316 } 1317 1318 #[test] 1319 fn test_named_color_white() { 1320 let cv = ComponentValue::Ident("white".to_string()); 1321 assert_eq!( 1322 parse_single_value(&cv), 1323 CssValue::Color(Color::rgb(255, 255, 255)) 1324 ); 1325 } 1326 1327 #[test] 1328 fn test_transparent() { 1329 let cv = ComponentValue::Ident("transparent".to_string()); 1330 assert_eq!(parse_single_value(&cv), CssValue::Transparent); 1331 } 1332 1333 #[test] 1334 fn test_current_color() { 1335 let cv = ComponentValue::Ident("currentColor".to_string()); 1336 assert_eq!(parse_single_value(&cv), CssValue::CurrentColor); 1337 } 1338 1339 #[test] 1340 fn test_rgb_function() { 1341 let args = vec![ 1342 ComponentValue::Number(255.0, NumericType::Integer), 1343 ComponentValue::Comma, 1344 ComponentValue::Whitespace, 1345 ComponentValue::Number(128.0, NumericType::Integer), 1346 ComponentValue::Comma, 1347 ComponentValue::Whitespace, 1348 ComponentValue::Number(0.0, NumericType::Integer), 1349 ]; 1350 let cv = ComponentValue::Function("rgb".to_string(), args); 1351 assert_eq!( 1352 parse_single_value(&cv), 1353 CssValue::Color(Color::rgb(255, 128, 0)) 1354 ); 1355 } 1356 1357 #[test] 1358 fn test_rgba_function() { 1359 let args = vec![ 1360 ComponentValue::Number(255.0, NumericType::Integer), 1361 ComponentValue::Comma, 1362 ComponentValue::Whitespace, 1363 ComponentValue::Number(0.0, NumericType::Integer), 1364 ComponentValue::Comma, 1365 ComponentValue::Whitespace, 1366 ComponentValue::Number(0.0, NumericType::Integer), 1367 ComponentValue::Comma, 1368 ComponentValue::Whitespace, 1369 ComponentValue::Number(0.5, NumericType::Number), 1370 ]; 1371 let cv = ComponentValue::Function("rgba".to_string(), args); 1372 assert_eq!( 1373 parse_single_value(&cv), 1374 CssValue::Color(Color::new(255, 0, 0, 128)) 1375 ); 1376 } 1377 1378 // -- Keyword tests ------------------------------------------------------ 1379 1380 #[test] 1381 fn test_keyword_auto() { 1382 let cv = ComponentValue::Ident("auto".to_string()); 1383 assert_eq!(parse_single_value(&cv), CssValue::Auto); 1384 } 1385 1386 #[test] 1387 fn test_keyword_inherit() { 1388 let cv = ComponentValue::Ident("inherit".to_string()); 1389 assert_eq!(parse_single_value(&cv), CssValue::Inherit); 1390 } 1391 1392 #[test] 1393 fn test_keyword_initial() { 1394 let cv = ComponentValue::Ident("initial".to_string()); 1395 assert_eq!(parse_single_value(&cv), CssValue::Initial); 1396 } 1397 1398 #[test] 1399 fn test_keyword_unset() { 1400 let cv = ComponentValue::Ident("unset".to_string()); 1401 assert_eq!(parse_single_value(&cv), CssValue::Unset); 1402 } 1403 1404 #[test] 1405 fn test_keyword_none() { 1406 let cv = ComponentValue::Ident("none".to_string()); 1407 assert_eq!(parse_single_value(&cv), CssValue::None); 1408 } 1409 1410 #[test] 1411 fn test_keyword_display_block() { 1412 let cv = ComponentValue::Ident("block".to_string()); 1413 assert_eq!( 1414 parse_single_value(&cv), 1415 CssValue::Keyword("block".to_string()) 1416 ); 1417 } 1418 1419 #[test] 1420 fn test_keyword_display_inline() { 1421 let cv = ComponentValue::Ident("inline".to_string()); 1422 assert_eq!( 1423 parse_single_value(&cv), 1424 CssValue::Keyword("inline".to_string()) 1425 ); 1426 } 1427 1428 #[test] 1429 fn test_keyword_display_flex() { 1430 let cv = ComponentValue::Ident("flex".to_string()); 1431 assert_eq!( 1432 parse_single_value(&cv), 1433 CssValue::Keyword("flex".to_string()) 1434 ); 1435 } 1436 1437 #[test] 1438 fn test_keyword_position() { 1439 for kw in &["static", "relative", "absolute", "fixed"] { 1440 let cv = ComponentValue::Ident(kw.to_string()); 1441 assert_eq!(parse_single_value(&cv), CssValue::Keyword(kw.to_string())); 1442 } 1443 } 1444 1445 #[test] 1446 fn test_keyword_text_align() { 1447 for kw in &["left", "center", "right", "justify"] { 1448 let cv = ComponentValue::Ident(kw.to_string()); 1449 assert_eq!(parse_single_value(&cv), CssValue::Keyword(kw.to_string())); 1450 } 1451 } 1452 1453 #[test] 1454 fn test_keyword_font_weight() { 1455 let cv = ComponentValue::Ident("bold".to_string()); 1456 assert_eq!( 1457 parse_single_value(&cv), 1458 CssValue::Keyword("bold".to_string()) 1459 ); 1460 let cv = ComponentValue::Ident("normal".to_string()); 1461 assert_eq!( 1462 parse_single_value(&cv), 1463 CssValue::Keyword("normal".to_string()) 1464 ); 1465 // Numeric font-weight 1466 let cv = ComponentValue::Number(700.0, NumericType::Integer); 1467 assert_eq!(parse_single_value(&cv), CssValue::Number(700.0)); 1468 } 1469 1470 #[test] 1471 fn test_keyword_overflow() { 1472 for kw in &["visible", "hidden", "scroll", "auto"] { 1473 let cv = ComponentValue::Ident(kw.to_string()); 1474 let expected = match *kw { 1475 "auto" => CssValue::Auto, 1476 _ => CssValue::Keyword(kw.to_string()), 1477 }; 1478 assert_eq!(parse_single_value(&cv), expected); 1479 } 1480 } 1481 1482 // -- parse_value (multi-value) ------------------------------------------ 1483 1484 #[test] 1485 fn test_parse_value_single() { 1486 let values = vec![ComponentValue::Ident("red".to_string())]; 1487 assert_eq!(parse_value(&values), CssValue::Color(Color::rgb(255, 0, 0))); 1488 } 1489 1490 #[test] 1491 fn test_parse_value_multi() { 1492 let values = vec![ 1493 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1494 ComponentValue::Whitespace, 1495 ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), 1496 ]; 1497 assert_eq!( 1498 parse_value(&values), 1499 CssValue::List(vec![ 1500 CssValue::Length(10.0, LengthUnit::Px), 1501 CssValue::Length(20.0, LengthUnit::Px), 1502 ]) 1503 ); 1504 } 1505 1506 // -- Shorthand expansion tests ------------------------------------------ 1507 1508 #[test] 1509 fn test_margin_one_value() { 1510 let values = vec![ComponentValue::Dimension( 1511 10.0, 1512 NumericType::Integer, 1513 "px".to_string(), 1514 )]; 1515 let result = expand_shorthand("margin", &values, false).unwrap(); 1516 assert_eq!(result.len(), 4); 1517 for decl in &result { 1518 assert_eq!(decl.value, CssValue::Length(10.0, LengthUnit::Px)); 1519 } 1520 assert_eq!(result[0].property, "margin-top"); 1521 assert_eq!(result[1].property, "margin-right"); 1522 assert_eq!(result[2].property, "margin-bottom"); 1523 assert_eq!(result[3].property, "margin-left"); 1524 } 1525 1526 #[test] 1527 fn test_margin_two_values() { 1528 let values = vec![ 1529 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1530 ComponentValue::Whitespace, 1531 ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), 1532 ]; 1533 let result = expand_shorthand("margin", &values, false).unwrap(); 1534 assert_eq!(result[0].value, CssValue::Length(10.0, LengthUnit::Px)); // top 1535 assert_eq!(result[1].value, CssValue::Length(20.0, LengthUnit::Px)); // right 1536 assert_eq!(result[2].value, CssValue::Length(10.0, LengthUnit::Px)); // bottom 1537 assert_eq!(result[3].value, CssValue::Length(20.0, LengthUnit::Px)); // left 1538 } 1539 1540 #[test] 1541 fn test_margin_three_values() { 1542 let values = vec![ 1543 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1544 ComponentValue::Whitespace, 1545 ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), 1546 ComponentValue::Whitespace, 1547 ComponentValue::Dimension(30.0, NumericType::Integer, "px".to_string()), 1548 ]; 1549 let result = expand_shorthand("margin", &values, false).unwrap(); 1550 assert_eq!(result[0].value, CssValue::Length(10.0, LengthUnit::Px)); // top 1551 assert_eq!(result[1].value, CssValue::Length(20.0, LengthUnit::Px)); // right 1552 assert_eq!(result[2].value, CssValue::Length(30.0, LengthUnit::Px)); // bottom 1553 assert_eq!(result[3].value, CssValue::Length(20.0, LengthUnit::Px)); // left 1554 } 1555 1556 #[test] 1557 fn test_margin_four_values() { 1558 let values = vec![ 1559 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1560 ComponentValue::Whitespace, 1561 ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), 1562 ComponentValue::Whitespace, 1563 ComponentValue::Dimension(30.0, NumericType::Integer, "px".to_string()), 1564 ComponentValue::Whitespace, 1565 ComponentValue::Dimension(40.0, NumericType::Integer, "px".to_string()), 1566 ]; 1567 let result = expand_shorthand("margin", &values, false).unwrap(); 1568 assert_eq!(result[0].value, CssValue::Length(10.0, LengthUnit::Px)); // top 1569 assert_eq!(result[1].value, CssValue::Length(20.0, LengthUnit::Px)); // right 1570 assert_eq!(result[2].value, CssValue::Length(30.0, LengthUnit::Px)); // bottom 1571 assert_eq!(result[3].value, CssValue::Length(40.0, LengthUnit::Px)); // left 1572 } 1573 1574 #[test] 1575 fn test_margin_auto() { 1576 let values = vec![ 1577 ComponentValue::Number(0.0, NumericType::Integer), 1578 ComponentValue::Whitespace, 1579 ComponentValue::Ident("auto".to_string()), 1580 ]; 1581 let result = expand_shorthand("margin", &values, false).unwrap(); 1582 assert_eq!(result[0].value, CssValue::Zero); // top 1583 assert_eq!(result[1].value, CssValue::Auto); // right 1584 assert_eq!(result[2].value, CssValue::Zero); // bottom 1585 assert_eq!(result[3].value, CssValue::Auto); // left 1586 } 1587 1588 #[test] 1589 fn test_padding_shorthand() { 1590 let values = vec![ComponentValue::Dimension( 1591 5.0, 1592 NumericType::Integer, 1593 "px".to_string(), 1594 )]; 1595 let result = expand_shorthand("padding", &values, false).unwrap(); 1596 assert_eq!(result.len(), 4); 1597 assert_eq!(result[0].property, "padding-top"); 1598 assert_eq!(result[1].property, "padding-right"); 1599 assert_eq!(result[2].property, "padding-bottom"); 1600 assert_eq!(result[3].property, "padding-left"); 1601 } 1602 1603 #[test] 1604 fn test_border_shorthand() { 1605 let values = vec![ 1606 ComponentValue::Dimension(1.0, NumericType::Integer, "px".to_string()), 1607 ComponentValue::Whitespace, 1608 ComponentValue::Ident("solid".to_string()), 1609 ComponentValue::Whitespace, 1610 ComponentValue::Ident("red".to_string()), 1611 ]; 1612 let result = expand_shorthand("border", &values, false).unwrap(); 1613 assert_eq!(result.len(), 3); 1614 assert_eq!(result[0].property, "border-width"); 1615 assert_eq!(result[0].value, CssValue::Length(1.0, LengthUnit::Px)); 1616 assert_eq!(result[1].property, "border-style"); 1617 assert_eq!(result[1].value, CssValue::Keyword("solid".to_string())); 1618 assert_eq!(result[2].property, "border-color"); 1619 assert_eq!(result[2].value, CssValue::Color(Color::rgb(255, 0, 0))); 1620 } 1621 1622 #[test] 1623 fn test_border_shorthand_defaults() { 1624 // Just a width 1625 let values = vec![ComponentValue::Dimension( 1626 2.0, 1627 NumericType::Integer, 1628 "px".to_string(), 1629 )]; 1630 let result = expand_shorthand("border", &values, false).unwrap(); 1631 assert_eq!(result[0].value, CssValue::Length(2.0, LengthUnit::Px)); 1632 assert_eq!(result[1].value, CssValue::None); // default style 1633 assert_eq!(result[2].value, CssValue::CurrentColor); // default color 1634 } 1635 1636 #[test] 1637 fn test_background_shorthand_color() { 1638 let values = vec![ComponentValue::Hash("ff0000".to_string(), HashType::Id)]; 1639 let result = expand_shorthand("background", &values, false).unwrap(); 1640 assert_eq!(result.len(), 1); 1641 assert_eq!(result[0].property, "background-color"); 1642 assert_eq!(result[0].value, CssValue::Color(Color::rgb(255, 0, 0))); 1643 } 1644 1645 #[test] 1646 fn test_non_shorthand_returns_none() { 1647 let values = vec![ComponentValue::Ident("red".to_string())]; 1648 assert!(expand_shorthand("color", &values, false).is_none()); 1649 } 1650 1651 #[test] 1652 fn test_important_propagated() { 1653 let values = vec![ComponentValue::Dimension( 1654 10.0, 1655 NumericType::Integer, 1656 "px".to_string(), 1657 )]; 1658 let result = expand_shorthand("margin", &values, true).unwrap(); 1659 for decl in &result { 1660 assert!(decl.important); 1661 } 1662 } 1663 1664 // -- Integration: parse from CSS text ----------------------------------- 1665 1666 #[test] 1667 fn test_parse_from_css_text() { 1668 use crate::parser::Parser; 1669 1670 let ss = Parser::parse("p { color: red; margin: 10px 20px; }"); 1671 let rule = match &ss.rules[0] { 1672 crate::parser::Rule::Style(r) => r, 1673 _ => panic!("expected style rule"), 1674 }; 1675 1676 // color: red 1677 let color_val = parse_value(&rule.declarations[0].value); 1678 assert_eq!(color_val, CssValue::Color(Color::rgb(255, 0, 0))); 1679 1680 // margin: 10px 20px (multi-value) 1681 let margin_val = parse_value(&rule.declarations[1].value); 1682 assert_eq!( 1683 margin_val, 1684 CssValue::List(vec![ 1685 CssValue::Length(10.0, LengthUnit::Px), 1686 CssValue::Length(20.0, LengthUnit::Px), 1687 ]) 1688 ); 1689 } 1690 1691 #[test] 1692 fn test_shorthand_from_css_text() { 1693 use crate::parser::Parser; 1694 1695 let ss = Parser::parse("div { margin: 10px 20px 30px 40px; }"); 1696 let rule = match &ss.rules[0] { 1697 crate::parser::Rule::Style(r) => r, 1698 _ => panic!("expected style rule"), 1699 }; 1700 1701 let longhands = expand_shorthand( 1702 &rule.declarations[0].property, 1703 &rule.declarations[0].value, 1704 rule.declarations[0].important, 1705 ) 1706 .unwrap(); 1707 1708 assert_eq!(longhands[0].property, "margin-top"); 1709 assert_eq!(longhands[0].value, CssValue::Length(10.0, LengthUnit::Px)); 1710 assert_eq!(longhands[1].property, "margin-right"); 1711 assert_eq!(longhands[1].value, CssValue::Length(20.0, LengthUnit::Px)); 1712 assert_eq!(longhands[2].property, "margin-bottom"); 1713 assert_eq!(longhands[2].value, CssValue::Length(30.0, LengthUnit::Px)); 1714 assert_eq!(longhands[3].property, "margin-left"); 1715 assert_eq!(longhands[3].value, CssValue::Length(40.0, LengthUnit::Px)); 1716 } 1717 1718 #[test] 1719 fn test_case_insensitive_units() { 1720 let cv = ComponentValue::Dimension(16.0, NumericType::Integer, "PX".to_string()); 1721 assert_eq!( 1722 parse_single_value(&cv), 1723 CssValue::Length(16.0, LengthUnit::Px) 1724 ); 1725 } 1726 1727 #[test] 1728 fn test_case_insensitive_color_name() { 1729 let cv = ComponentValue::Ident("RED".to_string()); 1730 assert_eq!( 1731 parse_single_value(&cv), 1732 CssValue::Color(Color::rgb(255, 0, 0)) 1733 ); 1734 } 1735 1736 #[test] 1737 fn test_case_insensitive_keywords() { 1738 let cv = ComponentValue::Ident("AUTO".to_string()); 1739 assert_eq!(parse_single_value(&cv), CssValue::Auto); 1740 1741 let cv = ComponentValue::Ident("INHERIT".to_string()); 1742 assert_eq!(parse_single_value(&cv), CssValue::Inherit); 1743 } 1744 1745 #[test] 1746 fn test_named_color_grey_alias() { 1747 let cv = ComponentValue::Ident("grey".to_string()); 1748 assert_eq!( 1749 parse_single_value(&cv), 1750 CssValue::Color(Color::rgb(128, 128, 128)) 1751 ); 1752 } 1753 1754 #[test] 1755 fn test_named_color_all_16_plus() { 1756 let colors = vec![ 1757 ("black", 0, 0, 0), 1758 ("silver", 192, 192, 192), 1759 ("gray", 128, 128, 128), 1760 ("white", 255, 255, 255), 1761 ("maroon", 128, 0, 0), 1762 ("red", 255, 0, 0), 1763 ("purple", 128, 0, 128), 1764 ("fuchsia", 255, 0, 255), 1765 ("green", 0, 128, 0), 1766 ("lime", 0, 255, 0), 1767 ("olive", 128, 128, 0), 1768 ("yellow", 255, 255, 0), 1769 ("navy", 0, 0, 128), 1770 ("blue", 0, 0, 255), 1771 ("teal", 0, 128, 128), 1772 ("aqua", 0, 255, 255), 1773 ("orange", 255, 165, 0), 1774 ]; 1775 for (name, r, g, b) in colors { 1776 let cv = ComponentValue::Ident(name.to_string()); 1777 assert_eq!( 1778 parse_single_value(&cv), 1779 CssValue::Color(Color::rgb(r, g, b)), 1780 "failed for {name}" 1781 ); 1782 } 1783 } 1784 1785 #[test] 1786 fn test_border_width_shorthand() { 1787 let values = vec![ 1788 ComponentValue::Dimension(1.0, NumericType::Integer, "px".to_string()), 1789 ComponentValue::Whitespace, 1790 ComponentValue::Dimension(2.0, NumericType::Integer, "px".to_string()), 1791 ]; 1792 let result = expand_shorthand("border-width", &values, false).unwrap(); 1793 assert_eq!(result.len(), 4); 1794 assert_eq!(result[0].property, "border-top-width"); 1795 assert_eq!(result[1].property, "border-right-width"); 1796 } 1797 1798 #[test] 1799 fn test_border_style_shorthand() { 1800 let values = vec![ComponentValue::Ident("solid".to_string())]; 1801 let result = expand_shorthand("border-style", &values, false).unwrap(); 1802 assert_eq!(result.len(), 4); 1803 assert_eq!(result[0].property, "border-top-style"); 1804 } 1805 1806 #[test] 1807 fn test_border_color_shorthand() { 1808 let values = vec![ComponentValue::Ident("red".to_string())]; 1809 let result = expand_shorthand("border-color", &values, false).unwrap(); 1810 assert_eq!(result.len(), 4); 1811 assert_eq!(result[0].property, "border-top-color"); 1812 } 1813 1814 #[test] 1815 fn test_string_value() { 1816 let cv = ComponentValue::String("hello".to_string()); 1817 assert_eq!( 1818 parse_single_value(&cv), 1819 CssValue::String("hello".to_string()) 1820 ); 1821 } 1822 1823 // -- Math function parsing tests ------------------------------------------ 1824 1825 #[test] 1826 fn test_calc_simple_px() { 1827 let args = vec![ 1828 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1829 ComponentValue::Whitespace, 1830 ComponentValue::Delim('+'), 1831 ComponentValue::Whitespace, 1832 ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), 1833 ]; 1834 let cv = ComponentValue::Function("calc".to_string(), args); 1835 let result = parse_single_value(&cv); 1836 assert_eq!( 1837 result, 1838 CssValue::Math(Box::new(MathExpr::Add( 1839 Box::new(MathExpr::Length(10.0, LengthUnit::Px)), 1840 Box::new(MathExpr::Length(20.0, LengthUnit::Px)), 1841 ))) 1842 ); 1843 } 1844 1845 #[test] 1846 fn test_calc_percentage_minus_px() { 1847 let args = vec![ 1848 ComponentValue::Percentage(100.0), 1849 ComponentValue::Whitespace, 1850 ComponentValue::Delim('-'), 1851 ComponentValue::Whitespace, 1852 ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), 1853 ]; 1854 let cv = ComponentValue::Function("calc".to_string(), args); 1855 let result = parse_single_value(&cv); 1856 assert_eq!( 1857 result, 1858 CssValue::Math(Box::new(MathExpr::Sub( 1859 Box::new(MathExpr::Percentage(100.0)), 1860 Box::new(MathExpr::Length(20.0, LengthUnit::Px)), 1861 ))) 1862 ); 1863 } 1864 1865 #[test] 1866 fn test_calc_multiplication() { 1867 // calc(2 * 10px) 1868 let args = vec![ 1869 ComponentValue::Number(2.0, NumericType::Integer), 1870 ComponentValue::Whitespace, 1871 ComponentValue::Delim('*'), 1872 ComponentValue::Whitespace, 1873 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1874 ]; 1875 let cv = ComponentValue::Function("calc".to_string(), args); 1876 let result = parse_single_value(&cv); 1877 assert_eq!( 1878 result, 1879 CssValue::Math(Box::new(MathExpr::Mul( 1880 Box::new(MathExpr::Number(2.0)), 1881 Box::new(MathExpr::Length(10.0, LengthUnit::Px)), 1882 ))) 1883 ); 1884 } 1885 1886 #[test] 1887 fn test_calc_division() { 1888 // calc(100px / 2) 1889 let args = vec![ 1890 ComponentValue::Dimension(100.0, NumericType::Integer, "px".to_string()), 1891 ComponentValue::Whitespace, 1892 ComponentValue::Delim('/'), 1893 ComponentValue::Whitespace, 1894 ComponentValue::Number(2.0, NumericType::Integer), 1895 ]; 1896 let cv = ComponentValue::Function("calc".to_string(), args); 1897 let result = parse_single_value(&cv); 1898 assert_eq!( 1899 result, 1900 CssValue::Math(Box::new(MathExpr::Div( 1901 Box::new(MathExpr::Length(100.0, LengthUnit::Px)), 1902 Box::new(MathExpr::Number(2.0)), 1903 ))) 1904 ); 1905 } 1906 1907 #[test] 1908 fn test_calc_operator_precedence() { 1909 // calc(10px + 2 * 5px) should be Add(10px, Mul(2, 5px)) 1910 let args = vec![ 1911 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1912 ComponentValue::Whitespace, 1913 ComponentValue::Delim('+'), 1914 ComponentValue::Whitespace, 1915 ComponentValue::Number(2.0, NumericType::Integer), 1916 ComponentValue::Whitespace, 1917 ComponentValue::Delim('*'), 1918 ComponentValue::Whitespace, 1919 ComponentValue::Dimension(5.0, NumericType::Integer, "px".to_string()), 1920 ]; 1921 let cv = ComponentValue::Function("calc".to_string(), args); 1922 let result = parse_single_value(&cv); 1923 assert_eq!( 1924 result, 1925 CssValue::Math(Box::new(MathExpr::Add( 1926 Box::new(MathExpr::Length(10.0, LengthUnit::Px)), 1927 Box::new(MathExpr::Mul( 1928 Box::new(MathExpr::Number(2.0)), 1929 Box::new(MathExpr::Length(5.0, LengthUnit::Px)), 1930 )), 1931 ))) 1932 ); 1933 } 1934 1935 #[test] 1936 fn test_min_two_args() { 1937 let args = vec![ 1938 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1939 ComponentValue::Comma, 1940 ComponentValue::Whitespace, 1941 ComponentValue::Dimension(5.0, NumericType::Integer, "vw".to_string()), 1942 ]; 1943 let cv = ComponentValue::Function("min".to_string(), args); 1944 let result = parse_single_value(&cv); 1945 assert_eq!( 1946 result, 1947 CssValue::Math(Box::new(MathExpr::Min(vec![ 1948 MathExpr::Length(10.0, LengthUnit::Px), 1949 MathExpr::Length(5.0, LengthUnit::Vw), 1950 ]))) 1951 ); 1952 } 1953 1954 #[test] 1955 fn test_max_two_args() { 1956 let args = vec![ 1957 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1958 ComponentValue::Comma, 1959 ComponentValue::Whitespace, 1960 ComponentValue::Dimension(5.0, NumericType::Integer, "vw".to_string()), 1961 ]; 1962 let cv = ComponentValue::Function("max".to_string(), args); 1963 let result = parse_single_value(&cv); 1964 assert_eq!( 1965 result, 1966 CssValue::Math(Box::new(MathExpr::Max(vec![ 1967 MathExpr::Length(10.0, LengthUnit::Px), 1968 MathExpr::Length(5.0, LengthUnit::Vw), 1969 ]))) 1970 ); 1971 } 1972 1973 #[test] 1974 fn test_clamp_three_args() { 1975 let args = vec![ 1976 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 1977 ComponentValue::Comma, 1978 ComponentValue::Whitespace, 1979 ComponentValue::Percentage(50.0), 1980 ComponentValue::Comma, 1981 ComponentValue::Whitespace, 1982 ComponentValue::Dimension(200.0, NumericType::Integer, "px".to_string()), 1983 ]; 1984 let cv = ComponentValue::Function("clamp".to_string(), args); 1985 let result = parse_single_value(&cv); 1986 assert_eq!( 1987 result, 1988 CssValue::Math(Box::new(MathExpr::Clamp( 1989 Box::new(MathExpr::Length(10.0, LengthUnit::Px)), 1990 Box::new(MathExpr::Percentage(50.0)), 1991 Box::new(MathExpr::Length(200.0, LengthUnit::Px)), 1992 ))) 1993 ); 1994 } 1995 1996 #[test] 1997 fn test_calc_nested_min() { 1998 // calc(min(10px, 5vw) + 20px) 1999 let args = vec![ 2000 ComponentValue::Function( 2001 "min".to_string(), 2002 vec![ 2003 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 2004 ComponentValue::Comma, 2005 ComponentValue::Whitespace, 2006 ComponentValue::Dimension(5.0, NumericType::Integer, "vw".to_string()), 2007 ], 2008 ), 2009 ComponentValue::Whitespace, 2010 ComponentValue::Delim('+'), 2011 ComponentValue::Whitespace, 2012 ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), 2013 ]; 2014 let cv = ComponentValue::Function("calc".to_string(), args); 2015 let result = parse_single_value(&cv); 2016 assert_eq!( 2017 result, 2018 CssValue::Math(Box::new(MathExpr::Add( 2019 Box::new(MathExpr::Min(vec![ 2020 MathExpr::Length(10.0, LengthUnit::Px), 2021 MathExpr::Length(5.0, LengthUnit::Vw), 2022 ])), 2023 Box::new(MathExpr::Length(20.0, LengthUnit::Px)), 2024 ))) 2025 ); 2026 } 2027 2028 #[test] 2029 fn test_calc_mixed_units() { 2030 // calc(50% - 2em) 2031 let args = vec![ 2032 ComponentValue::Percentage(50.0), 2033 ComponentValue::Whitespace, 2034 ComponentValue::Delim('-'), 2035 ComponentValue::Whitespace, 2036 ComponentValue::Dimension(2.0, NumericType::Number, "em".to_string()), 2037 ]; 2038 let cv = ComponentValue::Function("calc".to_string(), args); 2039 let result = parse_single_value(&cv); 2040 assert_eq!( 2041 result, 2042 CssValue::Math(Box::new(MathExpr::Sub( 2043 Box::new(MathExpr::Percentage(50.0)), 2044 Box::new(MathExpr::Length(2.0, LengthUnit::Em)), 2045 ))) 2046 ); 2047 } 2048 2049 #[test] 2050 fn test_invalid_calc_empty() { 2051 let args = vec![]; 2052 let cv = ComponentValue::Function("calc".to_string(), args); 2053 assert_eq!( 2054 parse_single_value(&cv), 2055 CssValue::Keyword("calc()".to_string()) 2056 ); 2057 } 2058 2059 #[test] 2060 fn test_invalid_clamp_too_few_args() { 2061 let args = vec![ 2062 ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), 2063 ComponentValue::Comma, 2064 ComponentValue::Dimension(50.0, NumericType::Integer, "px".to_string()), 2065 ]; 2066 let cv = ComponentValue::Function("clamp".to_string(), args); 2067 assert_eq!( 2068 parse_single_value(&cv), 2069 CssValue::Keyword("clamp()".to_string()) 2070 ); 2071 } 2072 2073 #[test] 2074 fn test_calc_from_css_text() { 2075 use crate::parser::Parser; 2076 2077 let ss = Parser::parse("div { width: calc(100% - 20px); }"); 2078 let rule = match &ss.rules[0] { 2079 crate::parser::Rule::Style(r) => r, 2080 _ => panic!("expected style rule"), 2081 }; 2082 let val = parse_value(&rule.declarations[0].value); 2083 assert!(matches!(val, CssValue::Math(_))); 2084 } 2085 2086 #[test] 2087 fn test_min_from_css_text() { 2088 use crate::parser::Parser; 2089 2090 let ss = Parser::parse("div { width: min(10px, 5vw); }"); 2091 let rule = match &ss.rules[0] { 2092 crate::parser::Rule::Style(r) => r, 2093 _ => panic!("expected style rule"), 2094 }; 2095 let val = parse_value(&rule.declarations[0].value); 2096 assert!(matches!(val, CssValue::Math(_))); 2097 } 2098 2099 #[test] 2100 fn test_clamp_from_css_text() { 2101 use crate::parser::Parser; 2102 2103 let ss = Parser::parse("div { width: clamp(10px, 50%, 200px); }"); 2104 let rule = match &ss.rules[0] { 2105 crate::parser::Rule::Style(r) => r, 2106 _ => panic!("expected style rule"), 2107 }; 2108 let val = parse_value(&rule.declarations[0].value); 2109 assert!(matches!(val, CssValue::Math(_))); 2110 } 2111 2112 #[test] 2113 fn expand_grid_column_shorthand() { 2114 let longhands = expand_shorthand("grid-column", &parse_cv("1 / 3"), false).unwrap(); 2115 assert_eq!(longhands.len(), 2); 2116 assert_eq!(longhands[0].property, "grid-column-start"); 2117 assert!(matches!(longhands[0].value, CssValue::Number(n) if n == 1.0)); 2118 assert_eq!(longhands[1].property, "grid-column-end"); 2119 assert!(matches!(longhands[1].value, CssValue::Number(n) if n == 3.0)); 2120 } 2121 2122 #[test] 2123 fn expand_grid_column_single_value() { 2124 let longhands = expand_shorthand("grid-column", &parse_cv("2"), false).unwrap(); 2125 assert_eq!(longhands[0].property, "grid-column-start"); 2126 assert!(matches!(longhands[0].value, CssValue::Number(n) if n == 2.0)); 2127 assert_eq!(longhands[1].property, "grid-column-end"); 2128 assert_eq!(longhands[1].value, CssValue::Auto); 2129 } 2130 2131 #[test] 2132 fn expand_grid_row_shorthand() { 2133 let longhands = expand_shorthand("grid-row", &parse_cv("1 / 4"), false).unwrap(); 2134 assert_eq!(longhands.len(), 2); 2135 assert_eq!(longhands[0].property, "grid-row-start"); 2136 assert_eq!(longhands[1].property, "grid-row-end"); 2137 } 2138 2139 #[test] 2140 fn expand_grid_area_shorthand() { 2141 let longhands = expand_shorthand("grid-area", &parse_cv("1 / 2 / 3 / 4"), false).unwrap(); 2142 assert_eq!(longhands.len(), 4); 2143 assert_eq!(longhands[0].property, "grid-row-start"); 2144 assert_eq!(longhands[1].property, "grid-column-start"); 2145 assert_eq!(longhands[2].property, "grid-row-end"); 2146 assert_eq!(longhands[3].property, "grid-column-end"); 2147 } 2148 2149 /// Helper: parse a CSS value string into component values using the parser. 2150 fn parse_cv(input: &str) -> Vec<ComponentValue> { 2151 use crate::parser::Parser; 2152 let css = format!("x {{ p: {} }}", input); 2153 let ss = Parser::parse(&css); 2154 let rule = match &ss.rules[0] { 2155 crate::parser::Rule::Style(r) => r, 2156 _ => panic!("expected style rule"), 2157 }; 2158 rule.declarations[0].value.clone() 2159 } 2160 2161 // -- Canvas color string parsing tests ---------------------------------- 2162 2163 #[test] 2164 fn canvas_color_hex_rgb() { 2165 assert_eq!(parse_color_string("#ff0000"), Some(Color::rgb(255, 0, 0))); 2166 } 2167 2168 #[test] 2169 fn canvas_color_hex_short() { 2170 assert_eq!(parse_color_string("#f00"), Some(Color::rgb(255, 0, 0))); 2171 } 2172 2173 #[test] 2174 fn canvas_color_hex_rgba() { 2175 assert_eq!( 2176 parse_color_string("#ff000080"), 2177 Some(Color::new(255, 0, 0, 128)) 2178 ); 2179 } 2180 2181 #[test] 2182 fn canvas_color_hex_short_rgba() { 2183 assert_eq!( 2184 parse_color_string("#f008"), 2185 Some(Color::new(255, 0, 0, 136)) 2186 ); 2187 } 2188 2189 #[test] 2190 fn canvas_color_named_red() { 2191 assert_eq!(parse_color_string("red"), Some(Color::rgb(255, 0, 0))); 2192 } 2193 2194 #[test] 2195 fn canvas_color_named_blue() { 2196 assert_eq!(parse_color_string("blue"), Some(Color::rgb(0, 0, 255))); 2197 } 2198 2199 #[test] 2200 fn canvas_color_transparent() { 2201 assert_eq!( 2202 parse_color_string("transparent"), 2203 Some(Color::new(0, 0, 0, 0)) 2204 ); 2205 } 2206 2207 #[test] 2208 fn canvas_color_rgb_func() { 2209 assert_eq!( 2210 parse_color_string("rgb(128, 64, 32)"), 2211 Some(Color::rgb(128, 64, 32)) 2212 ); 2213 } 2214 2215 #[test] 2216 fn canvas_color_rgba_func() { 2217 let c = parse_color_string("rgba(255, 0, 0, 0.5)").unwrap(); 2218 assert_eq!(c.r, 255); 2219 assert_eq!(c.g, 0); 2220 assert_eq!(c.b, 0); 2221 assert_eq!(c.a, 128); 2222 } 2223 2224 #[test] 2225 fn canvas_color_invalid_returns_none() { 2226 assert_eq!(parse_color_string("notacolor"), None); 2227 assert_eq!(parse_color_string(""), None); 2228 assert_eq!(parse_color_string("#xyz"), None); 2229 } 2230 2231 #[test] 2232 fn canvas_color_case_insensitive() { 2233 assert_eq!(parse_color_string("RED"), Some(Color::rgb(255, 0, 0))); 2234 assert_eq!( 2235 parse_color_string("Transparent"), 2236 Some(Color::new(0, 0, 0, 0)) 2237 ); 2238 } 2239 2240 #[test] 2241 fn canvas_color_whitespace_trimmed() { 2242 assert_eq!(parse_color_string(" red "), Some(Color::rgb(255, 0, 0))); 2243 } 2244}