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