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