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