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