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