1pub mod ast;
27pub mod error;
28pub mod parse;
29pub mod resolve;
30pub mod value;
31
32pub use ast::{
33 Combinator, Declaration, Node, PseudoClass, Rule, Selector, SimpleSelector, Stylesheet,
34};
35pub use error::ParseError;
36pub use parse::parse;
37pub use resolve::ResolvedStyle;
38pub use value::{Color, Length, SideValue, Value, expand_side_set, expand_sides};
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43
44 fn s(css: &str) -> Stylesheet {
45 parse(css).unwrap()
46 }
47
48 fn n<'a>(element: &'a str, classes: &'a [&'a str]) -> Node<'a> {
49 Node { element, classes }
50 }
51
52 #[test]
53 fn parses_type_selector_and_one_color_prop() {
54 let sheet = s("label { color: #fff; }");
55 assert_eq!(sheet.rules.len(), 1);
56 assert_eq!(
57 sheet.rules[0].selectors[0].parts[0].element.as_deref(),
58 Some("label")
59 );
60 let resolved = sheet.resolve(&n("label", &[]), &[], &[], None);
61 assert_eq!(
62 resolved.get("color"),
63 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
64 );
65 }
66
67 #[test]
68 fn class_selector_filters() {
69 let sheet = s(".prompt { color: #f00; }");
70 let hit = sheet.resolve(&n("label", &["prompt"]), &[], &[], None);
71 let miss = sheet.resolve(&n("label", &[]), &[], &[], None);
72 assert!(hit.get("color").is_some());
73 assert!(miss.is_empty());
74 }
75
76 #[test]
77 fn pseudo_class_only_applies_in_state() {
78 let sheet = s(".row { color: #aaa; } .row:hover { color: #fff; }");
79 let base = sheet.resolve(&n("row", &["row"]), &[], &[], None);
80 let hover = sheet.resolve(&n("row", &["row"]), &[], &[], Some(PseudoClass::Hover));
81 assert_eq!(
82 base.get("color"),
83 Some(&Value::Color(Color::rgb(0xaa, 0xaa, 0xaa)))
84 );
85 assert_eq!(
86 hover.get("color"),
87 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
88 );
89 }
90
91 #[test]
92 fn padding_shorthand_one_value() {
93 let sheet = s("button { padding: 10px; }");
94 let resolved = sheet.resolve(&n("button", &[]), &[], &[], None);
95 let Value::LengthSet(set) = resolved.get("padding").unwrap() else {
96 panic!("expected LengthSet");
97 };
98 assert_eq!(set, &vec![Length::Px(10.0)]);
99 let expanded = expand_sides(set).unwrap();
100 assert_eq!(expanded, [Length::Px(10.0); 4]);
101 }
102
103 #[test]
104 fn padding_shorthand_two_values_top_right() {
105 let sheet = s("button { padding: 10px 20px; }");
106 let r = sheet.resolve(&n("button", &[]), &[], &[], None);
107 let Value::LengthSet(set) = r.get("padding").unwrap() else {
108 unreachable!()
109 };
110 let exp = expand_sides(set).unwrap();
111 assert_eq!(
112 exp,
113 [
114 Length::Px(10.0),
115 Length::Px(20.0),
116 Length::Px(10.0),
117 Length::Px(20.0)
118 ]
119 );
120 }
121
122 #[test]
123 fn cascade_specificity_class_beats_type() {
124 let sheet = s("label { color: #aaa; } .head { color: #fff; }");
125 let r = sheet.resolve(&n("label", &["head"]), &[], &[], None);
126 assert_eq!(
127 r.get("color"),
128 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
129 );
130 }
131
132 #[test]
133 fn cascade_source_order_breaks_ties() {
134 let sheet = s(".a { color: #001; } .a { color: #002; }");
135 let r = sheet.resolve(&n("x", &["a"]), &[], &[], None);
136 assert_eq!(r.get("color"), Some(&Value::Color(Color::rgb(0, 0, 0x22))));
137 }
138
139 #[test]
140 fn rgb_and_rgba_functions() {
141 let sheet = s("x { color: rgb(255, 128, 0); background-color: rgba(0, 0, 0, 0.5); }");
142 let r = sheet.resolve(&n("x", &[]), &[], &[], None);
143 assert_eq!(
144 r.get("color"),
145 Some(&Value::Color(Color::rgb(0xff, 0x80, 0)))
146 );
147 assert_eq!(
148 r.get("background-color"),
149 Some(&Value::Color(Color::rgba(0, 0, 0, 128)))
150 );
151 }
152
153 #[test]
154 fn selector_list_applies_to_all() {
155 let sheet = s(".a, .b { color: #fff; }");
156 assert!(
157 sheet
158 .resolve(&n("x", &["a"]), &[], &[], None)
159 .get("color")
160 .is_some()
161 );
162 assert!(
163 sheet
164 .resolve(&n("x", &["b"]), &[], &[], None)
165 .get("color")
166 .is_some()
167 );
168 assert!(sheet.resolve(&n("x", &["c"]), &[], &[], None).is_empty());
169 }
170
171 #[test]
172 fn unitless_number_parses_as_px() {
173 let sheet = s("x { width: 100; }");
174 let r = sheet.resolve(&n("x", &[]), &[], &[], None);
175 assert_eq!(r.get("width"), Some(&Value::Length(Length::Px(100.0))));
176 }
177
178 #[test]
179 fn percent_length() {
180 let sheet = s("x { width: 50%; }");
181 let r = sheet.resolve(&n("x", &[]), &[], &[], None);
182 assert_eq!(r.get("width"), Some(&Value::Length(Length::Percent(50.0))));
183 }
184
185 #[test]
186 fn keyword_value() {
187 let sheet = s("x { display: flex; }");
188 let r = sheet.resolve(&n("x", &[]), &[], &[], None);
189 assert_eq!(r.get("display"), Some(&Value::Keyword("flex".to_string())));
190 }
191
192 #[test]
193 fn hex_short_form_expands() {
194 let sheet = s("x { color: #abc; }");
195 let r = sheet.resolve(&n("x", &[]), &[], &[], None);
196 assert_eq!(
197 r.get("color"),
198 Some(&Value::Color(Color::rgb(0xaa, 0xbb, 0xcc)))
199 );
200 }
201
202 #[test]
203 fn unknown_pseudo_class_dropped() {
204 let sheet = parse(":nonsense { color: #fff; }").unwrap();
208 assert!(sheet.rules.is_empty());
209 }
210
211 #[test]
212 fn descendant_combinator_parses() {
213 let sheet = parse(".a .b { color: #fff; }").unwrap();
215 assert_eq!(sheet.rules.len(), 1);
216 let sel = &sheet.rules[0].selectors[0];
217 assert_eq!(sel.combinators, vec![Combinator::Descendant]);
218 assert_eq!(sel.parts.len(), 2);
219 }
220
221 #[test]
222 fn descendant_combinator_through_comment() {
223 for css in [
227 ".a /* x */ .b { color: #fff; }",
228 ".a /* x */ .b { color: #fff; }",
229 ".a/* x */ .b { color: #fff; }",
230 ] {
231 let sheet = parse(css).unwrap();
232 assert_eq!(sheet.rules.len(), 1, "input: {css}");
233 let sel = &sheet.rules[0].selectors[0];
234 assert_eq!(
235 sel.combinators,
236 vec![Combinator::Descendant],
237 "input: {css}"
238 );
239 assert_eq!(sel.parts.len(), 2, "input: {css}");
240 }
241 }
242
243 #[test]
244 fn pseudo_class_is_case_insensitive() {
245 let sheet = s(".row:HOVER { color: #fff; } .row:Focus { color: #aaa; }");
246 let h = sheet.resolve(&n("row", &["row"]), &[], &[], Some(PseudoClass::Hover));
247 let f = sheet.resolve(&n("row", &["row"]), &[], &[], Some(PseudoClass::Focus));
248 assert_eq!(
249 h.get("color"),
250 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
251 );
252 assert_eq!(
253 f.get("color"),
254 Some(&Value::Color(Color::rgb(0xaa, 0xaa, 0xaa)))
255 );
256 }
257
258 #[test]
259 fn bad_declaration_does_not_drop_neighbours() {
260 let sheet = s("x { font: 12px Arial; color: #fff; padding: 4px; }");
265 let r = sheet.resolve(&n("x", &[]), &[], &[], None);
266 assert_eq!(
267 r.get("color"),
268 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
269 );
270 let Value::LengthSet(set) = r.get("padding").unwrap() else {
271 unreachable!()
272 };
273 assert_eq!(set, &vec![Length::Px(4.0)]);
274 assert!(r.get("font").is_none(), "font must not have leaked through");
275 }
276
277 #[test]
278 fn important_flag_is_tolerated() {
279 let sheet = s("x { color: #fff !important; }");
284 let r = sheet.resolve(&n("x", &[]), &[], &[], None);
285 assert_eq!(
286 r.get("color"),
287 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
288 );
289 }
290
291 #[test]
292 fn at_rules_are_silently_skipped() {
293 let sheet = s(r#"
296 @charset "utf-8";
297 @media (min-width: 100px) { .ignored { color: #000; } }
298 .visible { color: #fff; }
299 "#);
300 let v = sheet.resolve(&n("x", &["visible"]), &[], &[], None);
301 assert_eq!(
302 v.get("color"),
303 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
304 );
305 let i = sheet.resolve(&n("x", &["ignored"]), &[], &[], None);
306 assert!(i.is_empty(), "@media block contents must not leak");
307 }
308
309 #[test]
310 fn descendant_combinator_all_shapes_parse() {
311 for css in [
314 "label span { color: #fff; }",
315 "label .b { color: #fff; }",
316 "label :hover { color: #fff; }",
317 ".a label { color: #fff; }",
318 ":hover label { color: #fff; }",
319 ] {
320 let sheet = parse(css).unwrap();
321 assert_eq!(
322 sheet.rules.len(),
323 1,
324 "descendant combinator must parse to one rule: {css}"
325 );
326 assert_eq!(
327 sheet.rules[0].selectors[0].combinators,
328 vec![Combinator::Descendant],
329 "expected Descendant combinator: {css}"
330 );
331 }
332 }
333
334 #[test]
335 fn important_flag_surfaces_on_declaration() {
336 let sheet = parse(".a { color: #fff !important; padding: 4px; }").unwrap();
337 let decls = &sheet.rules[0].declarations;
338 let color = decls.iter().find(|d| d.property == "color").unwrap();
339 let padding = decls.iter().find(|d| d.property == "padding").unwrap();
340 assert!(color.important, "!important must survive on the AST");
341 assert!(!padding.important);
342 }
343
344 #[test]
345 fn important_beats_higher_specificity() {
346 let sheet = s(".important { color: #fff !important; } \
349 .specific:hover { color: #000; }");
350 let r = sheet.resolve(
351 &n("x", &["important", "specific"]),
352 &[],
353 &[],
354 Some(PseudoClass::Hover),
355 );
356 assert_eq!(
357 r.get("color"),
358 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
359 );
360 }
361
362 #[test]
363 fn important_loses_to_later_important() {
364 let sheet = s(".a { color: #001 !important; } .a { color: #002 !important; }");
367 let r = sheet.resolve(&n("x", &["a"]), &[], &[], None);
368 assert_eq!(r.get("color"), Some(&Value::Color(Color::rgb(0, 0, 0x22))));
369 }
370
371 #[test]
372 fn malformed_rule_does_not_drop_neighbours() {
373 let sheet = s(":nonsense { color: #000; } \
376 .good { color: #fff; }");
377 let r = sheet.resolve(&n("x", &["good"]), &[], &[], None);
378 assert_eq!(
379 r.get("color"),
380 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
381 );
382 }
383
384 #[test]
385 fn unknown_color_name_rejected_for_color_property() {
386 let sheet = s("x { color: nonsense; }");
390 let r = sheet.resolve(&n("x", &[]), &[], &[], None);
391 assert!(r.get("color").is_none());
392 }
393
394 #[test]
399 fn display_flex() {
400 let r = s("x { display: flex; }").resolve(&n("x", &[]), &[], &[], None);
401 assert_eq!(r.get("display"), Some(&Value::Keyword("flex".into())));
402 }
403
404 #[test]
405 fn display_unknown_rejected() {
406 let r = s("x { display: inline; }").resolve(&n("x", &[]), &[], &[], None);
407 assert!(r.get("display").is_none());
408 }
409
410 #[test]
411 fn flex_direction() {
412 let r = s("x { flex-direction: column; }").resolve(&n("x", &[]), &[], &[], None);
413 assert_eq!(
414 r.get("flex-direction"),
415 Some(&Value::Keyword("column".into()))
416 );
417 }
418
419 #[test]
420 fn flex_grow_and_shrink() {
421 let r = s("x { flex-grow: 2; flex-shrink: 0; }").resolve(&n("x", &[]), &[], &[], None);
422 assert_eq!(r.get("flex-grow"), Some(&Value::Number(2.0)));
423 assert_eq!(r.get("flex-shrink"), Some(&Value::Number(0.0)));
424 }
425
426 #[test]
427 fn flex_grow_negative_dropped() {
428 let r = s("x { flex-grow: -1; }").resolve(&n("x", &[]), &[], &[], None);
431 assert!(r.get("flex-grow").is_none());
432 }
433
434 #[test]
435 fn flex_basis_length() {
436 let r = s("x { flex-basis: 200px; }").resolve(&n("x", &[]), &[], &[], None);
437 assert_eq!(r.get("flex-basis"), Some(&Value::Length(Length::Px(200.0))));
438 }
439
440 #[test]
441 fn flex_basis_auto() {
442 let r = s("x { flex-basis: auto; }").resolve(&n("x", &[]), &[], &[], None);
443 assert_eq!(r.get("flex-basis"), Some(&Value::Auto));
444 }
445
446 #[test]
447 fn align_items() {
448 let r = s("x { align-items: center; }").resolve(&n("x", &[]), &[], &[], None);
449 assert_eq!(r.get("align-items"), Some(&Value::Keyword("center".into())));
450 }
451
452 #[test]
453 fn justify_content() {
454 let r = s("x { justify-content: space-between; }").resolve(&n("x", &[]), &[], &[], None);
455 assert_eq!(
456 r.get("justify-content"),
457 Some(&Value::Keyword("space-between".into()))
458 );
459 }
460
461 #[test]
462 fn gap() {
463 let r = s("x { gap: 8px; row-gap: 4px; column-gap: 2px; }").resolve(
464 &n("x", &[]),
465 &[],
466 &[],
467 None,
468 );
469 assert_eq!(r.get("gap"), Some(&Value::Length(Length::Px(8.0))));
470 assert_eq!(r.get("row-gap"), Some(&Value::Length(Length::Px(4.0))));
471 assert_eq!(r.get("column-gap"), Some(&Value::Length(Length::Px(2.0))));
472 }
473
474 #[test]
477 fn border_shorthand() {
478 let r = s("x { border: 1px solid #fff; }").resolve(&n("x", &[]), &[], &[], None);
479 assert_eq!(
480 r.get("border"),
481 Some(&Value::Border {
482 width: Length::Px(1.0),
483 color: Color::rgb(0xff, 0xff, 0xff),
484 })
485 );
486 }
487
488 #[test]
489 fn border_out_of_order_tokens() {
490 let r = s("x { border: solid 1px #fff; }").resolve(&n("x", &[]), &[], &[], None);
491 assert_eq!(
492 r.get("border"),
493 Some(&Value::Border {
494 width: Length::Px(1.0),
495 color: Color::rgb(0xff, 0xff, 0xff),
496 })
497 );
498 }
499
500 #[test]
501 fn border_none_is_transparent_zero() {
502 let r = s("x { border: none; }").resolve(&n("x", &[]), &[], &[], None);
506 assert_eq!(
507 r.get("border"),
508 Some(&Value::Border {
509 width: Length::Px(0.0),
510 color: Color::rgba(0, 0, 0, 0),
511 })
512 );
513 }
514
515 #[test]
516 fn border_no_color_rejected() {
517 let r = s("x { border: 1px solid; }").resolve(&n("x", &[]), &[], &[], None);
519 assert!(r.get("border").is_none());
520 }
521
522 #[test]
523 fn border_unknown_style_keyword_ignored() {
524 for css in [
528 "x { border: 2px dashed #f00; }",
529 "x { border: 2px dotted #f00; }",
530 "x { border: 2px double #f00; }",
531 "x { border: 2px groove #f00; }",
532 ] {
533 let r = s(css).resolve(&n("x", &[]), &[], &[], None);
534 assert_eq!(
535 r.get("border"),
536 Some(&Value::Border {
537 width: Length::Px(2.0),
538 color: Color::rgb(0xff, 0x00, 0x00),
539 }),
540 "input: {css}"
541 );
542 }
543 }
544
545 #[test]
546 fn border_side() {
547 let r = s("x { border-top: 2px solid red; }").resolve(&n("x", &[]), &[], &[], None);
548 assert_eq!(
549 r.get("border-top"),
550 Some(&Value::Border {
551 width: Length::Px(2.0),
552 color: Color::rgb(0xff, 0x00, 0x00),
553 })
554 );
555 }
556
557 #[test]
558 fn border_width_and_color() {
559 let r =
560 s("x { border-width: 3px; border-color: blue; }").resolve(&n("x", &[]), &[], &[], None);
561 assert_eq!(
562 r.get("border-width"),
563 Some(&Value::LengthSet(vec![Length::Px(3.0)]))
564 );
565 assert_eq!(
566 r.get("border-color"),
567 Some(&Value::Color(Color::rgb(0x00, 0x00, 0xff)))
568 );
569 }
570
571 #[test]
572 fn border_width_four_side_shorthand() {
573 let r = s("x { border-width: 1px 2px 3px 4px; }").resolve(&n("x", &[]), &[], &[], None);
574 let Value::LengthSet(set) = r.get("border-width").unwrap() else {
575 panic!("expected LengthSet");
576 };
577 assert_eq!(
578 set,
579 &vec![
580 Length::Px(1.0),
581 Length::Px(2.0),
582 Length::Px(3.0),
583 Length::Px(4.0),
584 ]
585 );
586 }
587
588 #[test]
589 fn border_radius() {
590 let r = s("x { border-radius: 4px 8px; }").resolve(&n("x", &[]), &[], &[], None);
591 let Value::LengthSet(set) = r.get("border-radius").unwrap() else {
592 panic!("expected LengthSet");
593 };
594 assert_eq!(set, &vec![Length::Px(4.0), Length::Px(8.0)]);
595 }
596
597 #[test]
598 fn outline_shorthand() {
599 let r = s("x { outline: 1px solid #000; }").resolve(&n("x", &[]), &[], &[], None);
600 assert_eq!(
601 r.get("outline"),
602 Some(&Value::Border {
603 width: Length::Px(1.0),
604 color: Color::rgb(0x00, 0x00, 0x00),
605 })
606 );
607 }
608
609 #[test]
612 fn width_auto() {
613 let r = s("x { width: auto; }").resolve(&n("x", &[]), &[], &[], None);
614 assert_eq!(r.get("width"), Some(&Value::Auto));
615 }
616
617 #[test]
618 fn height_auto() {
619 let r = s("x { height: auto; }").resolve(&n("x", &[]), &[], &[], None);
620 assert_eq!(r.get("height"), Some(&Value::Auto));
621 }
622
623 #[test]
624 fn margin_auto() {
625 let r = s("x { margin: auto; }").resolve(&n("x", &[]), &[], &[], None);
626 assert_eq!(r.get("margin"), Some(&Value::Auto));
627 }
628
629 #[test]
630 fn margin_mixed_auto() {
631 let r = s("x { margin: 4px auto; }").resolve(&n("x", &[]), &[], &[], None);
633 let Value::SideSet(sides) = r.get("margin").unwrap() else {
634 panic!("expected SideSet");
635 };
636 assert_eq!(sides[0], SideValue::Length(Length::Px(4.0)));
637 assert_eq!(sides[1], SideValue::Auto);
638 }
639
640 #[test]
641 fn margin_all_lengths_downcasts_to_length_set() {
642 let r = s("x { margin: 4px 8px; }").resolve(&n("x", &[]), &[], &[], None);
644 assert!(
645 matches!(r.get("margin"), Some(Value::LengthSet(_))),
646 "expected LengthSet"
647 );
648 }
649
650 #[test]
651 fn expand_side_set_mirrors_css_shorthand() {
652 let one = vec![SideValue::Auto];
653 let two = vec![SideValue::Length(Length::Px(4.0)), SideValue::Auto];
654 let three = vec![
655 SideValue::Length(Length::Px(1.0)),
656 SideValue::Auto,
657 SideValue::Length(Length::Px(3.0)),
658 ];
659 let four = vec![
660 SideValue::Length(Length::Px(1.0)),
661 SideValue::Length(Length::Px(2.0)),
662 SideValue::Length(Length::Px(3.0)),
663 SideValue::Length(Length::Px(4.0)),
664 ];
665 assert_eq!(expand_side_set(&one).unwrap(), [SideValue::Auto; 4]);
666 let exp_two = expand_side_set(&two).unwrap();
667 assert_eq!(exp_two[0], SideValue::Length(Length::Px(4.0)));
668 assert_eq!(exp_two[1], SideValue::Auto);
669 assert_eq!(exp_two[2], SideValue::Length(Length::Px(4.0)));
670 assert_eq!(exp_two[3], SideValue::Auto);
671 let exp_three = expand_side_set(&three).unwrap();
672 assert_eq!(
673 exp_three[1], exp_three[3],
674 "right == left when 3 values given"
675 );
676 let exp_four = expand_side_set(&four).unwrap();
677 assert_eq!(exp_four[3], SideValue::Length(Length::Px(4.0)));
678 assert!(expand_side_set(&[]).is_none());
680 assert!(expand_side_set(&[SideValue::Auto; 5]).is_none());
681 }
682
683 #[test]
686 fn font_family_quoted_and_keyword() {
687 let r = s(r#"x { font-family: "Hack Nerd Font", monospace; }"#).resolve(
688 &n("x", &[]),
689 &[],
690 &[],
691 None,
692 );
693 let Value::FontFamilyList(list) = r.get("font-family").unwrap() else {
694 panic!("expected FontFamilyList");
695 };
696 assert_eq!(
697 list,
698 &vec!["Hack Nerd Font".to_string(), "monospace".to_string()]
699 );
700 }
701
702 #[test]
703 fn font_family_single_ident() {
704 let r = s("x { font-family: monospace; }").resolve(&n("x", &[]), &[], &[], None);
705 let Value::FontFamilyList(list) = r.get("font-family").unwrap() else {
706 panic!("expected FontFamilyList");
707 };
708 assert_eq!(list, &vec!["monospace".to_string()]);
709 }
710
711 #[test]
712 fn font_family_trailing_comma_dropped() {
713 let r =
716 s(r#"x { font-family: "Hack",; color: #fff; }"#).resolve(&n("x", &[]), &[], &[], None);
717 assert!(r.get("font-family").is_none());
718 assert_eq!(
719 r.get("color"),
720 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
721 );
722 }
723
724 #[test]
725 fn font_size() {
726 let r = s("x { font-size: 16px; }").resolve(&n("x", &[]), &[], &[], None);
727 assert_eq!(r.get("font-size"), Some(&Value::Length(Length::Px(16.0))));
728 }
729
730 #[test]
731 fn font_weight_numeric() {
732 let r = s("x { font-weight: 350; }").resolve(&n("x", &[]), &[], &[], None);
733 assert_eq!(r.get("font-weight"), Some(&Value::Number(350.0)));
734 }
735
736 #[test]
737 fn font_weight_bold_keyword() {
738 let r = s("x { font-weight: bold; }").resolve(&n("x", &[]), &[], &[], None);
739 assert_eq!(r.get("font-weight"), Some(&Value::Keyword("bold".into())));
740 }
741
742 #[test]
743 fn font_weight_bolder_rejected() {
744 let r = s("x { font-weight: bolder; }").resolve(&n("x", &[]), &[], &[], None);
745 assert!(r.get("font-weight").is_none());
746 }
747
748 #[test]
749 fn font_style() {
750 let r = s("x { font-style: italic; }").resolve(&n("x", &[]), &[], &[], None);
751 assert_eq!(r.get("font-style"), Some(&Value::Keyword("italic".into())));
752 }
753
754 #[test]
755 fn text_align() {
756 let r = s("x { text-align: center; }").resolve(&n("x", &[]), &[], &[], None);
757 assert_eq!(r.get("text-align"), Some(&Value::Keyword("center".into())));
758 }
759
760 #[test]
761 fn line_height_unitless() {
762 let r = s("x { line-height: 1.5; }").resolve(&n("x", &[]), &[], &[], None);
763 assert_eq!(r.get("line-height"), Some(&Value::Number(1.5)));
764 }
765
766 #[test]
767 fn line_height_px() {
768 let r = s("x { line-height: 24px; }").resolve(&n("x", &[]), &[], &[], None);
769 assert_eq!(r.get("line-height"), Some(&Value::Length(Length::Px(24.0))));
770 }
771
772 #[test]
775 fn font_style_oblique_accepted() {
776 let r = s("x { font-style: oblique; }").resolve(&n("x", &[]), &[], &[], None);
777 assert_eq!(r.get("font-style"), Some(&Value::Keyword("oblique".into())));
778 }
779
780 #[test]
781 fn font_style_unknown_keyword_dropped() {
782 let r = s("x { font-style: weird; color: #fff; }").resolve(&n("x", &[]), &[], &[], None);
784 assert!(r.get("font-style").is_none());
785 assert_eq!(
786 r.get("color"),
787 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
788 );
789 }
790
791 #[test]
794 fn border_top_color_resolves() {
795 for side in ["top", "right", "bottom", "left"] {
796 let css = format!("x {{ border-{side}-color: red; }}");
797 let r = s(&css).resolve(&n("x", &[]), &[], &[], None);
798 let prop = format!("border-{side}-color");
799 assert_eq!(
800 r.get(&prop),
801 Some(&Value::Color(Color::rgb(0xff, 0x00, 0x00))),
802 "border-{side}-color must resolve as Color"
803 );
804 }
805 }
806
807 #[test]
810 fn font_weight_out_of_range_dropped() {
811 for bad in ["9999", "-100", "0.5", "0"] {
813 let css = format!("x {{ font-weight: {bad}; color: #fff; }}");
814 let r = s(&css).resolve(&n("x", &[]), &[], &[], None);
815 assert!(
816 r.get("font-weight").is_none(),
817 "font-weight: {bad} should be dropped"
818 );
819 assert!(
821 r.get("color").is_some(),
822 "color must survive bad font-weight: {bad}"
823 );
824 }
825 let r = s("x { font-weight: 700; }").resolve(&n("x", &[]), &[], &[], None);
827 assert_eq!(r.get("font-weight"), Some(&Value::Number(700.0)));
828 for kw in ["bold", "normal"] {
830 let css = format!("x {{ font-weight: {kw}; }}");
831 let r = s(&css).resolve(&n("x", &[]), &[], &[], None);
832 assert_eq!(
833 r.get("font-weight"),
834 Some(&Value::Keyword(kw.into())),
835 "font-weight: {kw} keyword must resolve"
836 );
837 }
838 let r = s("x { font-weight: bolder; color: #fff; }").resolve(&n("x", &[]), &[], &[], None);
840 assert!(
841 r.get("font-weight").is_none(),
842 "unsupported font-weight keyword must be dropped"
843 );
844 for ok in ["1", "1000"] {
846 let css = format!("x {{ font-weight: {ok}; }}");
847 let r = s(&css).resolve(&n("x", &[]), &[], &[], None);
848 assert_eq!(
849 r.get("font-weight"),
850 Some(&Value::Number(ok.parse().unwrap())),
851 "font-weight: {ok} boundary value must resolve"
852 );
853 }
854 }
855
856 #[test]
859 fn named_colors_level1() {
860 let cases: &[(&str, Color)] = &[
861 ("silver", Color::rgb(0xc0, 0xc0, 0xc0)),
862 ("maroon", Color::rgb(0x80, 0x00, 0x00)),
863 ("purple", Color::rgb(0x80, 0x00, 0x80)),
864 ("fuchsia", Color::rgb(0xff, 0x00, 0xff)),
865 ("lime", Color::rgb(0x00, 0xff, 0x00)),
866 ("olive", Color::rgb(0x80, 0x80, 0x00)),
867 ("yellow", Color::rgb(0xff, 0xff, 0x00)),
868 ("navy", Color::rgb(0x00, 0x00, 0x80)),
869 ("teal", Color::rgb(0x00, 0x80, 0x80)),
870 ("aqua", Color::rgb(0x00, 0xff, 0xff)),
871 ];
872 for (name, expected) in cases {
873 let css = format!("x {{ color: {name}; }}");
874 let r = s(&css).resolve(&n("x", &[]), &[], &[], None);
875 assert_eq!(
876 r.get("color"),
877 Some(&Value::Color(*expected)),
878 "named color `{name}` mismatch"
879 );
880 }
881 }
882
883 #[test]
884 fn named_colors_extras() {
885 let cases: &[(&str, Color)] = &[
886 ("gray", Color::rgb(0x80, 0x80, 0x80)),
887 ("grey", Color::rgb(0x80, 0x80, 0x80)),
888 ("cyan", Color::rgb(0x00, 0xff, 0xff)),
889 ("magenta", Color::rgb(0xff, 0x00, 0xff)),
890 ("orange", Color::rgb(0xff, 0xa5, 0x00)),
891 ("brown", Color::rgb(0xa5, 0x2a, 0x2a)),
892 ("pink", Color::rgb(0xff, 0xc0, 0xcb)),
893 ];
894 for (name, expected) in cases {
895 let css = format!("x {{ color: {name}; }}");
896 let r = s(&css).resolve(&n("x", &[]), &[], &[], None);
897 assert_eq!(
898 r.get("color"),
899 Some(&Value::Color(*expected)),
900 "named color `{name}` mismatch"
901 );
902 }
903 }
904
905 #[test]
908 fn descendant_no_match_without_ancestor() {
909 let sheet = s(".outer .target { color: #fff; }");
911 let r = sheet.resolve(&n("div", &["target"]), &[], &[], None);
912 assert!(r.get("color").is_none());
913 }
914
915 #[test]
916 fn descendant_match_with_ancestor() {
917 let sheet = s(".outer .target { color: #fff; }");
918 let ancestors = [n("div", &["outer"])];
919 let r = sheet.resolve(&n("div", &["target"]), &ancestors, &[], None);
920 assert_eq!(
921 r.get("color"),
922 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
923 );
924 }
925
926 #[test]
927 fn descendant_match_with_distant_ancestor() {
928 let sheet = s(".outer .target { color: #fff; }");
930 let ancestors = [n("root", &[]), n("div", &["outer"]), n("div", &["mid"])];
931 let r = sheet.resolve(&n("span", &["target"]), &ancestors, &[], None);
932 assert_eq!(
933 r.get("color"),
934 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
935 );
936 }
937
938 #[test]
939 fn child_match_immediate_parent() {
940 let sheet = s(".outer > .target { color: #fff; }");
941 let ancestors = [n("div", &["outer"])];
942 let r = sheet.resolve(&n("span", &["target"]), &ancestors, &[], None);
943 assert_eq!(
944 r.get("color"),
945 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
946 );
947 }
948
949 #[test]
950 fn child_no_match_grandparent() {
951 let sheet = s(".outer > .target { color: #fff; }");
953 let ancestors = [n("div", &["outer"]), n("div", &["mid"])];
954 let r = sheet.resolve(&n("span", &["target"]), &ancestors, &[], None);
955 assert!(r.get("color").is_none());
956 }
957
958 #[test]
959 fn adjacent_sibling_match() {
960 let sheet = s(".prev + .target { color: #fff; }");
961 let prev_siblings = [n("div", &["prev"])];
962 let r = sheet.resolve(&n("span", &["target"]), &[], &prev_siblings, None);
963 assert_eq!(
964 r.get("color"),
965 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
966 );
967 }
968
969 #[test]
970 fn adjacent_sibling_no_match_non_immediate() {
971 let sheet = s(".prev + .target { color: #fff; }");
973 let prev_siblings = [n("div", &["prev"]), n("div", &["between"])];
974 let r = sheet.resolve(&n("span", &["target"]), &[], &prev_siblings, None);
975 assert!(r.get("color").is_none());
976 }
977
978 #[test]
979 fn general_sibling_match_any() {
980 let sheet = s(".prev ~ .target { color: #fff; }");
981 let prev_siblings = [n("div", &["prev"]), n("div", &["between"])];
983 let r = sheet.resolve(&n("span", &["target"]), &[], &prev_siblings, None);
984 assert_eq!(
985 r.get("color"),
986 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
987 );
988 }
989
990 #[test]
991 fn general_sibling_no_match_without_sibling() {
992 let sheet = s(".prev ~ .target { color: #fff; }");
993 let r = sheet.resolve(&n("span", &["target"]), &[], &[], None);
994 assert!(r.get("color").is_none());
995 }
996
997 #[test]
998 fn chained_adjacent_siblings_match() {
999 let sheet = s(".a + .b + .target { color: #fff; }");
1005 let prev_siblings = [n("div", &["a"]), n("div", &["b"])];
1006 let r = sheet.resolve(&n("span", &["target"]), &[], &prev_siblings, None);
1007 assert_eq!(
1008 r.get("color"),
1009 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
1010 );
1011 }
1012
1013 #[test]
1014 fn chained_general_siblings_match() {
1015 let sheet = s(".a ~ .b ~ .target { color: #fff; }");
1018 let prev_siblings = [n("div", &["a"]), n("div", &["between"]), n("div", &["b"])];
1019 let r = sheet.resolve(&n("span", &["target"]), &[], &prev_siblings, None);
1020 assert_eq!(
1021 r.get("color"),
1022 Some(&Value::Color(Color::rgb(0xff, 0xff, 0xff)))
1023 );
1024 }
1025
1026 #[test]
1027 fn specificity_sums_across_parts() {
1028 let sheet = s(".a .b.c { color: #fff; }");
1030 let sel = &sheet.rules[0].selectors[0];
1031 assert_eq!(sel.specificity(), 30);
1032 }
1033
1034 #[test]
1035 fn pseudo_on_ancestor_part_does_not_match() {
1036 let sheet = s(".outer:hover > .target { color: #fff; }");
1040 let ancestors = [n("div", &["outer"])];
1041 let r = sheet.resolve(
1044 &n("span", &["target"]),
1045 &ancestors,
1046 &[],
1047 Some(PseudoClass::Hover),
1048 );
1049 assert!(r.get("color").is_none());
1050 }
1051
1052 #[test]
1053 fn explicit_child_combinator_with_whitespace() {
1054 let sheet = s(".a > .b { color: #fff; }");
1056 let sel = &sheet.rules[0].selectors[0];
1057 assert_eq!(sel.combinators, vec![Combinator::Child]);
1058 }
1059
1060 #[test]
1061 fn explicit_adjacent_sibling_combinator() {
1062 let sheet = s(".a + .b { color: #fff; }");
1063 let sel = &sheet.rules[0].selectors[0];
1064 assert_eq!(sel.combinators, vec![Combinator::AdjacentSibling]);
1065 }
1066
1067 #[test]
1068 fn explicit_general_sibling_combinator() {
1069 let sheet = s(".a ~ .b { color: #fff; }");
1070 let sel = &sheet.rules[0].selectors[0];
1071 assert_eq!(sel.combinators, vec![Combinator::GeneralSibling]);
1072 }
1073
1074 #[test]
1077 fn iter_returns_source_order() {
1078 let sheet = s(".a { width: 10px; } .a { color: #001; }");
1086 let r = sheet.resolve(&n("x", &["a"]), &[], &[], None);
1087 let keys: Vec<&str> = r.iter().map(|(k, _)| k).collect();
1088 let width_pos = keys.iter().position(|&k| k == "width").unwrap();
1089 let color_pos = keys.iter().position(|&k| k == "color").unwrap();
1090 assert!(
1091 width_pos < color_pos,
1092 "width (rule 0) must come before color (rule 1): got {keys:?}"
1093 );
1094 }
1095
1096 #[test]
1097 fn shorthand_then_longhand_source_order() {
1098 let sheet_a = s("x { border: 1px solid red; } x { border-color: blue; }");
1101 let r_a = sheet_a.resolve(&n("x", &[]), &[], &[], None);
1102 let keys_a: Vec<&str> = r_a.iter().map(|(k, _)| k).collect();
1103 let border_pos = keys_a.iter().position(|&k| k == "border").unwrap();
1104 let bc_pos = keys_a.iter().position(|&k| k == "border-color").unwrap();
1105 assert!(
1106 border_pos < bc_pos,
1107 "border (rule 0) must come before border-color (rule 1): got {keys_a:?}"
1108 );
1109
1110 let sheet_b = s("x { border-color: blue; } x { border: 1px solid red; }");
1113 let r_b = sheet_b.resolve(&n("x", &[]), &[], &[], None);
1114 let keys_b: Vec<&str> = r_b.iter().map(|(k, _)| k).collect();
1115 let border_pos_b = keys_b.iter().position(|&k| k == "border").unwrap();
1116 let bc_pos_b = keys_b.iter().position(|&k| k == "border-color").unwrap();
1117 assert!(
1118 bc_pos_b < border_pos_b,
1119 "border-color (rule 0) must come before border (rule 1): got {keys_b:?}"
1120 );
1121 }
1122
1123 #[test]
1124 fn intra_rule_source_order() {
1125 let sheet = s("x { border-color: blue; border: 1px solid red; }");
1132 let r = sheet.resolve(&n("x", &[]), &[], &[], None);
1133 let keys: Vec<&str> = r.iter().map(|(k, _)| k).collect();
1134 let bc_pos = keys.iter().position(|&k| k == "border-color").unwrap();
1135 let border_pos = keys.iter().position(|&k| k == "border").unwrap();
1136 assert!(
1137 bc_pos < border_pos,
1138 "border-color (decl 0) must come before border (decl 1) within the same rule: got {keys:?}"
1139 );
1140
1141 let sheet_rev = s("x { border: 1px solid red; border-color: blue; }");
1144 let r_rev = sheet_rev.resolve(&n("x", &[]), &[], &[], None);
1145 let keys_rev: Vec<&str> = r_rev.iter().map(|(k, _)| k).collect();
1146 let border_pos_rev = keys_rev.iter().position(|&k| k == "border").unwrap();
1147 let bc_pos_rev = keys_rev.iter().position(|&k| k == "border-color").unwrap();
1148 assert!(
1149 border_pos_rev < bc_pos_rev,
1150 "border (decl 0) must come before border-color (decl 1) within the same rule: got {keys_rev:?}"
1151 );
1152 }
1153}