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