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