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