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