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