web engine - experimental web browser

Implement CSS cascade and computed style resolution

Adds the `computed` module to the `style` crate with:

- `ComputedStyle` struct with typed fields for all CSS properties:
display, margin, padding, border (width/style/color), width, height,
color, font-size, font-weight, font-style, font-family, text-align,
text-decoration, line-height, background-color, position, top/right/
bottom/left, overflow, visibility

- Cascade algorithm:
1. Collects matching rules via selector matching (from existing matching module)
2. Sorts by origin (UA < author) and specificity
3. Separates normal vs !important declarations
4. Applies inline styles (from style attribute) with highest priority
5. Expands shorthand properties (margin, padding, border, background)

- Property inheritance:
- Inherited properties (color, font-size, font-weight, font-style,
font-family, text-align, text-decoration, line-height, visibility)
automatically inherit from parent computed values
- Non-inherited properties reset to initial values
- `inherit`, `initial`, `unset` keyword support

- User-agent default stylesheet with standard HTML element defaults:
display types, body margin, heading sizes/weights/margins,
paragraph margins, bold/italic/underline for semantic elements

- Relative value resolution:
- `em` units relative to parent font-size (for font-size) or
current font-size (for other properties)
- `rem` units relative to root (16px)
- Physical unit conversion (pt, cm, mm, in, pc)
- Percentage resolution for font-size

- `resolve_styles()` API that builds a styled tree (StyledNode) from
a DOM document and author stylesheets

- 34 comprehensive tests covering UA defaults, author overrides,
cascade ordering, specificity, inheritance, inherit/initial/unset
keywords, em resolution, inline styles, shorthand expansion,
multiple stylesheets, and display:none removal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

authored by pierrelf.com

Claude Opus 4.6 and committed by tangled.org b337e17e b851697e

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