we (web engine): Experimental web browser project to understand the limits of Claude
1//! CSS cascade and computed style resolution.
2//!
3//! For each DOM element, resolves the final computed value of every CSS property
4//! by collecting matching rules, applying the cascade (specificity + source order),
5//! handling property inheritance, and resolving relative values.
6
7use we_css::parser::{Declaration, Stylesheet};
8use we_css::values::{expand_shorthand, parse_value, Color, CssValue, LengthUnit};
9use we_dom::{Document, NodeData, NodeId};
10
11use crate::matching::collect_matching_rules;
12
13// ---------------------------------------------------------------------------
14// Display
15// ---------------------------------------------------------------------------
16
17/// CSS `display` property values (subset).
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum Display {
20 Block,
21 #[default]
22 Inline,
23 Flex,
24 InlineFlex,
25 None,
26}
27
28// ---------------------------------------------------------------------------
29// Position
30// ---------------------------------------------------------------------------
31
32/// CSS `position` property values.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum Position {
35 #[default]
36 Static,
37 Relative,
38 Absolute,
39 Fixed,
40 Sticky,
41}
42
43// ---------------------------------------------------------------------------
44// FontWeight
45// ---------------------------------------------------------------------------
46
47/// CSS `font-weight` as a numeric value (100-900).
48#[derive(Debug, Clone, Copy, PartialEq)]
49pub struct FontWeight(pub f32);
50
51impl Default for FontWeight {
52 fn default() -> Self {
53 FontWeight(400.0) // normal
54 }
55}
56
57// ---------------------------------------------------------------------------
58// FontStyle
59// ---------------------------------------------------------------------------
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
62pub enum FontStyle {
63 #[default]
64 Normal,
65 Italic,
66 Oblique,
67}
68
69// ---------------------------------------------------------------------------
70// TextAlign
71// ---------------------------------------------------------------------------
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
74pub enum TextAlign {
75 #[default]
76 Left,
77 Right,
78 Center,
79 Justify,
80}
81
82// ---------------------------------------------------------------------------
83// TextDecoration
84// ---------------------------------------------------------------------------
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
87pub enum TextDecoration {
88 #[default]
89 None,
90 Underline,
91 Overline,
92 LineThrough,
93}
94
95// ---------------------------------------------------------------------------
96// BoxSizing
97// ---------------------------------------------------------------------------
98
99/// CSS `box-sizing` property values.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101pub enum BoxSizing {
102 #[default]
103 ContentBox,
104 BorderBox,
105}
106
107// ---------------------------------------------------------------------------
108// Float
109// ---------------------------------------------------------------------------
110
111/// CSS `float` property values.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
113pub enum Float {
114 #[default]
115 None,
116 Left,
117 Right,
118}
119
120// ---------------------------------------------------------------------------
121// Clear
122// ---------------------------------------------------------------------------
123
124/// CSS `clear` property values.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
126pub enum Clear {
127 #[default]
128 None,
129 Left,
130 Right,
131 Both,
132}
133
134// ---------------------------------------------------------------------------
135// Overflow
136// ---------------------------------------------------------------------------
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
139pub enum Overflow {
140 #[default]
141 Visible,
142 Hidden,
143 Scroll,
144 Auto,
145}
146
147// ---------------------------------------------------------------------------
148// Visibility
149// ---------------------------------------------------------------------------
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
152pub enum Visibility {
153 #[default]
154 Visible,
155 Hidden,
156 Collapse,
157}
158
159// ---------------------------------------------------------------------------
160// Flex enums
161// ---------------------------------------------------------------------------
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
164pub enum FlexDirection {
165 #[default]
166 Row,
167 RowReverse,
168 Column,
169 ColumnReverse,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
173pub enum FlexWrap {
174 #[default]
175 Nowrap,
176 Wrap,
177 WrapReverse,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
181pub enum JustifyContent {
182 #[default]
183 FlexStart,
184 FlexEnd,
185 Center,
186 SpaceBetween,
187 SpaceAround,
188 SpaceEvenly,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
192pub enum AlignItems {
193 #[default]
194 Stretch,
195 FlexStart,
196 FlexEnd,
197 Center,
198 Baseline,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
202pub enum AlignContent {
203 #[default]
204 Stretch,
205 FlexStart,
206 FlexEnd,
207 Center,
208 SpaceBetween,
209 SpaceAround,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
213pub enum AlignSelf {
214 #[default]
215 Auto,
216 FlexStart,
217 FlexEnd,
218 Center,
219 Baseline,
220 Stretch,
221}
222
223// ---------------------------------------------------------------------------
224// BorderStyle
225// ---------------------------------------------------------------------------
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
228pub enum BorderStyle {
229 #[default]
230 None,
231 Hidden,
232 Dotted,
233 Dashed,
234 Solid,
235 Double,
236 Groove,
237 Ridge,
238 Inset,
239 Outset,
240}
241
242// ---------------------------------------------------------------------------
243// LengthOrAuto
244// ---------------------------------------------------------------------------
245
246/// A computed length (resolved to px), a percentage (unresolved), or `auto`.
247#[derive(Debug, Clone, Copy, PartialEq, Default)]
248pub enum LengthOrAuto {
249 Length(f32),
250 /// Percentage value (0.0–100.0), resolved during layout against the containing block.
251 Percentage(f32),
252 #[default]
253 Auto,
254}
255
256// ---------------------------------------------------------------------------
257// ComputedStyle
258// ---------------------------------------------------------------------------
259
260/// The fully resolved computed style for a single element.
261#[derive(Debug, Clone, PartialEq)]
262pub struct ComputedStyle {
263 // Display
264 pub display: Display,
265
266 // Box model: margin
267 pub margin_top: LengthOrAuto,
268 pub margin_right: LengthOrAuto,
269 pub margin_bottom: LengthOrAuto,
270 pub margin_left: LengthOrAuto,
271
272 // Box model: padding (percentages resolve against containing block width)
273 pub padding_top: LengthOrAuto,
274 pub padding_right: LengthOrAuto,
275 pub padding_bottom: LengthOrAuto,
276 pub padding_left: LengthOrAuto,
277
278 // Box model: border width
279 pub border_top_width: f32,
280 pub border_right_width: f32,
281 pub border_bottom_width: f32,
282 pub border_left_width: f32,
283
284 // Box model: border style
285 pub border_top_style: BorderStyle,
286 pub border_right_style: BorderStyle,
287 pub border_bottom_style: BorderStyle,
288 pub border_left_style: BorderStyle,
289
290 // Box model: border color
291 pub border_top_color: Color,
292 pub border_right_color: Color,
293 pub border_bottom_color: Color,
294 pub border_left_color: Color,
295
296 // Box model: dimensions
297 pub width: LengthOrAuto,
298 pub height: LengthOrAuto,
299
300 // Box model: sizing
301 pub box_sizing: BoxSizing,
302
303 // Text / inherited
304 pub color: Color,
305 pub font_size: f32,
306 pub font_weight: FontWeight,
307 pub font_style: FontStyle,
308 pub font_family: String,
309 pub text_align: TextAlign,
310 pub text_decoration: TextDecoration,
311 pub line_height: f32,
312
313 // Background
314 pub background_color: Color,
315
316 // Position
317 pub position: Position,
318 pub top: LengthOrAuto,
319 pub right: LengthOrAuto,
320 pub bottom: LengthOrAuto,
321 pub left: LengthOrAuto,
322 pub z_index: Option<i32>,
323
324 // Float
325 pub float: Float,
326 pub clear: Clear,
327
328 // Overflow
329 pub overflow: Overflow,
330
331 // Visibility (inherited)
332 pub visibility: Visibility,
333
334 // Flex container properties
335 pub flex_direction: FlexDirection,
336 pub flex_wrap: FlexWrap,
337 pub justify_content: JustifyContent,
338 pub align_items: AlignItems,
339 pub align_content: AlignContent,
340 pub row_gap: f32,
341 pub column_gap: f32,
342
343 // Flex item properties
344 pub flex_grow: f32,
345 pub flex_shrink: f32,
346 pub flex_basis: LengthOrAuto,
347 pub align_self: AlignSelf,
348 pub order: i32,
349}
350
351impl Default for ComputedStyle {
352 fn default() -> Self {
353 ComputedStyle {
354 display: Display::Inline,
355
356 margin_top: LengthOrAuto::Length(0.0),
357 margin_right: LengthOrAuto::Length(0.0),
358 margin_bottom: LengthOrAuto::Length(0.0),
359 margin_left: LengthOrAuto::Length(0.0),
360
361 padding_top: LengthOrAuto::Length(0.0),
362 padding_right: LengthOrAuto::Length(0.0),
363 padding_bottom: LengthOrAuto::Length(0.0),
364 padding_left: LengthOrAuto::Length(0.0),
365
366 border_top_width: 0.0,
367 border_right_width: 0.0,
368 border_bottom_width: 0.0,
369 border_left_width: 0.0,
370
371 border_top_style: BorderStyle::None,
372 border_right_style: BorderStyle::None,
373 border_bottom_style: BorderStyle::None,
374 border_left_style: BorderStyle::None,
375
376 border_top_color: Color::rgb(0, 0, 0),
377 border_right_color: Color::rgb(0, 0, 0),
378 border_bottom_color: Color::rgb(0, 0, 0),
379 border_left_color: Color::rgb(0, 0, 0),
380
381 width: LengthOrAuto::Auto,
382 height: LengthOrAuto::Auto,
383
384 box_sizing: BoxSizing::ContentBox,
385
386 color: Color::rgb(0, 0, 0),
387 font_size: 16.0,
388 font_weight: FontWeight(400.0),
389 font_style: FontStyle::Normal,
390 font_family: String::new(),
391 text_align: TextAlign::Left,
392 text_decoration: TextDecoration::None,
393 line_height: 19.2, // 1.2 * 16
394
395 background_color: Color::new(0, 0, 0, 0), // transparent
396
397 position: Position::Static,
398 top: LengthOrAuto::Auto,
399 right: LengthOrAuto::Auto,
400 bottom: LengthOrAuto::Auto,
401 left: LengthOrAuto::Auto,
402 z_index: None,
403
404 float: Float::None,
405 clear: Clear::None,
406
407 overflow: Overflow::Visible,
408 visibility: Visibility::Visible,
409
410 flex_direction: FlexDirection::Row,
411 flex_wrap: FlexWrap::Nowrap,
412 justify_content: JustifyContent::FlexStart,
413 align_items: AlignItems::Stretch,
414 align_content: AlignContent::Stretch,
415 row_gap: 0.0,
416 column_gap: 0.0,
417
418 flex_grow: 0.0,
419 flex_shrink: 1.0,
420 flex_basis: LengthOrAuto::Auto,
421 align_self: AlignSelf::Auto,
422 order: 0,
423 }
424 }
425}
426
427// ---------------------------------------------------------------------------
428// Property classification: inherited vs non-inherited
429// ---------------------------------------------------------------------------
430
431fn is_inherited_property(property: &str) -> bool {
432 matches!(
433 property,
434 "color"
435 | "font-size"
436 | "font-weight"
437 | "font-style"
438 | "font-family"
439 | "text-align"
440 | "text-decoration"
441 | "line-height"
442 | "visibility"
443 )
444}
445
446// ---------------------------------------------------------------------------
447// User-agent stylesheet
448// ---------------------------------------------------------------------------
449
450/// Returns the user-agent default stylesheet.
451pub fn ua_stylesheet() -> Stylesheet {
452 use we_css::parser::Parser;
453 Parser::parse(UA_CSS)
454}
455
456const UA_CSS: &str = r#"
457html, body, div, p, pre, h1, h2, h3, h4, h5, h6,
458ul, ol, li, blockquote, section, article, nav,
459header, footer, main, hr {
460 display: block;
461}
462
463span, a, em, strong, b, i, u, code, small, sub, sup, br {
464 display: inline;
465}
466
467head, title, script, style, link, meta {
468 display: none;
469}
470
471body {
472 margin: 8px;
473}
474
475h1 {
476 font-size: 2em;
477 margin-top: 0.67em;
478 margin-bottom: 0.67em;
479 font-weight: bold;
480}
481
482h2 {
483 font-size: 1.5em;
484 margin-top: 0.83em;
485 margin-bottom: 0.83em;
486 font-weight: bold;
487}
488
489h3 {
490 font-size: 1.17em;
491 margin-top: 1em;
492 margin-bottom: 1em;
493 font-weight: bold;
494}
495
496h4 {
497 font-size: 1em;
498 margin-top: 1.33em;
499 margin-bottom: 1.33em;
500 font-weight: bold;
501}
502
503h5 {
504 font-size: 0.83em;
505 margin-top: 1.67em;
506 margin-bottom: 1.67em;
507 font-weight: bold;
508}
509
510h6 {
511 font-size: 0.67em;
512 margin-top: 2.33em;
513 margin-bottom: 2.33em;
514 font-weight: bold;
515}
516
517p {
518 margin-top: 1em;
519 margin-bottom: 1em;
520}
521
522strong, b {
523 font-weight: bold;
524}
525
526em, i {
527 font-style: italic;
528}
529
530a {
531 color: blue;
532 text-decoration: underline;
533}
534
535u {
536 text-decoration: underline;
537}
538"#;
539
540// ---------------------------------------------------------------------------
541// Resolve a CssValue to f32 px given context
542// ---------------------------------------------------------------------------
543
544fn resolve_length_unit(value: f64, unit: LengthUnit, em_base: f32, viewport: (f32, f32)) -> f32 {
545 let v = value as f32;
546 let (vw, vh) = viewport;
547 match unit {
548 LengthUnit::Px => v,
549 LengthUnit::Em => v * em_base,
550 LengthUnit::Rem => v * 16.0, // root font size is always 16px for now
551 LengthUnit::Pt => v * (96.0 / 72.0),
552 LengthUnit::Cm => v * (96.0 / 2.54),
553 LengthUnit::Mm => v * (96.0 / 25.4),
554 LengthUnit::In => v * 96.0,
555 LengthUnit::Pc => v * 16.0,
556 LengthUnit::Vw => v * vw / 100.0,
557 LengthUnit::Vh => v * vh / 100.0,
558 LengthUnit::Vmin => v * vw.min(vh) / 100.0,
559 LengthUnit::Vmax => v * vw.max(vh) / 100.0,
560 }
561}
562
563/// Resolve a CSS value to `LengthOrAuto` for layout properties (width, height,
564/// margin, padding, top/right/bottom/left). Percentages are preserved as
565/// `LengthOrAuto::Percentage` for later resolution during layout against the
566/// containing block.
567fn resolve_layout_length_or_auto(
568 value: &CssValue,
569 current_font_size: f32,
570 viewport: (f32, f32),
571) -> LengthOrAuto {
572 match value {
573 CssValue::Auto => LengthOrAuto::Auto,
574 CssValue::Percentage(p) => LengthOrAuto::Percentage(*p as f32),
575 CssValue::Length(n, unit) => {
576 LengthOrAuto::Length(resolve_length_unit(*n, *unit, current_font_size, viewport))
577 }
578 CssValue::Zero => LengthOrAuto::Length(0.0),
579 CssValue::Number(n) if *n == 0.0 => LengthOrAuto::Length(0.0),
580 _ => LengthOrAuto::Auto,
581 }
582}
583
584fn resolve_color(value: &CssValue, current_color: Color) -> Option<Color> {
585 match value {
586 CssValue::Color(c) => Some(*c),
587 CssValue::CurrentColor => Some(current_color),
588 CssValue::Transparent => Some(Color::new(0, 0, 0, 0)),
589 _ => None,
590 }
591}
592
593// ---------------------------------------------------------------------------
594// Apply a single property value to a ComputedStyle
595// ---------------------------------------------------------------------------
596
597fn apply_property(
598 style: &mut ComputedStyle,
599 property: &str,
600 value: &CssValue,
601 parent: &ComputedStyle,
602 viewport: (f32, f32),
603) {
604 // Handle inherit/initial/unset
605 match value {
606 CssValue::Inherit => {
607 inherit_property(style, property, parent);
608 return;
609 }
610 CssValue::Initial => {
611 reset_property_to_initial(style, property);
612 return;
613 }
614 CssValue::Unset => {
615 if is_inherited_property(property) {
616 inherit_property(style, property, parent);
617 } else {
618 reset_property_to_initial(style, property);
619 }
620 return;
621 }
622 _ => {}
623 }
624
625 let parent_fs = parent.font_size;
626 let current_fs = style.font_size;
627
628 match property {
629 "display" => {
630 style.display = match value {
631 CssValue::Keyword(k) => match k.as_str() {
632 "block" => Display::Block,
633 "inline" => Display::Inline,
634 "flex" => Display::Flex,
635 "inline-flex" => Display::InlineFlex,
636 _ => Display::Block,
637 },
638 CssValue::None => Display::None,
639 _ => style.display,
640 };
641 }
642
643 // Margin (percentages preserved for layout resolution)
644 "margin-top" => {
645 style.margin_top = resolve_layout_length_or_auto(value, current_fs, viewport);
646 }
647 "margin-right" => {
648 style.margin_right = resolve_layout_length_or_auto(value, current_fs, viewport);
649 }
650 "margin-bottom" => {
651 style.margin_bottom = resolve_layout_length_or_auto(value, current_fs, viewport);
652 }
653 "margin-left" => {
654 style.margin_left = resolve_layout_length_or_auto(value, current_fs, viewport);
655 }
656
657 // Padding (percentages preserved for layout resolution)
658 "padding-top" => {
659 style.padding_top = resolve_layout_length_or_auto(value, current_fs, viewport);
660 }
661 "padding-right" => {
662 style.padding_right = resolve_layout_length_or_auto(value, current_fs, viewport);
663 }
664 "padding-bottom" => {
665 style.padding_bottom = resolve_layout_length_or_auto(value, current_fs, viewport);
666 }
667 "padding-left" => {
668 style.padding_left = resolve_layout_length_or_auto(value, current_fs, viewport);
669 }
670
671 // Border width
672 "border-top-width" | "border-right-width" | "border-bottom-width" | "border-left-width" => {
673 let w = resolve_border_width(value, parent_fs, viewport);
674 match property {
675 "border-top-width" => style.border_top_width = w,
676 "border-right-width" => style.border_right_width = w,
677 "border-bottom-width" => style.border_bottom_width = w,
678 "border-left-width" => style.border_left_width = w,
679 _ => {}
680 }
681 }
682
683 // Border width shorthand (single value applied to all sides)
684 "border-width" => {
685 let w = resolve_border_width(value, parent_fs, viewport);
686 style.border_top_width = w;
687 style.border_right_width = w;
688 style.border_bottom_width = w;
689 style.border_left_width = w;
690 }
691
692 // Border style
693 "border-top-style" | "border-right-style" | "border-bottom-style" | "border-left-style" => {
694 let s = parse_border_style(value);
695 match property {
696 "border-top-style" => style.border_top_style = s,
697 "border-right-style" => style.border_right_style = s,
698 "border-bottom-style" => style.border_bottom_style = s,
699 "border-left-style" => style.border_left_style = s,
700 _ => {}
701 }
702 }
703
704 "border-style" => {
705 let s = parse_border_style(value);
706 style.border_top_style = s;
707 style.border_right_style = s;
708 style.border_bottom_style = s;
709 style.border_left_style = s;
710 }
711
712 // Border color
713 "border-top-color" | "border-right-color" | "border-bottom-color" | "border-left-color" => {
714 if let Some(c) = resolve_color(value, style.color) {
715 match property {
716 "border-top-color" => style.border_top_color = c,
717 "border-right-color" => style.border_right_color = c,
718 "border-bottom-color" => style.border_bottom_color = c,
719 "border-left-color" => style.border_left_color = c,
720 _ => {}
721 }
722 }
723 }
724
725 "border-color" => {
726 if let Some(c) = resolve_color(value, style.color) {
727 style.border_top_color = c;
728 style.border_right_color = c;
729 style.border_bottom_color = c;
730 style.border_left_color = c;
731 }
732 }
733
734 // Dimensions (percentages preserved for layout resolution)
735 "width" => {
736 style.width = resolve_layout_length_or_auto(value, current_fs, viewport);
737 }
738 "height" => {
739 style.height = resolve_layout_length_or_auto(value, current_fs, viewport);
740 }
741
742 // Box sizing
743 "box-sizing" => {
744 style.box_sizing = match value {
745 CssValue::Keyword(k) => match k.as_str() {
746 "content-box" => BoxSizing::ContentBox,
747 "border-box" => BoxSizing::BorderBox,
748 _ => style.box_sizing,
749 },
750 _ => style.box_sizing,
751 };
752 }
753
754 // Color (inherited)
755 "color" => {
756 if let Some(c) = resolve_color(value, parent.color) {
757 style.color = c;
758 // Update border colors to match (currentColor default)
759 }
760 }
761
762 // Font-size (inherited) — special: em units relative to parent
763 "font-size" => {
764 match value {
765 CssValue::Length(n, unit) => {
766 style.font_size = resolve_length_unit(*n, *unit, parent_fs, viewport);
767 }
768 CssValue::Percentage(p) => {
769 style.font_size = (*p / 100.0) as f32 * parent_fs;
770 }
771 CssValue::Zero => {
772 style.font_size = 0.0;
773 }
774 CssValue::Keyword(k) => {
775 style.font_size = match k.as_str() {
776 "xx-small" => 9.0,
777 "x-small" => 10.0,
778 "small" => 13.0,
779 "medium" => 16.0,
780 "large" => 18.0,
781 "x-large" => 24.0,
782 "xx-large" => 32.0,
783 "smaller" => parent_fs * 0.833,
784 "larger" => parent_fs * 1.2,
785 _ => style.font_size,
786 };
787 }
788 _ => {}
789 }
790 // Update line-height when font-size changes
791 style.line_height = style.font_size * 1.2;
792 }
793
794 // Font-weight (inherited)
795 "font-weight" => {
796 style.font_weight = match value {
797 CssValue::Keyword(k) => match k.as_str() {
798 "normal" => FontWeight(400.0),
799 "bold" => FontWeight(700.0),
800 "lighter" => FontWeight((parent.font_weight.0 - 100.0).max(100.0)),
801 "bolder" => FontWeight((parent.font_weight.0 + 300.0).min(900.0)),
802 _ => style.font_weight,
803 },
804 CssValue::Number(n) => FontWeight(*n as f32),
805 _ => style.font_weight,
806 };
807 }
808
809 // Font-style (inherited)
810 "font-style" => {
811 style.font_style = match value {
812 CssValue::Keyword(k) => match k.as_str() {
813 "normal" => FontStyle::Normal,
814 "italic" => FontStyle::Italic,
815 "oblique" => FontStyle::Oblique,
816 _ => style.font_style,
817 },
818 _ => style.font_style,
819 };
820 }
821
822 // Font-family (inherited)
823 "font-family" => {
824 if let CssValue::String(s) | CssValue::Keyword(s) = value {
825 style.font_family = s.clone();
826 }
827 }
828
829 // Text-align (inherited)
830 "text-align" => {
831 style.text_align = match value {
832 CssValue::Keyword(k) => match k.as_str() {
833 "left" => TextAlign::Left,
834 "right" => TextAlign::Right,
835 "center" => TextAlign::Center,
836 "justify" => TextAlign::Justify,
837 _ => style.text_align,
838 },
839 _ => style.text_align,
840 };
841 }
842
843 // Text-decoration (inherited)
844 "text-decoration" => {
845 style.text_decoration = match value {
846 CssValue::Keyword(k) => match k.as_str() {
847 "underline" => TextDecoration::Underline,
848 "overline" => TextDecoration::Overline,
849 "line-through" => TextDecoration::LineThrough,
850 _ => style.text_decoration,
851 },
852 CssValue::None => TextDecoration::None,
853 _ => style.text_decoration,
854 };
855 }
856
857 // Line-height (inherited)
858 "line-height" => match value {
859 CssValue::Keyword(k) if k == "normal" => {
860 style.line_height = style.font_size * 1.2;
861 }
862 CssValue::Number(n) => {
863 style.line_height = *n as f32 * style.font_size;
864 }
865 CssValue::Length(n, unit) => {
866 style.line_height = resolve_length_unit(*n, *unit, style.font_size, viewport);
867 }
868 CssValue::Percentage(p) => {
869 style.line_height = (*p / 100.0) as f32 * style.font_size;
870 }
871 _ => {}
872 },
873
874 // Background color
875 "background-color" => {
876 if let Some(c) = resolve_color(value, style.color) {
877 style.background_color = c;
878 }
879 }
880
881 // Position
882 "position" => {
883 style.position = match value {
884 CssValue::Keyword(k) => match k.as_str() {
885 "static" => Position::Static,
886 "relative" => Position::Relative,
887 "absolute" => Position::Absolute,
888 "fixed" => Position::Fixed,
889 "sticky" => Position::Sticky,
890 _ => style.position,
891 },
892 _ => style.position,
893 };
894 }
895
896 // Position offsets (percentages preserved for layout resolution)
897 "top" => style.top = resolve_layout_length_or_auto(value, current_fs, viewport),
898 "right" => style.right = resolve_layout_length_or_auto(value, current_fs, viewport),
899 "bottom" => style.bottom = resolve_layout_length_or_auto(value, current_fs, viewport),
900 "left" => style.left = resolve_layout_length_or_auto(value, current_fs, viewport),
901
902 // z-index
903 "z-index" => {
904 style.z_index = match value {
905 CssValue::Number(n) => Some(*n as i32),
906 CssValue::Keyword(k) if k == "auto" => None,
907 _ => style.z_index,
908 };
909 }
910
911 // Float
912 "float" => {
913 style.float = match value {
914 CssValue::Keyword(k) => match k.as_str() {
915 "none" => Float::None,
916 "left" => Float::Left,
917 "right" => Float::Right,
918 _ => style.float,
919 },
920 _ => style.float,
921 };
922 }
923
924 // Clear
925 "clear" => {
926 style.clear = match value {
927 CssValue::Keyword(k) => match k.as_str() {
928 "none" => Clear::None,
929 "left" => Clear::Left,
930 "right" => Clear::Right,
931 "both" => Clear::Both,
932 _ => style.clear,
933 },
934 _ => style.clear,
935 };
936 }
937
938 // Overflow
939 "overflow" => {
940 style.overflow = match value {
941 CssValue::Keyword(k) => match k.as_str() {
942 "visible" => Overflow::Visible,
943 "hidden" => Overflow::Hidden,
944 "scroll" => Overflow::Scroll,
945 _ => style.overflow,
946 },
947 CssValue::Auto => Overflow::Auto,
948 _ => style.overflow,
949 };
950 }
951
952 // Visibility (inherited)
953 "visibility" => {
954 style.visibility = match value {
955 CssValue::Keyword(k) => match k.as_str() {
956 "visible" => Visibility::Visible,
957 "hidden" => Visibility::Hidden,
958 "collapse" => Visibility::Collapse,
959 _ => style.visibility,
960 },
961 _ => style.visibility,
962 };
963 }
964
965 // Flex container properties
966 "flex-direction" => {
967 style.flex_direction = match value {
968 CssValue::Keyword(k) => match k.as_str() {
969 "row" => FlexDirection::Row,
970 "row-reverse" => FlexDirection::RowReverse,
971 "column" => FlexDirection::Column,
972 "column-reverse" => FlexDirection::ColumnReverse,
973 _ => style.flex_direction,
974 },
975 _ => style.flex_direction,
976 };
977 }
978 "flex-wrap" => {
979 style.flex_wrap = match value {
980 CssValue::Keyword(k) => match k.as_str() {
981 "nowrap" => FlexWrap::Nowrap,
982 "wrap" => FlexWrap::Wrap,
983 "wrap-reverse" => FlexWrap::WrapReverse,
984 _ => style.flex_wrap,
985 },
986 _ => style.flex_wrap,
987 };
988 }
989 "justify-content" => {
990 style.justify_content = match value {
991 CssValue::Keyword(k) => match k.as_str() {
992 "flex-start" => JustifyContent::FlexStart,
993 "flex-end" => JustifyContent::FlexEnd,
994 "center" => JustifyContent::Center,
995 "space-between" => JustifyContent::SpaceBetween,
996 "space-around" => JustifyContent::SpaceAround,
997 "space-evenly" => JustifyContent::SpaceEvenly,
998 _ => style.justify_content,
999 },
1000 _ => style.justify_content,
1001 };
1002 }
1003 "align-items" => {
1004 style.align_items = match value {
1005 CssValue::Keyword(k) => match k.as_str() {
1006 "stretch" => AlignItems::Stretch,
1007 "flex-start" => AlignItems::FlexStart,
1008 "flex-end" => AlignItems::FlexEnd,
1009 "center" => AlignItems::Center,
1010 "baseline" => AlignItems::Baseline,
1011 _ => style.align_items,
1012 },
1013 _ => style.align_items,
1014 };
1015 }
1016 "align-content" => {
1017 style.align_content = match value {
1018 CssValue::Keyword(k) => match k.as_str() {
1019 "stretch" => AlignContent::Stretch,
1020 "flex-start" => AlignContent::FlexStart,
1021 "flex-end" => AlignContent::FlexEnd,
1022 "center" => AlignContent::Center,
1023 "space-between" => AlignContent::SpaceBetween,
1024 "space-around" => AlignContent::SpaceAround,
1025 _ => style.align_content,
1026 },
1027 _ => style.align_content,
1028 };
1029 }
1030 "row-gap" => {
1031 if let CssValue::Length(n, unit) = value {
1032 style.row_gap = resolve_length_unit(*n, *unit, current_fs, viewport);
1033 } else if let CssValue::Zero = value {
1034 style.row_gap = 0.0;
1035 }
1036 }
1037 "column-gap" => {
1038 if let CssValue::Length(n, unit) = value {
1039 style.column_gap = resolve_length_unit(*n, *unit, current_fs, viewport);
1040 } else if let CssValue::Zero = value {
1041 style.column_gap = 0.0;
1042 }
1043 }
1044
1045 // Flex item properties
1046 "flex-grow" => {
1047 if let CssValue::Number(n) = value {
1048 style.flex_grow = *n as f32;
1049 } else if let CssValue::Zero = value {
1050 style.flex_grow = 0.0;
1051 }
1052 }
1053 "flex-shrink" => {
1054 if let CssValue::Number(n) = value {
1055 style.flex_shrink = *n as f32;
1056 } else if let CssValue::Zero = value {
1057 style.flex_shrink = 0.0;
1058 }
1059 }
1060 "flex-basis" => {
1061 style.flex_basis = resolve_layout_length_or_auto(value, current_fs, viewport);
1062 }
1063 "align-self" => {
1064 style.align_self = match value {
1065 CssValue::Keyword(k) => match k.as_str() {
1066 "flex-start" => AlignSelf::FlexStart,
1067 "flex-end" => AlignSelf::FlexEnd,
1068 "center" => AlignSelf::Center,
1069 "baseline" => AlignSelf::Baseline,
1070 "stretch" => AlignSelf::Stretch,
1071 _ => style.align_self,
1072 },
1073 CssValue::Auto => AlignSelf::Auto,
1074 _ => style.align_self,
1075 };
1076 }
1077 "order" => {
1078 if let CssValue::Number(n) = value {
1079 style.order = *n as i32;
1080 } else if let CssValue::Zero = value {
1081 style.order = 0;
1082 }
1083 }
1084
1085 _ => {} // Unknown property — ignore
1086 }
1087}
1088
1089fn resolve_border_width(value: &CssValue, em_base: f32, viewport: (f32, f32)) -> f32 {
1090 match value {
1091 CssValue::Length(n, unit) => resolve_length_unit(*n, *unit, em_base, viewport),
1092 CssValue::Zero => 0.0,
1093 CssValue::Number(n) if *n == 0.0 => 0.0,
1094 CssValue::Keyword(k) => match k.as_str() {
1095 "thin" => 1.0,
1096 "medium" => 3.0,
1097 "thick" => 5.0,
1098 _ => 0.0,
1099 },
1100 _ => 0.0,
1101 }
1102}
1103
1104fn parse_border_style(value: &CssValue) -> BorderStyle {
1105 match value {
1106 CssValue::Keyword(k) => match k.as_str() {
1107 "none" => BorderStyle::None,
1108 "hidden" => BorderStyle::Hidden,
1109 "dotted" => BorderStyle::Dotted,
1110 "dashed" => BorderStyle::Dashed,
1111 "solid" => BorderStyle::Solid,
1112 "double" => BorderStyle::Double,
1113 "groove" => BorderStyle::Groove,
1114 "ridge" => BorderStyle::Ridge,
1115 "inset" => BorderStyle::Inset,
1116 "outset" => BorderStyle::Outset,
1117 _ => BorderStyle::None,
1118 },
1119 CssValue::None => BorderStyle::None,
1120 _ => BorderStyle::None,
1121 }
1122}
1123
1124fn inherit_property(style: &mut ComputedStyle, property: &str, parent: &ComputedStyle) {
1125 match property {
1126 "color" => style.color = parent.color,
1127 "font-size" => {
1128 style.font_size = parent.font_size;
1129 style.line_height = style.font_size * 1.2;
1130 }
1131 "font-weight" => style.font_weight = parent.font_weight,
1132 "font-style" => style.font_style = parent.font_style,
1133 "font-family" => style.font_family = parent.font_family.clone(),
1134 "text-align" => style.text_align = parent.text_align,
1135 "text-decoration" => style.text_decoration = parent.text_decoration,
1136 "line-height" => style.line_height = parent.line_height,
1137 "visibility" => style.visibility = parent.visibility,
1138 // Non-inherited properties: inherit from parent if explicitly requested
1139 "display" => style.display = parent.display,
1140 "margin-top" => style.margin_top = parent.margin_top,
1141 "margin-right" => style.margin_right = parent.margin_right,
1142 "margin-bottom" => style.margin_bottom = parent.margin_bottom,
1143 "margin-left" => style.margin_left = parent.margin_left,
1144 "padding-top" => style.padding_top = parent.padding_top,
1145 "padding-right" => style.padding_right = parent.padding_right,
1146 "padding-bottom" => style.padding_bottom = parent.padding_bottom,
1147 "padding-left" => style.padding_left = parent.padding_left,
1148 "width" => style.width = parent.width,
1149 "height" => style.height = parent.height,
1150 "box-sizing" => style.box_sizing = parent.box_sizing,
1151 "background-color" => style.background_color = parent.background_color,
1152 "position" => style.position = parent.position,
1153 "float" => style.float = parent.float,
1154 "clear" => style.clear = parent.clear,
1155 "overflow" => style.overflow = parent.overflow,
1156 "flex-direction" => style.flex_direction = parent.flex_direction,
1157 "flex-wrap" => style.flex_wrap = parent.flex_wrap,
1158 "justify-content" => style.justify_content = parent.justify_content,
1159 "align-items" => style.align_items = parent.align_items,
1160 "align-content" => style.align_content = parent.align_content,
1161 "row-gap" => style.row_gap = parent.row_gap,
1162 "column-gap" => style.column_gap = parent.column_gap,
1163 "flex-grow" => style.flex_grow = parent.flex_grow,
1164 "flex-shrink" => style.flex_shrink = parent.flex_shrink,
1165 "flex-basis" => style.flex_basis = parent.flex_basis,
1166 "align-self" => style.align_self = parent.align_self,
1167 "order" => style.order = parent.order,
1168 _ => {}
1169 }
1170}
1171
1172fn reset_property_to_initial(style: &mut ComputedStyle, property: &str) {
1173 let initial = ComputedStyle::default();
1174 match property {
1175 "display" => style.display = initial.display,
1176 "margin-top" => style.margin_top = initial.margin_top,
1177 "margin-right" => style.margin_right = initial.margin_right,
1178 "margin-bottom" => style.margin_bottom = initial.margin_bottom,
1179 "margin-left" => style.margin_left = initial.margin_left,
1180 "padding-top" => style.padding_top = initial.padding_top,
1181 "padding-right" => style.padding_right = initial.padding_right,
1182 "padding-bottom" => style.padding_bottom = initial.padding_bottom,
1183 "padding-left" => style.padding_left = initial.padding_left,
1184 "border-top-width" => style.border_top_width = initial.border_top_width,
1185 "border-right-width" => style.border_right_width = initial.border_right_width,
1186 "border-bottom-width" => style.border_bottom_width = initial.border_bottom_width,
1187 "border-left-width" => style.border_left_width = initial.border_left_width,
1188 "width" => style.width = initial.width,
1189 "height" => style.height = initial.height,
1190 "box-sizing" => style.box_sizing = initial.box_sizing,
1191 "color" => style.color = initial.color,
1192 "font-size" => {
1193 style.font_size = initial.font_size;
1194 style.line_height = initial.line_height;
1195 }
1196 "font-weight" => style.font_weight = initial.font_weight,
1197 "font-style" => style.font_style = initial.font_style,
1198 "font-family" => style.font_family = initial.font_family.clone(),
1199 "text-align" => style.text_align = initial.text_align,
1200 "text-decoration" => style.text_decoration = initial.text_decoration,
1201 "line-height" => style.line_height = initial.line_height,
1202 "background-color" => style.background_color = initial.background_color,
1203 "position" => style.position = initial.position,
1204 "top" => style.top = initial.top,
1205 "right" => style.right = initial.right,
1206 "bottom" => style.bottom = initial.bottom,
1207 "left" => style.left = initial.left,
1208 "float" => style.float = initial.float,
1209 "clear" => style.clear = initial.clear,
1210 "overflow" => style.overflow = initial.overflow,
1211 "visibility" => style.visibility = initial.visibility,
1212 "flex-direction" => style.flex_direction = initial.flex_direction,
1213 "flex-wrap" => style.flex_wrap = initial.flex_wrap,
1214 "justify-content" => style.justify_content = initial.justify_content,
1215 "align-items" => style.align_items = initial.align_items,
1216 "align-content" => style.align_content = initial.align_content,
1217 "row-gap" => style.row_gap = initial.row_gap,
1218 "column-gap" => style.column_gap = initial.column_gap,
1219 "flex-grow" => style.flex_grow = initial.flex_grow,
1220 "flex-shrink" => style.flex_shrink = initial.flex_shrink,
1221 "flex-basis" => style.flex_basis = initial.flex_basis,
1222 "align-self" => style.align_self = initial.align_self,
1223 "order" => style.order = initial.order,
1224 _ => {}
1225 }
1226}
1227
1228// ---------------------------------------------------------------------------
1229// Styled tree
1230// ---------------------------------------------------------------------------
1231
1232/// A node in the styled tree: a DOM node paired with its computed style.
1233#[derive(Debug)]
1234pub struct StyledNode {
1235 pub node: NodeId,
1236 pub style: ComputedStyle,
1237 pub children: Vec<StyledNode>,
1238}
1239
1240/// Extract CSS stylesheets from `<style>` elements in the document.
1241///
1242/// Walks the DOM tree, finds all `<style>` elements, extracts their text
1243/// content, and parses each as a CSS stylesheet. Returns stylesheets in
1244/// document order.
1245pub fn extract_stylesheets(doc: &Document) -> Vec<Stylesheet> {
1246 let mut stylesheets = Vec::new();
1247 collect_style_elements(doc, doc.root(), &mut stylesheets);
1248 stylesheets
1249}
1250
1251fn collect_style_elements(doc: &Document, node: NodeId, stylesheets: &mut Vec<Stylesheet>) {
1252 match doc.node_data(node) {
1253 NodeData::Element { tag_name, .. } if tag_name == "style" => {
1254 let mut css_text = String::new();
1255 for child in doc.children(node) {
1256 if let NodeData::Text { data } = doc.node_data(child) {
1257 css_text.push_str(data);
1258 }
1259 }
1260 if !css_text.is_empty() {
1261 stylesheets.push(we_css::parser::Parser::parse(&css_text));
1262 }
1263 }
1264 _ => {
1265 for child in doc.children(node) {
1266 collect_style_elements(doc, child, stylesheets);
1267 }
1268 }
1269 }
1270}
1271
1272/// Resolve styles for an entire document tree.
1273///
1274/// `stylesheets` is a list of author stylesheets (the UA stylesheet is
1275/// automatically prepended).
1276pub fn resolve_styles(
1277 doc: &Document,
1278 author_stylesheets: &[Stylesheet],
1279 viewport: (f32, f32),
1280) -> Option<StyledNode> {
1281 let ua = ua_stylesheet();
1282
1283 // Combine UA + author stylesheets into a single list for rule collection.
1284 // UA rules come first (lower priority), author rules come after.
1285 let mut combined = Stylesheet {
1286 rules: ua.rules.clone(),
1287 };
1288 for ss in author_stylesheets {
1289 combined.rules.extend(ss.rules.iter().cloned());
1290 }
1291
1292 let root = doc.root();
1293 resolve_node(doc, root, &combined, &ComputedStyle::default(), viewport)
1294}
1295
1296fn resolve_node(
1297 doc: &Document,
1298 node: NodeId,
1299 stylesheet: &Stylesheet,
1300 parent_style: &ComputedStyle,
1301 viewport: (f32, f32),
1302) -> Option<StyledNode> {
1303 match doc.node_data(node) {
1304 NodeData::Document => {
1305 // Document node: resolve children, return first element child or wrapper.
1306 let mut children = Vec::new();
1307 for child in doc.children(node) {
1308 if let Some(styled) = resolve_node(doc, child, stylesheet, parent_style, viewport) {
1309 children.push(styled);
1310 }
1311 }
1312 if children.len() == 1 {
1313 children.into_iter().next()
1314 } else if children.is_empty() {
1315 None
1316 } else {
1317 Some(StyledNode {
1318 node,
1319 style: parent_style.clone(),
1320 children,
1321 })
1322 }
1323 }
1324 NodeData::Element { .. } => {
1325 let style = compute_style_for_element(doc, node, stylesheet, parent_style, viewport);
1326
1327 if style.display == Display::None {
1328 return None;
1329 }
1330
1331 let mut children = Vec::new();
1332 for child in doc.children(node) {
1333 if let Some(styled) = resolve_node(doc, child, stylesheet, &style, viewport) {
1334 children.push(styled);
1335 }
1336 }
1337
1338 Some(StyledNode {
1339 node,
1340 style,
1341 children,
1342 })
1343 }
1344 NodeData::Text { data } => {
1345 if data.trim().is_empty() {
1346 return None;
1347 }
1348 // Text nodes inherit all properties from their parent.
1349 Some(StyledNode {
1350 node,
1351 style: parent_style.clone(),
1352 children: Vec::new(),
1353 })
1354 }
1355 NodeData::Comment { .. } => None,
1356 }
1357}
1358
1359/// Compute the style for a single element node.
1360fn compute_style_for_element(
1361 doc: &Document,
1362 node: NodeId,
1363 stylesheet: &Stylesheet,
1364 parent_style: &ComputedStyle,
1365 viewport: (f32, f32),
1366) -> ComputedStyle {
1367 // Start from initial values, inheriting inherited properties from parent
1368 let mut style = ComputedStyle {
1369 color: parent_style.color,
1370 font_size: parent_style.font_size,
1371 font_weight: parent_style.font_weight,
1372 font_style: parent_style.font_style,
1373 font_family: parent_style.font_family.clone(),
1374 text_align: parent_style.text_align,
1375 text_decoration: parent_style.text_decoration,
1376 line_height: parent_style.line_height,
1377 visibility: parent_style.visibility,
1378 ..ComputedStyle::default()
1379 };
1380
1381 // Step 2: Collect matching rules, sorted by specificity + source order
1382 let matched_rules = collect_matching_rules(doc, node, stylesheet);
1383
1384 // Step 3: Separate normal and !important declarations
1385 let mut normal_decls: Vec<(String, CssValue)> = Vec::new();
1386 let mut important_decls: Vec<(String, CssValue)> = Vec::new();
1387
1388 for matched in &matched_rules {
1389 for decl in &matched.rule.declarations {
1390 let property = &decl.property;
1391
1392 // Try shorthand expansion first
1393 if let Some(longhands) = expand_shorthand(property, &decl.value, decl.important) {
1394 for lh in longhands {
1395 if lh.important {
1396 important_decls.push((lh.property, lh.value));
1397 } else {
1398 normal_decls.push((lh.property, lh.value));
1399 }
1400 }
1401 } else {
1402 // Regular longhand property
1403 let value = parse_value(&decl.value);
1404 if decl.important {
1405 important_decls.push((property.clone(), value));
1406 } else {
1407 normal_decls.push((property.clone(), value));
1408 }
1409 }
1410 }
1411 }
1412
1413 // Step 4: Apply inline style declarations (from style attribute).
1414 // Inline styles have specificity (1,0,0,0) — higher than any selector.
1415 // We apply them after stylesheet rules so they override.
1416 let inline_decls = parse_inline_style(doc, node);
1417
1418 // Step 5: Apply normal declarations (already in specificity order)
1419 for (prop, value) in &normal_decls {
1420 apply_property(&mut style, prop, value, parent_style, viewport);
1421 }
1422
1423 // Step 6: Apply inline style normal declarations (override stylesheet normals)
1424 for decl in &inline_decls {
1425 if !decl.important {
1426 let property = decl.property.as_str();
1427 if let Some(longhands) = expand_shorthand(property, &decl.value, false) {
1428 for lh in &longhands {
1429 apply_property(&mut style, &lh.property, &lh.value, parent_style, viewport);
1430 }
1431 } else {
1432 let value = parse_value(&decl.value);
1433 apply_property(&mut style, property, &value, parent_style, viewport);
1434 }
1435 }
1436 }
1437
1438 // Step 7: Apply !important declarations (override everything normal)
1439 for (prop, value) in &important_decls {
1440 apply_property(&mut style, prop, value, parent_style, viewport);
1441 }
1442
1443 // Step 8: Apply inline style !important declarations (highest priority)
1444 for decl in &inline_decls {
1445 if decl.important {
1446 let property = decl.property.as_str();
1447 if let Some(longhands) = expand_shorthand(property, &decl.value, true) {
1448 for lh in &longhands {
1449 apply_property(&mut style, &lh.property, &lh.value, parent_style, viewport);
1450 }
1451 } else {
1452 let value = parse_value(&decl.value);
1453 apply_property(&mut style, property, &value, parent_style, viewport);
1454 }
1455 }
1456 }
1457
1458 style
1459}
1460
1461/// Parse inline style from the `style` attribute of an element.
1462fn parse_inline_style(doc: &Document, node: NodeId) -> Vec<Declaration> {
1463 if let Some(style_attr) = doc.get_attribute(node, "style") {
1464 // Wrap in a dummy rule so the CSS parser can parse it
1465 let css = format!("x {{ {style_attr} }}");
1466 let ss = we_css::parser::Parser::parse(&css);
1467 if let Some(we_css::parser::Rule::Style(rule)) = ss.rules.into_iter().next() {
1468 rule.declarations
1469 } else {
1470 Vec::new()
1471 }
1472 } else {
1473 Vec::new()
1474 }
1475}
1476
1477// ---------------------------------------------------------------------------
1478// Tests
1479// ---------------------------------------------------------------------------
1480
1481#[cfg(test)]
1482mod tests {
1483 use super::*;
1484 use we_css::parser::Parser;
1485
1486 fn make_doc_with_body() -> (Document, NodeId, NodeId, NodeId) {
1487 let mut doc = Document::new();
1488 let root = doc.root();
1489 let html = doc.create_element("html");
1490 let body = doc.create_element("body");
1491 doc.append_child(root, html);
1492 doc.append_child(html, body);
1493 (doc, root, html, body)
1494 }
1495
1496 // -----------------------------------------------------------------------
1497 // UA defaults
1498 // -----------------------------------------------------------------------
1499
1500 #[test]
1501 fn ua_body_has_8px_margin() {
1502 let (doc, _, _, _) = make_doc_with_body();
1503 let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap();
1504 // styled is <html>, first child is <body>
1505 let body = &styled.children[0];
1506 assert_eq!(body.style.margin_top, LengthOrAuto::Length(8.0));
1507 assert_eq!(body.style.margin_right, LengthOrAuto::Length(8.0));
1508 assert_eq!(body.style.margin_bottom, LengthOrAuto::Length(8.0));
1509 assert_eq!(body.style.margin_left, LengthOrAuto::Length(8.0));
1510 }
1511
1512 #[test]
1513 fn ua_h1_font_size() {
1514 let (mut doc, _, _, body) = make_doc_with_body();
1515 let h1 = doc.create_element("h1");
1516 let text = doc.create_text("Title");
1517 doc.append_child(body, h1);
1518 doc.append_child(h1, text);
1519
1520 let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap();
1521 let body_node = &styled.children[0];
1522 let h1_node = &body_node.children[0];
1523
1524 // h1 = 2em of parent (16px) = 32px
1525 assert_eq!(h1_node.style.font_size, 32.0);
1526 assert_eq!(h1_node.style.font_weight, FontWeight(700.0));
1527 }
1528
1529 #[test]
1530 fn ua_h2_font_size() {
1531 let (mut doc, _, _, body) = make_doc_with_body();
1532 let h2 = doc.create_element("h2");
1533 doc.append_child(body, h2);
1534
1535 let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap();
1536 let body_node = &styled.children[0];
1537 let h2_node = &body_node.children[0];
1538
1539 // h2 = 1.5em = 24px
1540 assert_eq!(h2_node.style.font_size, 24.0);
1541 }
1542
1543 #[test]
1544 fn ua_p_has_1em_margins() {
1545 let (mut doc, _, _, body) = make_doc_with_body();
1546 let p = doc.create_element("p");
1547 let text = doc.create_text("text");
1548 doc.append_child(body, p);
1549 doc.append_child(p, text);
1550
1551 let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap();
1552 let body_node = &styled.children[0];
1553 let p_node = &body_node.children[0];
1554
1555 // p margin-top/bottom = 1em = 16px
1556 assert_eq!(p_node.style.margin_top, LengthOrAuto::Length(16.0));
1557 assert_eq!(p_node.style.margin_bottom, LengthOrAuto::Length(16.0));
1558 }
1559
1560 #[test]
1561 fn ua_display_block_elements() {
1562 let (mut doc, _, _, body) = make_doc_with_body();
1563 let div = doc.create_element("div");
1564 doc.append_child(body, div);
1565
1566 let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap();
1567 let body_node = &styled.children[0];
1568 let div_node = &body_node.children[0];
1569 assert_eq!(div_node.style.display, Display::Block);
1570 }
1571
1572 #[test]
1573 fn ua_display_inline_elements() {
1574 let (mut doc, _, _, body) = make_doc_with_body();
1575 let p = doc.create_element("p");
1576 let span = doc.create_element("span");
1577 let text = doc.create_text("x");
1578 doc.append_child(body, p);
1579 doc.append_child(p, span);
1580 doc.append_child(span, text);
1581
1582 let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap();
1583 let body_node = &styled.children[0];
1584 let p_node = &body_node.children[0];
1585 let span_node = &p_node.children[0];
1586 assert_eq!(span_node.style.display, Display::Inline);
1587 }
1588
1589 #[test]
1590 fn ua_display_none_for_head() {
1591 let (mut doc, _, html, _) = make_doc_with_body();
1592 let head = doc.create_element("head");
1593 let title = doc.create_element("title");
1594 doc.append_child(html, head);
1595 doc.append_child(head, title);
1596
1597 let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap();
1598 // head should not appear in styled tree (display: none)
1599 for child in &styled.children {
1600 if let NodeData::Element { tag_name, .. } = doc.node_data(child.node) {
1601 assert_ne!(tag_name.as_str(), "head");
1602 }
1603 }
1604 }
1605
1606 // -----------------------------------------------------------------------
1607 // Author stylesheets
1608 // -----------------------------------------------------------------------
1609
1610 #[test]
1611 fn author_color_override() {
1612 let (mut doc, _, _, body) = make_doc_with_body();
1613 let p = doc.create_element("p");
1614 let text = doc.create_text("hello");
1615 doc.append_child(body, p);
1616 doc.append_child(p, text);
1617
1618 let ss = Parser::parse("p { color: red; }");
1619 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1620 let body_node = &styled.children[0];
1621 let p_node = &body_node.children[0];
1622 assert_eq!(p_node.style.color, Color::rgb(255, 0, 0));
1623 }
1624
1625 #[test]
1626 fn author_background_color() {
1627 let (mut doc, _, _, body) = make_doc_with_body();
1628 let div = doc.create_element("div");
1629 doc.append_child(body, div);
1630
1631 let ss = Parser::parse("div { background-color: blue; }");
1632 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1633 let body_node = &styled.children[0];
1634 let div_node = &body_node.children[0];
1635 assert_eq!(div_node.style.background_color, Color::rgb(0, 0, 255));
1636 }
1637
1638 #[test]
1639 fn author_font_size_px() {
1640 let (mut doc, _, _, body) = make_doc_with_body();
1641 let p = doc.create_element("p");
1642 doc.append_child(body, p);
1643
1644 let ss = Parser::parse("p { font-size: 24px; }");
1645 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1646 let body_node = &styled.children[0];
1647 let p_node = &body_node.children[0];
1648 assert_eq!(p_node.style.font_size, 24.0);
1649 }
1650
1651 #[test]
1652 fn author_margin_px() {
1653 let (mut doc, _, _, body) = make_doc_with_body();
1654 let div = doc.create_element("div");
1655 doc.append_child(body, div);
1656
1657 let ss = Parser::parse("div { margin-top: 20px; margin-bottom: 10px; }");
1658 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1659 let body_node = &styled.children[0];
1660 let div_node = &body_node.children[0];
1661 assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(20.0));
1662 assert_eq!(div_node.style.margin_bottom, LengthOrAuto::Length(10.0));
1663 }
1664
1665 // -----------------------------------------------------------------------
1666 // Cascade: specificity ordering
1667 // -----------------------------------------------------------------------
1668
1669 #[test]
1670 fn higher_specificity_wins() {
1671 let (mut doc, _, _, body) = make_doc_with_body();
1672 let p = doc.create_element("p");
1673 doc.set_attribute(p, "class", "highlight");
1674 let text = doc.create_text("x");
1675 doc.append_child(body, p);
1676 doc.append_child(p, text);
1677
1678 // .highlight (0,1,0) > p (0,0,1)
1679 let ss = Parser::parse("p { color: red; } .highlight { color: green; }");
1680 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1681 let body_node = &styled.children[0];
1682 let p_node = &body_node.children[0];
1683 assert_eq!(p_node.style.color, Color::rgb(0, 128, 0));
1684 }
1685
1686 #[test]
1687 fn source_order_tiebreak() {
1688 let (mut doc, _, _, body) = make_doc_with_body();
1689 let p = doc.create_element("p");
1690 let text = doc.create_text("x");
1691 doc.append_child(body, p);
1692 doc.append_child(p, text);
1693
1694 // Same specificity: later wins
1695 let ss = Parser::parse("p { color: red; } p { color: blue; }");
1696 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1697 let body_node = &styled.children[0];
1698 let p_node = &body_node.children[0];
1699 assert_eq!(p_node.style.color, Color::rgb(0, 0, 255));
1700 }
1701
1702 #[test]
1703 fn important_overrides_specificity() {
1704 let (mut doc, _, _, body) = make_doc_with_body();
1705 let p = doc.create_element("p");
1706 doc.set_attribute(p, "id", "main");
1707 let text = doc.create_text("x");
1708 doc.append_child(body, p);
1709 doc.append_child(p, text);
1710
1711 // #main (1,0,0) has higher specificity, but p has !important
1712 let ss = Parser::parse("#main { color: blue; } p { color: red !important; }");
1713 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1714 let body_node = &styled.children[0];
1715 let p_node = &body_node.children[0];
1716 assert_eq!(p_node.style.color, Color::rgb(255, 0, 0));
1717 }
1718
1719 // -----------------------------------------------------------------------
1720 // Inheritance
1721 // -----------------------------------------------------------------------
1722
1723 #[test]
1724 fn color_inherits_to_children() {
1725 let (mut doc, _, _, body) = make_doc_with_body();
1726 let div = doc.create_element("div");
1727 let p = doc.create_element("p");
1728 let text = doc.create_text("x");
1729 doc.append_child(body, div);
1730 doc.append_child(div, p);
1731 doc.append_child(p, text);
1732
1733 let ss = Parser::parse("div { color: green; }");
1734 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1735 let body_node = &styled.children[0];
1736 let div_node = &body_node.children[0];
1737 let p_node = &div_node.children[0];
1738
1739 assert_eq!(div_node.style.color, Color::rgb(0, 128, 0));
1740 // p inherits color from div
1741 assert_eq!(p_node.style.color, Color::rgb(0, 128, 0));
1742 }
1743
1744 #[test]
1745 fn font_size_inherits() {
1746 let (mut doc, _, _, body) = make_doc_with_body();
1747 let div = doc.create_element("div");
1748 let p = doc.create_element("p");
1749 let text = doc.create_text("x");
1750 doc.append_child(body, div);
1751 doc.append_child(div, p);
1752 doc.append_child(p, text);
1753
1754 let ss = Parser::parse("div { font-size: 20px; }");
1755 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1756 let body_node = &styled.children[0];
1757 let div_node = &body_node.children[0];
1758 let p_node = &div_node.children[0];
1759
1760 assert_eq!(div_node.style.font_size, 20.0);
1761 assert_eq!(p_node.style.font_size, 20.0);
1762 }
1763
1764 #[test]
1765 fn margin_does_not_inherit() {
1766 let (mut doc, _, _, body) = make_doc_with_body();
1767 let div = doc.create_element("div");
1768 let span = doc.create_element("span");
1769 let text = doc.create_text("x");
1770 doc.append_child(body, div);
1771 doc.append_child(div, span);
1772 doc.append_child(span, text);
1773
1774 let ss = Parser::parse("div { margin-top: 50px; }");
1775 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1776 let body_node = &styled.children[0];
1777 let div_node = &body_node.children[0];
1778 let span_node = &div_node.children[0];
1779
1780 assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(50.0));
1781 // span should NOT inherit margin
1782 assert_eq!(span_node.style.margin_top, LengthOrAuto::Length(0.0));
1783 }
1784
1785 // -----------------------------------------------------------------------
1786 // inherit / initial / unset keywords
1787 // -----------------------------------------------------------------------
1788
1789 #[test]
1790 fn inherit_keyword_for_non_inherited() {
1791 let (mut doc, _, _, body) = make_doc_with_body();
1792 let div = doc.create_element("div");
1793 let p = doc.create_element("p");
1794 let text = doc.create_text("x");
1795 doc.append_child(body, div);
1796 doc.append_child(div, p);
1797 doc.append_child(p, text);
1798
1799 let ss = Parser::parse("div { background-color: red; } p { background-color: inherit; }");
1800 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1801 let body_node = &styled.children[0];
1802 let div_node = &body_node.children[0];
1803 let p_node = &div_node.children[0];
1804
1805 assert_eq!(div_node.style.background_color, Color::rgb(255, 0, 0));
1806 assert_eq!(p_node.style.background_color, Color::rgb(255, 0, 0));
1807 }
1808
1809 #[test]
1810 fn initial_keyword_resets() {
1811 let (mut doc, _, _, body) = make_doc_with_body();
1812 let div = doc.create_element("div");
1813 let p = doc.create_element("p");
1814 let text = doc.create_text("x");
1815 doc.append_child(body, div);
1816 doc.append_child(div, p);
1817 doc.append_child(p, text);
1818
1819 // div sets color to red, p resets to initial (black)
1820 let ss = Parser::parse("div { color: red; } p { color: initial; }");
1821 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1822 let body_node = &styled.children[0];
1823 let div_node = &body_node.children[0];
1824 let p_node = &div_node.children[0];
1825
1826 assert_eq!(div_node.style.color, Color::rgb(255, 0, 0));
1827 assert_eq!(p_node.style.color, Color::rgb(0, 0, 0)); // initial
1828 }
1829
1830 #[test]
1831 fn unset_inherits_for_inherited_property() {
1832 let (mut doc, _, _, body) = make_doc_with_body();
1833 let div = doc.create_element("div");
1834 let p = doc.create_element("p");
1835 let text = doc.create_text("x");
1836 doc.append_child(body, div);
1837 doc.append_child(div, p);
1838 doc.append_child(p, text);
1839
1840 // color is inherited, so unset => inherit
1841 let ss = Parser::parse("div { color: green; } p { color: unset; }");
1842 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1843 let body_node = &styled.children[0];
1844 let div_node = &body_node.children[0];
1845 let p_node = &div_node.children[0];
1846
1847 assert_eq!(p_node.style.color, div_node.style.color);
1848 }
1849
1850 #[test]
1851 fn unset_resets_for_non_inherited_property() {
1852 let (mut doc, _, _, body) = make_doc_with_body();
1853 let div = doc.create_element("div");
1854 let p = doc.create_element("p");
1855 let text = doc.create_text("x");
1856 doc.append_child(body, div);
1857 doc.append_child(div, p);
1858 doc.append_child(p, text);
1859
1860 // margin is non-inherited, so unset => initial (0)
1861 let ss = Parser::parse("p { margin-top: unset; }");
1862 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1863 let body_node = &styled.children[0];
1864 let p_node = &body_node.children[0];
1865
1866 // UA sets p margin-top to 1em=16px, but unset resets to initial (0)
1867 assert_eq!(p_node.style.margin_top, LengthOrAuto::Length(0.0));
1868 }
1869
1870 // -----------------------------------------------------------------------
1871 // Em unit resolution
1872 // -----------------------------------------------------------------------
1873
1874 #[test]
1875 fn em_margin_relative_to_font_size() {
1876 let (mut doc, _, _, body) = make_doc_with_body();
1877 let div = doc.create_element("div");
1878 let text = doc.create_text("x");
1879 doc.append_child(body, div);
1880 doc.append_child(div, text);
1881
1882 let ss = Parser::parse("div { font-size: 20px; margin-top: 2em; }");
1883 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1884 let body_node = &styled.children[0];
1885 let div_node = &body_node.children[0];
1886
1887 assert_eq!(div_node.style.font_size, 20.0);
1888 // margin-top 2em resolves relative to the element's own computed font-size (20px)
1889 assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(40.0));
1890 }
1891
1892 #[test]
1893 fn em_font_size_relative_to_parent() {
1894 let (mut doc, _, _, body) = make_doc_with_body();
1895 let div = doc.create_element("div");
1896 let p = doc.create_element("p");
1897 let text = doc.create_text("x");
1898 doc.append_child(body, div);
1899 doc.append_child(div, p);
1900 doc.append_child(p, text);
1901
1902 let ss = Parser::parse("div { font-size: 20px; } p { font-size: 1.5em; }");
1903 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1904 let body_node = &styled.children[0];
1905 let div_node = &body_node.children[0];
1906 let p_node = &div_node.children[0];
1907
1908 assert_eq!(div_node.style.font_size, 20.0);
1909 // p font-size = 1.5 * parent(20px) = 30px
1910 assert_eq!(p_node.style.font_size, 30.0);
1911 }
1912
1913 // -----------------------------------------------------------------------
1914 // Inline styles
1915 // -----------------------------------------------------------------------
1916
1917 #[test]
1918 fn inline_style_overrides_stylesheet() {
1919 let (mut doc, _, _, body) = make_doc_with_body();
1920 let p = doc.create_element("p");
1921 doc.set_attribute(p, "style", "color: green;");
1922 let text = doc.create_text("x");
1923 doc.append_child(body, p);
1924 doc.append_child(p, text);
1925
1926 let ss = Parser::parse("p { color: red; }");
1927 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1928 let body_node = &styled.children[0];
1929 let p_node = &body_node.children[0];
1930
1931 // Inline style wins over author stylesheet
1932 assert_eq!(p_node.style.color, Color::rgb(0, 128, 0));
1933 }
1934
1935 #[test]
1936 fn inline_style_important() {
1937 let (mut doc, _, _, body) = make_doc_with_body();
1938 let p = doc.create_element("p");
1939 doc.set_attribute(p, "style", "color: green !important;");
1940 let text = doc.create_text("x");
1941 doc.append_child(body, p);
1942 doc.append_child(p, text);
1943
1944 let ss = Parser::parse("p { color: red !important; }");
1945 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1946 let body_node = &styled.children[0];
1947 let p_node = &body_node.children[0];
1948
1949 // Inline !important beats stylesheet !important
1950 assert_eq!(p_node.style.color, Color::rgb(0, 128, 0));
1951 }
1952
1953 // -----------------------------------------------------------------------
1954 // Shorthand expansion
1955 // -----------------------------------------------------------------------
1956
1957 #[test]
1958 fn margin_shorthand_in_stylesheet() {
1959 let (mut doc, _, _, body) = make_doc_with_body();
1960 let div = doc.create_element("div");
1961 let text = doc.create_text("x");
1962 doc.append_child(body, div);
1963 doc.append_child(div, text);
1964
1965 let ss = Parser::parse("div { margin: 10px 20px; }");
1966 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1967 let body_node = &styled.children[0];
1968 let div_node = &body_node.children[0];
1969
1970 assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(10.0));
1971 assert_eq!(div_node.style.margin_right, LengthOrAuto::Length(20.0));
1972 assert_eq!(div_node.style.margin_bottom, LengthOrAuto::Length(10.0));
1973 assert_eq!(div_node.style.margin_left, LengthOrAuto::Length(20.0));
1974 }
1975
1976 #[test]
1977 fn padding_shorthand() {
1978 let (mut doc, _, _, body) = make_doc_with_body();
1979 let div = doc.create_element("div");
1980 let text = doc.create_text("x");
1981 doc.append_child(body, div);
1982 doc.append_child(div, text);
1983
1984 let ss = Parser::parse("div { padding: 5px; }");
1985 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
1986 let body_node = &styled.children[0];
1987 let div_node = &body_node.children[0];
1988
1989 assert_eq!(div_node.style.padding_top, LengthOrAuto::Length(5.0));
1990 assert_eq!(div_node.style.padding_right, LengthOrAuto::Length(5.0));
1991 assert_eq!(div_node.style.padding_bottom, LengthOrAuto::Length(5.0));
1992 assert_eq!(div_node.style.padding_left, LengthOrAuto::Length(5.0));
1993 }
1994
1995 // -----------------------------------------------------------------------
1996 // UA + author cascade
1997 // -----------------------------------------------------------------------
1998
1999 #[test]
2000 fn author_overrides_ua() {
2001 let (mut doc, _, _, body) = make_doc_with_body();
2002 let p = doc.create_element("p");
2003 let text = doc.create_text("x");
2004 doc.append_child(body, p);
2005 doc.append_child(p, text);
2006
2007 // UA gives p margin-top=1em=16px. Author overrides to 0.
2008 let ss = Parser::parse("p { margin-top: 0; margin-bottom: 0; }");
2009 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
2010 let body_node = &styled.children[0];
2011 let p_node = &body_node.children[0];
2012
2013 assert_eq!(p_node.style.margin_top, LengthOrAuto::Length(0.0));
2014 assert_eq!(p_node.style.margin_bottom, LengthOrAuto::Length(0.0));
2015 }
2016
2017 // -----------------------------------------------------------------------
2018 // Text node inherits parent style
2019 // -----------------------------------------------------------------------
2020
2021 #[test]
2022 fn text_node_inherits_style() {
2023 let (mut doc, _, _, body) = make_doc_with_body();
2024 let p = doc.create_element("p");
2025 let text = doc.create_text("hello");
2026 doc.append_child(body, p);
2027 doc.append_child(p, text);
2028
2029 let ss = Parser::parse("p { color: red; font-size: 20px; }");
2030 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
2031 let body_node = &styled.children[0];
2032 let p_node = &body_node.children[0];
2033 let text_node = &p_node.children[0];
2034
2035 assert_eq!(text_node.style.color, Color::rgb(255, 0, 0));
2036 assert_eq!(text_node.style.font_size, 20.0);
2037 }
2038
2039 // -----------------------------------------------------------------------
2040 // Multiple stylesheets
2041 // -----------------------------------------------------------------------
2042
2043 #[test]
2044 fn multiple_author_stylesheets() {
2045 let (mut doc, _, _, body) = make_doc_with_body();
2046 let p = doc.create_element("p");
2047 let text = doc.create_text("x");
2048 doc.append_child(body, p);
2049 doc.append_child(p, text);
2050
2051 let ss1 = Parser::parse("p { color: red; }");
2052 let ss2 = Parser::parse("p { color: blue; }");
2053 let styled = resolve_styles(&doc, &[ss1, ss2], (800.0, 600.0)).unwrap();
2054 let body_node = &styled.children[0];
2055 let p_node = &body_node.children[0];
2056
2057 // Later stylesheet wins (higher source order)
2058 assert_eq!(p_node.style.color, Color::rgb(0, 0, 255));
2059 }
2060
2061 // -----------------------------------------------------------------------
2062 // Border
2063 // -----------------------------------------------------------------------
2064
2065 #[test]
2066 fn border_shorthand_all_sides() {
2067 let (mut doc, _, _, body) = make_doc_with_body();
2068 let div = doc.create_element("div");
2069 let text = doc.create_text("x");
2070 doc.append_child(body, div);
2071 doc.append_child(div, text);
2072
2073 let ss = Parser::parse("div { border: 2px solid red; }");
2074 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
2075 let body_node = &styled.children[0];
2076 let div_node = &body_node.children[0];
2077
2078 assert_eq!(div_node.style.border_top_width, 2.0);
2079 assert_eq!(div_node.style.border_top_style, BorderStyle::Solid);
2080 assert_eq!(div_node.style.border_top_color, Color::rgb(255, 0, 0));
2081 }
2082
2083 // -----------------------------------------------------------------------
2084 // Position
2085 // -----------------------------------------------------------------------
2086
2087 #[test]
2088 fn position_property() {
2089 let (mut doc, _, _, body) = make_doc_with_body();
2090 let div = doc.create_element("div");
2091 let text = doc.create_text("x");
2092 doc.append_child(body, div);
2093 doc.append_child(div, text);
2094
2095 let ss = Parser::parse("div { position: relative; top: 10px; }");
2096 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
2097 let body_node = &styled.children[0];
2098 let div_node = &body_node.children[0];
2099
2100 assert_eq!(div_node.style.position, Position::Relative);
2101 assert_eq!(div_node.style.top, LengthOrAuto::Length(10.0));
2102 }
2103
2104 // -----------------------------------------------------------------------
2105 // Display: none removes from tree
2106 // -----------------------------------------------------------------------
2107
2108 #[test]
2109 fn display_none_removes_element() {
2110 let (mut doc, _, _, body) = make_doc_with_body();
2111 let div1 = doc.create_element("div");
2112 let t1 = doc.create_text("visible");
2113 doc.append_child(body, div1);
2114 doc.append_child(div1, t1);
2115
2116 let div2 = doc.create_element("div");
2117 doc.set_attribute(div2, "class", "hidden");
2118 let t2 = doc.create_text("hidden");
2119 doc.append_child(body, div2);
2120 doc.append_child(div2, t2);
2121
2122 let ss = Parser::parse(".hidden { display: none; }");
2123 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
2124 let body_node = &styled.children[0];
2125
2126 // Only div1 should appear
2127 assert_eq!(body_node.children.len(), 1);
2128 }
2129
2130 // -----------------------------------------------------------------------
2131 // Visibility
2132 // -----------------------------------------------------------------------
2133
2134 #[test]
2135 fn visibility_inherited() {
2136 let (mut doc, _, _, body) = make_doc_with_body();
2137 let div = doc.create_element("div");
2138 let p = doc.create_element("p");
2139 let text = doc.create_text("x");
2140 doc.append_child(body, div);
2141 doc.append_child(div, p);
2142 doc.append_child(p, text);
2143
2144 let ss = Parser::parse("div { visibility: hidden; }");
2145 let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap();
2146 let body_node = &styled.children[0];
2147 let div_node = &body_node.children[0];
2148 let p_node = &div_node.children[0];
2149
2150 assert_eq!(div_node.style.visibility, Visibility::Hidden);
2151 assert_eq!(p_node.style.visibility, Visibility::Hidden);
2152 }
2153
2154 // -----------------------------------------------------------------------
2155 // extract_stylesheets
2156 // -----------------------------------------------------------------------
2157
2158 #[test]
2159 fn extract_single_style_element() {
2160 let (mut doc, _, html, _) = make_doc_with_body();
2161 let head = doc.create_element("head");
2162 let style = doc.create_element("style");
2163 let css_text = doc.create_text("p { color: red; }");
2164 doc.append_child(html, head);
2165 doc.append_child(head, style);
2166 doc.append_child(style, css_text);
2167
2168 let sheets = extract_stylesheets(&doc);
2169 assert_eq!(sheets.len(), 1);
2170 assert!(!sheets[0].rules.is_empty());
2171 }
2172
2173 #[test]
2174 fn extract_multiple_style_elements() {
2175 let (mut doc, _, html, _) = make_doc_with_body();
2176 let head = doc.create_element("head");
2177 doc.append_child(html, head);
2178
2179 let style1 = doc.create_element("style");
2180 let css1 = doc.create_text("p { color: red; }");
2181 doc.append_child(head, style1);
2182 doc.append_child(style1, css1);
2183
2184 let style2 = doc.create_element("style");
2185 let css2 = doc.create_text("div { margin: 10px; }");
2186 doc.append_child(head, style2);
2187 doc.append_child(style2, css2);
2188
2189 let sheets = extract_stylesheets(&doc);
2190 assert_eq!(sheets.len(), 2);
2191 }
2192
2193 #[test]
2194 fn extract_no_style_elements() {
2195 let (doc, _, _, _) = make_doc_with_body();
2196 let sheets = extract_stylesheets(&doc);
2197 assert!(sheets.is_empty());
2198 }
2199
2200 #[test]
2201 fn extract_empty_style_element() {
2202 let (mut doc, _, html, _) = make_doc_with_body();
2203 let head = doc.create_element("head");
2204 let style = doc.create_element("style");
2205 doc.append_child(html, head);
2206 doc.append_child(head, style);
2207 // No text child — empty style element.
2208
2209 let sheets = extract_stylesheets(&doc);
2210 assert!(sheets.is_empty());
2211 }
2212
2213 #[test]
2214 fn extract_style_in_body() {
2215 let (mut doc, _, _, body) = make_doc_with_body();
2216 // Style elements in body should also be extracted.
2217 let style = doc.create_element("style");
2218 let css_text = doc.create_text("h1 { font-size: 2em; }");
2219 doc.append_child(body, style);
2220 doc.append_child(style, css_text);
2221
2222 let sheets = extract_stylesheets(&doc);
2223 assert_eq!(sheets.len(), 1);
2224 }
2225
2226 #[test]
2227 fn extract_and_resolve_styles_from_dom() {
2228 // Build a DOM with <style> and verify resolved styles.
2229 let (mut doc, _, html, body) = make_doc_with_body();
2230 let head = doc.create_element("head");
2231 let style = doc.create_element("style");
2232 let css_text = doc.create_text("p { color: red; font-size: 24px; }");
2233 doc.append_child(html, head);
2234 doc.append_child(head, style);
2235 doc.append_child(style, css_text);
2236
2237 let p = doc.create_element("p");
2238 let text = doc.create_text("hello");
2239 doc.append_child(body, p);
2240 doc.append_child(p, text);
2241
2242 let sheets = extract_stylesheets(&doc);
2243 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap();
2244 let body_node = &styled.children[0];
2245 let p_node = &body_node.children[0];
2246
2247 assert_eq!(p_node.style.color, Color::rgb(255, 0, 0));
2248 assert_eq!(p_node.style.font_size, 24.0);
2249 }
2250
2251 #[test]
2252 fn style_attribute_works_with_extract() {
2253 // Inline style attributes should work even without <style> elements.
2254 let (mut doc, _, _, body) = make_doc_with_body();
2255 let div = doc.create_element("div");
2256 doc.set_attribute(div, "style", "color: green; margin-top: 20px;");
2257 let text = doc.create_text("styled");
2258 doc.append_child(body, div);
2259 doc.append_child(div, text);
2260
2261 let sheets = extract_stylesheets(&doc);
2262 assert!(sheets.is_empty());
2263
2264 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap();
2265 let body_node = &styled.children[0];
2266 let div_node = &body_node.children[0];
2267
2268 assert_eq!(div_node.style.color, Color::rgb(0, 128, 0));
2269 assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(20.0));
2270 }
2271
2272 #[test]
2273 fn style_element_and_inline_style_combined() {
2274 let (mut doc, _, html, body) = make_doc_with_body();
2275 let head = doc.create_element("head");
2276 let style = doc.create_element("style");
2277 let css_text = doc.create_text("p { color: red; font-size: 20px; }");
2278 doc.append_child(html, head);
2279 doc.append_child(head, style);
2280 doc.append_child(style, css_text);
2281
2282 let p = doc.create_element("p");
2283 // Inline style overrides stylesheet for color.
2284 doc.set_attribute(p, "style", "color: blue;");
2285 let text = doc.create_text("hello");
2286 doc.append_child(body, p);
2287 doc.append_child(p, text);
2288
2289 let sheets = extract_stylesheets(&doc);
2290 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap();
2291 let body_node = &styled.children[0];
2292 let p_node = &body_node.children[0];
2293
2294 // Inline style wins for color.
2295 assert_eq!(p_node.style.color, Color::rgb(0, 0, 255));
2296 // Font-size comes from stylesheet.
2297 assert_eq!(p_node.style.font_size, 20.0);
2298 }
2299
2300 #[test]
2301 fn multiple_style_elements_cascade_correctly() {
2302 let (mut doc, _, html, body) = make_doc_with_body();
2303 let head = doc.create_element("head");
2304 doc.append_child(html, head);
2305
2306 // First stylesheet sets color to red.
2307 let style1 = doc.create_element("style");
2308 let css1 = doc.create_text("p { color: red; }");
2309 doc.append_child(head, style1);
2310 doc.append_child(style1, css1);
2311
2312 // Second stylesheet sets color to blue (later wins in cascade).
2313 let style2 = doc.create_element("style");
2314 let css2 = doc.create_text("p { color: blue; }");
2315 doc.append_child(head, style2);
2316 doc.append_child(style2, css2);
2317
2318 let p = doc.create_element("p");
2319 let text = doc.create_text("hello");
2320 doc.append_child(body, p);
2321 doc.append_child(p, text);
2322
2323 let sheets = extract_stylesheets(&doc);
2324 assert_eq!(sheets.len(), 2);
2325
2326 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap();
2327 let body_node = &styled.children[0];
2328 let p_node = &body_node.children[0];
2329
2330 // Later stylesheet wins.
2331 assert_eq!(p_node.style.color, Color::rgb(0, 0, 255));
2332 }
2333
2334 #[test]
2335 fn box_sizing_content_box_default() {
2336 let html_str = r#"<!DOCTYPE html>
2337<html><head></head><body><div>Test</div></body></html>"#;
2338 let doc = we_html::parse_html(html_str);
2339 let sheets = extract_stylesheets(&doc);
2340 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap();
2341 let body = &styled.children[0];
2342 let div = &body.children[0];
2343 assert_eq!(div.style.box_sizing, BoxSizing::ContentBox);
2344 }
2345
2346 #[test]
2347 fn box_sizing_border_box_parsed() {
2348 let html_str = r#"<!DOCTYPE html>
2349<html><head><style>div { box-sizing: border-box; }</style></head>
2350<body><div>Test</div></body></html>"#;
2351 let doc = we_html::parse_html(html_str);
2352 let sheets = extract_stylesheets(&doc);
2353 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap();
2354 let body = &styled.children[0];
2355 let div = &body.children[0];
2356 assert_eq!(div.style.box_sizing, BoxSizing::BorderBox);
2357 }
2358
2359 #[test]
2360 fn box_sizing_not_inherited() {
2361 let html_str = r#"<!DOCTYPE html>
2362<html><head><style>.parent { box-sizing: border-box; }</style></head>
2363<body><div class="parent"><p>Child</p></div></body></html>"#;
2364 let doc = we_html::parse_html(html_str);
2365 let sheets = extract_stylesheets(&doc);
2366 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap();
2367 let body = &styled.children[0];
2368 let parent = &body.children[0];
2369 let child = &parent.children[0];
2370 assert_eq!(parent.style.box_sizing, BoxSizing::BorderBox);
2371 assert_eq!(child.style.box_sizing, BoxSizing::ContentBox);
2372 }
2373
2374 #[test]
2375 fn z_index_parsing() {
2376 let html_str = r#"<!DOCTYPE html>
2377<html><head><style>
2378.a { z-index: 5; }
2379.b { z-index: -3; }
2380.c { z-index: auto; }
2381</style></head>
2382<body>
2383<div class="a">A</div>
2384<div class="b">B</div>
2385<div class="c">C</div>
2386</body></html>"#;
2387 let doc = we_html::parse_html(html_str);
2388 let sheets = extract_stylesheets(&doc);
2389 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap();
2390 let body = &styled.children[0];
2391 let a = &body.children[0];
2392 let b = &body.children[1];
2393 let c = &body.children[2];
2394 assert_eq!(a.style.z_index, Some(5));
2395 assert_eq!(b.style.z_index, Some(-3));
2396 assert_eq!(c.style.z_index, None);
2397 }
2398
2399 #[test]
2400 fn z_index_default_is_auto() {
2401 let html_str = r#"<!DOCTYPE html>
2402<html><body><div>test</div></body></html>"#;
2403 let doc = we_html::parse_html(html_str);
2404 let sheets = extract_stylesheets(&doc);
2405 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap();
2406 let body = &styled.children[0];
2407 let div = &body.children[0];
2408 assert_eq!(div.style.z_index, None);
2409 }
2410
2411 #[test]
2412 fn position_sticky_parsing() {
2413 let html_str = r#"<!DOCTYPE html>
2414<html><head><style>
2415.s { position: sticky; top: 10px; }
2416</style></head>
2417<body><div class="s">Sticky</div></body></html>"#;
2418 let doc = we_html::parse_html(html_str);
2419 let sheets = extract_stylesheets(&doc);
2420 let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap();
2421 let body = &styled.children[0];
2422 let div = &body.children[0];
2423 assert_eq!(div.style.position, Position::Sticky);
2424 assert_eq!(div.style.top, LengthOrAuto::Length(10.0));
2425 }
2426}