we (web engine): Experimental web browser project to understand the limits of Claude
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}