///|
/// UA defaults, CSS cascade, and inline style resolution helpers.

///|
/// Check if a compound selector is the :root pseudo-class
fn is_root_selector(selector : @css.CompoundSelector) -> Bool {
  for sub in selector.subclasses {
    match sub {
      @css.SimpleSelector::PseudoClass(@css.PseudoClass::Root) => return true
      _ => ()
    }
  }
  false
}

///|
fn clone_string_map(source : Map[String, String]) -> Map[String, String] {
  // Native copy pre-sizes the target, avoiding the incremental grow/rehash
  // churn of inserting entries one-by-one.
  source.copy()
}

///|
/// True when this element writes into the inherited custom-property map, so the
/// map must be cloned before mutation. When false, the element inherits the
/// parent's vars unchanged and the map can be shared read-only — avoiding an
/// O(vars) clone per element, the dominant style-resolution cost on var-heavy
/// pages. Conservative: any `--` declaration, a `color-scheme` declaration, or
/// a not-yet-seeded color-scheme key forces the clone. Mirrors exactly the
/// write conditions of collect_cascaded_custom_properties /
/// collect_inline_custom_properties / seed_effective_color_scheme_for_direct_style.
fn element_contributes_css_vars(
  cascaded : @css.CascadedValues?,
  inline_css : String?,
  css_vars : Map[String, String],
) -> Bool {
  // Root / first element still needs the color-scheme seed written.
  if css_vars.get(renderer_effective_color_scheme_key) is None {
    return true
  }
  match cascaded {
    Some(cv) => {
      let mut contributes = false
      cv.each(fn(prop, _decl) {
        if prop.has_prefix("--") || prop == "color-scheme" {
          contributes = true
        }
      })
      if contributes {
        return true
      }
    }
    None => ()
  }
  match inline_css {
    Some(css) =>
      for declaration in parse_inline_declarations_cached(css) {
        let (prop, _value) = declaration
        if prop.has_prefix("--") || prop.to_lower() == "color-scheme" {
          return true
        }
      }
    None => ()
  }
  false
}

///|
/// Build the element's effective custom-property map. Shares the inherited map
/// read-only when the element contributes nothing (the common case), else
/// clones and folds in the element's own custom properties + color-scheme seed.
fn compute_element_css_vars(
  cascaded : @css.CascadedValues?,
  inline_css : String?,
  css_vars : Map[String, String],
) -> Map[String, String] {
  if element_contributes_css_vars(cascaded, inline_css, css_vars) {
    let vars = css_vars.copy()
    collect_cascaded_custom_properties(cascaded, vars)
    collect_inline_custom_properties(inline_css, vars)
    seed_effective_color_scheme_for_direct_style(cascaded, inline_css, vars)
    vars
  } else {
    css_vars
  }
}

///|
fn collect_cascaded_custom_properties(
  cascaded : @css.CascadedValues?,
  vars : Map[String, String],
) -> Unit {
  match cascaded {
    Some(cascaded_values) =>
      cascaded_values.each(fn(prop, decl) {
        if prop.has_prefix("--") {
          match decl.value {
            @css.PropertyValue::Value(value) => vars[prop] = value
            _ => ()
          }
        }
      })
    None => ()
  }
}

///|
fn collect_inline_custom_properties(
  inline_css : String?,
  vars : Map[String, String],
) -> Unit {
  match inline_css {
    Some(css) =>
      for declaration in parse_inline_declarations_cached(css) {
        let (prop, value) = declaration
        if prop.has_prefix("--") {
          vars[prop] = value
        }
      }
    None => ()
  }
}

///|
let renderer_effective_color_scheme_key = "--crater-effective-color-scheme"

///|
fn first_color_scheme_keyword_for_renderer(value : String) -> String? {
  let normalized = value.to_lower()
  let tokens = normalized.split(" ")
  for token in tokens {
    let t = token.to_owned().trim()
    if t == "light" || t == "dark" {
      return Some(t.to_owned())
    }
  }
  None
}

///|
fn seed_effective_color_scheme_for_direct_style(
  cascaded : @css.CascadedValues?,
  inline_css : String?,
  vars : Map[String, String],
) -> Unit {
  match vars.get(renderer_effective_color_scheme_key) {
    Some(_) => ()
    None => vars[renderer_effective_color_scheme_key] = "light"
  }
  match cascaded {
    Some(cascaded_values) =>
      match cascaded_values.get_value("color-scheme") {
        Some(value) =>
          match first_color_scheme_keyword_for_renderer(value) {
            Some(scheme) => vars[renderer_effective_color_scheme_key] = scheme
            None => ()
          }
        None => ()
      }
    None => ()
  }
  match inline_css {
    Some(css) =>
      for declaration in parse_inline_declarations_cached(css) {
        let (prop, value) = declaration
        if prop.to_lower() == "color-scheme" {
          match first_color_scheme_keyword_for_renderer(value) {
            Some(scheme) => vars[renderer_effective_color_scheme_key] = scheme
            None => ()
          }
        }
      }
    None => ()
  }
}

///|
/// Check if a complex selector matches :root (simple :root without combinators)
fn is_root_only_selector(selector : @css.ComplexSelector) -> Bool {
  // :root should be a simple selector without combinators
  if selector.tail.length() > 0 {
    return false
  }
  is_root_selector(selector.head)
}

///|
/// Collect CSS custom properties from :root rules in stylesheets
/// Evaluates media queries to correctly apply dark mode variables
fn collect_root_css_variables(
  stylesheets : Array[@css.Stylesheet],
  ctx : RenderContext,
) -> Map[String, String] {
  let vars : Map[String, String] = {}
  // Create media environment for evaluating @media queries
  let media_env = @css.MediaEnvironment::with_color_scheme(
    ctx.viewport_width,
    ctx.viewport_height,
    ctx.color_scheme,
  )
  for stylesheet in stylesheets {
    for rule in stylesheet.rules {
      // Check media query if present
      let media_matches = match rule.media_query {
        Some(mq) => mq.evaluate(media_env)
        None => true
      }
      if !media_matches {
        continue
      }
      // Check if this rule targets :root
      if is_root_only_selector(rule.selector) {
        // Collect custom properties (--*) from declarations
        for decl in rule.declarations {
          if decl.property.has_prefix("--") {
            match decl.value {
              @css.PropertyValue::Value(v) => vars[decl.property] = v
              _ => ()
            }
          }
        }
      }
    }
  }
  vars
}

///|
/// UA default styles depend only on the (lowercased) tag name and every call
/// site treats the result as read-only, so memoize them. Building a fresh
/// `Style` per element — a large struct, plus a spread for the tagged defaults —
/// was pure allocation/drop churn in the render profile; sharing one instance
/// per tag is safe because `Rect` fields are immutable and no caller mutates the
/// returned style.
let ua_default_style_cache : Ref[Map[String, @style.Style]] = { val: {} }

///|
/// Get user-agent default style for an element tag (memoized by tag name).
fn get_ua_default_style(tag : String) -> @style.Style {
  let key = tag.to_lower()
  match ua_default_style_cache.val.get(key) {
    Some(cached) => cached
    None => {
      let computed = compute_ua_default_style(key)
      ua_default_style_cache.val.set(key, computed)
      computed
    }
  }
}

///|
/// Compute the user-agent default style for a (already lowercased) tag.
fn compute_ua_default_style(tag : String) -> @style.Style {
  let default_style = @style.Style::default()

  // Default margins for block elements (based on typical browser defaults)
  // Values are in em units, converted to px assuming 16px base font.
  // `tag` is already lowercased by the memoizing `get_ua_default_style` wrapper.
  match tag {
    "html" => { ..default_style, font_family: "serif" }
    "body" =>
      {
        ..default_style,
        font_family: "serif",
        margin: @types.Rect::all(@types.Length(8.0)),
      }
    "frameset" =>
      {
        ..default_style,
        width: @types.Percent(1.0),
        height: @types.Percent(1.0),
      }
    "p" =>
      {
        // p has 1em margin-top and margin-bottom
        ..default_style,
        margin: {
          left: @types.Length(0.0),
          right: @types.Length(0.0),
          top: @types.Length(16.0),
          bottom: @types.Length(16.0),
        },
      }
    "h1" =>
      {
        // h1 UA defaults: bold 2em heading with block margins.
        ..default_style,
        font_size: 32.0,
        font_weight: 700.0,
        line_height: 38.4,
        margin: {
          left: @types.Length(0.0),
          right: @types.Length(0.0),
          top: @types.Length(21.44),
          bottom: @types.Length(21.44),
        },
      }
    "h2" =>
      {
        // h2 UA defaults: 1.5em bold heading with 0.83em block margins.
        ..default_style,
        font_size: 24.0,
        font_weight: 700.0,
        line_height: 28.8,
        margin: {
          left: @types.Length(0.0),
          right: @types.Length(0.0),
          top: @types.Length(19.92),
          bottom: @types.Length(19.92),
        },
      }
    "h3" =>
      {
        // h3 UA defaults: 1.17em bold heading with 1em block margins.
        ..default_style,
        font_size: 18.72,
        font_weight: 700.0,
        line_height: 22.464,
        margin: {
          left: @types.Length(0.0),
          right: @types.Length(0.0),
          top: @types.Length(18.72),
          bottom: @types.Length(18.72),
        },
      }
    "h4" =>
      {
        // h4 UA defaults: 1em bold heading with 1.33em block margins.
        ..default_style,
        font_weight: 700.0,
        line_height: 19.2,
        margin: {
          left: @types.Length(0.0),
          right: @types.Length(0.0),
          top: @types.Length(21.28),
          bottom: @types.Length(21.28),
        },
      }
    "h5" =>
      {
        // h5 UA defaults: 0.83em bold heading with 1.67em block margins.
        ..default_style,
        font_size: 13.28,
        font_weight: 700.0,
        line_height: 15.936,
        margin: {
          left: @types.Length(0.0),
          right: @types.Length(0.0),
          top: @types.Length(22.1776),
          bottom: @types.Length(22.1776),
        },
      }
    "h6" =>
      {
        // h6 UA defaults: 0.67em bold heading with 2.33em block margins.
        ..default_style,
        font_size: 10.72,
        font_weight: 700.0,
        line_height: 12.864,
        margin: {
          left: @types.Length(0.0),
          right: @types.Length(0.0),
          top: @types.Length(24.9776),
          bottom: @types.Length(24.9776),
        },
      }
    "ul" | "ol" =>
      // Lists: margin-block 1em; padding-inline-start 40px.
      // Use named fields because Rect::new ordering is (left, right, top, bottom).
      {
        ..default_style,
        margin: {
          left: @types.Length(0.0),
          right: @types.Length(0.0),
          top: @types.Length(16.0),
          bottom: @types.Length(16.0),
        },
        padding: {
          left: @types.Length(40.0),
          right: @types.Length(0.0),
          top: @types.Length(0.0),
          bottom: @types.Length(0.0),
        },
      }
    "li" =>
      {
        ..default_style
        // Empty list items still occupy one line box in browsers.
        ,
        min_height: @types.Length(19.2),
      }
    "blockquote" =>
      {
        // blockquote has 1em margin and 40px horizontal margins
        ..default_style,
        margin: {
          left: @types.Length(40.0),
          right: @types.Length(40.0),
          top: @types.Length(16.0),
          bottom: @types.Length(16.0),
        },
      }
    "fieldset" =>
      {
        ..default_style,
        margin: {
          left: @types.Length(2.0),
          right: @types.Length(2.0),
          top: @types.Length(0.0),
          bottom: @types.Length(0.0),
        },
        padding: {
          left: @types.Length(12.0),
          right: @types.Length(12.0),
          top: @types.Length(5.6),
          bottom: @types.Length(10.0),
        },
        border: {
          left: @types.Length(2.0),
          right: @types.Length(2.0),
          top: @types.Length(2.0),
          bottom: @types.Length(2.0),
        },
        min_width: @types.MinContent,
      }
    "legend" =>
      {
        ..default_style
        // Browsers render legend with shrink-wrap-like sizing behavior.
        // InlineBlock approximates that without a dedicated fieldset algorithm.
        ,
        display: @types.InlineBlock,
        padding: {
          left: @types.Length(2.0),
          right: @types.Length(2.0),
          top: @types.Length(0.0),
          bottom: @types.Length(0.0),
        },
      }
    // Pre element - preserve formatting, prevent wrapping
    "pre" =>
      {
        // pre uses smaller monospace text, preserves whitespace, and has 1em
        // margins relative to its own default font size.
        ..default_style,
        display: @types.Block,
        font_family: "monospace",
        font_size: 13.0,
        line_height: 15.6,
        white_space: @style.WhiteSpace::Pre,
        overflow_x: @types.Hidden,
        margin: {
          left: @types.Length(0.0),
          right: @types.Length(0.0),
          top: @types.Length(13.0),
          bottom: @types.Length(13.0),
        },
      }
    // Link element - blue color (standard browser UA style)
    "a" =>
      {
        ..default_style,
        display: @types.Inline,
        // Standard link blue: #0000EE (rgb(0, 0, 238))
        color: @types.Color::rgb(0, 0, 238),
        text_decoration_underline: true,
      }
    // Underline elements
    "u" | "ins" =>
      {
        ..default_style,
        display: @types.Inline,
        text_decoration_underline: true,
      }
    "s" | "strike" | "del" =>
      {
        ..default_style,
        display: @types.Inline,
        text_decoration_line_through: true,
      }
    // Bold inline elements
    "strong" | "b" =>
      { ..default_style, display: @types.Inline, font_weight: 700.0 }
    // Inline elements - HTML default display: inline
    "span"
    | "em"
    | "i"
    | "code"
    | "abbr"
    | "cite"
    | "dfn"
    | "kbd"
    | "samp"
    | "var"
    | "sub"
    | "sup"
    | "small"
    | "mark"
    | "q"
    | "time"
    | "data"
    | "picture"
    | "source"
    | "ruby"
    | "rb"
    | "rbc"
    | "rt"
    | "rtc"
    | "rp"
    | "bdi"
    | "bdo"
    | "wbr" => { ..default_style, display: @types.Inline }
    // Replaced inline elements - inline by default
    // SVG has intrinsic size of 300x150 when no width/height specified
    "img"
    | "video"
    | "audio"
    | "canvas"
    | "iframe"
    | "object"
    | "embed"
    | "svg" => { ..default_style, display: @types.Inline }
    // SVG child elements - inline-block to fit content
    "g"
    | "rect"
    | "circle"
    | "ellipse"
    | "line"
    | "polyline"
    | "polygon"
    | "path"
    | "text"
    | "tspan"
    | "defs"
    | "use"
    | "symbol"
    | "clipPath"
    | "mask"
    | "pattern"
    | "linearGradient"
    | "radialGradient"
    | "stop"
    | "image"
    | "foreignObject" => { ..default_style, display: @types.InlineBlock }
    // Form elements - inline-block and border-box by default
    "input" | "select" | "textarea" =>
      {
        ..default_style,
        display: @types.InlineBlock,
        box_sizing: @types.BorderBox,
        font_size: 13.0,
        line_height: 15.0,
      }
    // Buttons use platform UI metrics in browsers and are typically smaller
    // than inherited body text when no explicit font is set.
    "button" =>
      {
        ..default_style,
        display: @types.InlineBlock,
        box_sizing: @types.BorderBox,
        font_size: 14.0,
        line_height: 16.1,
      }
    "label" => { ..default_style, display: @types.Inline }
    // br is inline with no width
    "br" => { ..default_style, display: @types.Inline }
    // hr: horizontal rule (1px top border + margin)
    "hr" =>
      {
        ..default_style,
        margin: {
          top: @types.Dimension::Length(8.0),
          bottom: @types.Dimension::Length(8.0),
          left: @types.Dimension::Auto,
          right: @types.Dimension::Auto,
        },
        border: {
          top: @types.Dimension::Length(1.0),
          right: @types.Dimension::Length(0.0),
          bottom: @types.Dimension::Length(0.0),
          left: @types.Dimension::Length(0.0),
        },
      }
    // Table elements
    "table" =>
      {
        // Tables default to `box-sizing: content-box` like every other element
        // (there is no UA rule making them border-box). An explicit `width` is
        // therefore the content-box width, and border/padding — including the
        // legacy `` presentational hint — are added outside it.
        ..default_style,
        display: @types.Table,
        border_spacing: 2.0,
      }
    "tr" => { ..default_style, display: @types.TableRow }
    "td" =>
      {
        ..default_style,
        display: @types.TableCell,
        padding: @types.Rect::all(@types.Length(1.0)),
        vertical_align: @style.Middle,
        text_align: @style.TextAlign::Left, // UA default: start (left for LTR)
      }
    "th" =>
      {
        ..default_style,
        display: @types.TableCell,
        padding: @types.Rect::all(@types.Length(1.0)),
        vertical_align: @style.Middle,
        text_align: @style.TextAlign::Center, // th defaults to center
        font_weight: 700.0,
      }
    "thead" => { ..default_style, display: @types.TableHeaderGroup }
    "tbody" => { ..default_style, display: @types.TableRowGroup }
    "tfoot" => { ..default_style, display: @types.TableFooterGroup }
    "caption" => { ..default_style, display: @types.TableCaption }
    "col" => { ..default_style, display: @types.TableColumn }
    "colgroup" => { ..default_style, display: @types.TableColumnGroup }
    // Hidden elements - display: none by default
    "template"
    | "script"
    | "style"
    | "head"
    | "meta"
    | "link"
    | "title"
    | "track"
    | "param" => { ..default_style, display: @types.Display::None }
    // 
is deprecated but widely used (e.g., Hacker News) "center" => { ..default_style, text_align: @style.TextAlign::Center } other => // Custom elements (a hyphen in the tag name, per the HTML spec) and other // unknown hyphenated tags default to display:inline, matching browsers — // e.g. GitHub's , . Unlisted standard elements // (div, section, ...) keep the block default above. if other.contains("-") { { ..default_style, display: @types.Inline } } else { default_style } } } ///| fn uses_table_normal_line_height(tag : String) -> Bool { match tag.to_lower() { "table" | "thead" | "tbody" | "tfoot" | "tr" | "td" | "th" | "caption" => true _ => false } } ///| fn is_display_contents_suppressed_html_element(tag : String) -> Bool { match tag.to_lower() { "br" | "wbr" | "meter" | "progress" | "canvas" | "embed" | "object" | "audio" | "iframe" | "img" | "video" | "input" | "textarea" | "select" => true _ => false } } ///| fn normalize_display_contents_for_unusual_html( tag : String, style : @style.Style, ) -> @style.Style { if style.display == @types.Contents && is_display_contents_suppressed_html_element(tag) { { ..style, display: @types.Display::None } } else { style } } ///| /// Whether a `filter` / `backdrop-filter` value establishes a containing /// block for absolutely positioned descendants. var() is resolved against the /// element's custom properties first. A value that resolves to `none`, to an /// empty string (unresolved var with no fallback), or — for var()-bearing /// declarations — to a token that is not a filter function list computes to /// the initial value `none` and therefore does not establish the CB. fn filter_value_establishes_effect_cb( raw_value : String, css_vars : Map[String, String], ) -> Bool { let had_var = raw_value.contains("var(") let resolved = if had_var { let empty_attributes : Map[String, String] = {} resolve_pseudo_content_value(raw_value, css_vars, empty_attributes) } else { raw_value } let normalized = resolved.to_lower().trim().to_owned() if normalized == "" || normalized == "none" { false } else if had_var { // Only a resolved filter function list (or url() reference) establishes // the CB; a custom property holding a non-filter token computes to none. filter_value_has_function(normalized) } else { // Literal (non-var) values keep the prior permissive behaviour. true } } ///| /// Whether a lowercased value contains a recognised filter function token. fn filter_value_has_function(lowered : String) -> Bool { lowered.contains("blur(") || lowered.contains("brightness(") || lowered.contains("contrast(") || lowered.contains("drop-shadow(") || lowered.contains("grayscale(") || lowered.contains("hue-rotate(") || lowered.contains("invert(") || lowered.contains("opacity(") || lowered.contains("saturate(") || lowered.contains("sepia(") || lowered.contains("url(") } ///| /// Walk up the ancestor chain to the nearest `
` and report whether it /// carries a non-zero HTML `border` presentational attribute. A bare or /// non-integer `border` counts as a (1px) border; `border="0"` does not. fn table_ancestor_has_border(elem : @css.Element) -> Bool { match elem.parent { Some(ancestor) => if ancestor.tag_name.to_lower() == "table" { match ancestor.get_attribute("border") { Some(border_value) => { let n = @string.parse_double(border_value.to_string()) catch { _ => 1.0 } n > 0.0 } None => false } } else { table_ancestor_has_border(ancestor) } None => false } } ///| /// Compute element style using indexed stylesheets for better performance fn compute_element_style_indexed( selector_elem : @css.Element, inline_css : String?, indexed_stylesheets : Array[@css.IndexedStylesheet], is_root : Bool, ctx : RenderContext, parent_style : @style.Style?, css_vars : Map[String, String], ) -> @style.Style { let cascaded = if indexed_stylesheets.length() > 0 { // Create media environment for media query evaluation only when needed. let media_env = @css.MediaEnvironment::with_color_scheme( ctx.viewport_width, ctx.viewport_height, ctx.color_scheme, ) Some( @css.cascade_element_indexed( selector_elem, indexed_stylesheets, [], Some(media_env), ), ) } else { None } apply_cascaded_to_style( cascaded, selector_elem, inline_css, is_root, ctx, parent_style, css_vars, ) } ///| /// Fold a cascaded value set (document or, later, shadow-scoped) together with /// inline styles and inherited context into a concrete `@style.Style`. Split /// out of `compute_element_style_indexed` so a DomTree-driven shadow cascade can /// reuse the exact same post-cascade pipeline (#285). fn apply_cascaded_to_style( cascaded : @css.CascadedValues?, selector_elem : @css.Element, inline_css : String?, is_root : Bool, ctx : RenderContext, parent_style : @style.Style?, css_vars : Map[String, String], ) -> @style.Style { let element_css_vars = compute_element_css_vars( cascaded, inline_css, css_vars, ) let mut author_line_height_present = false match cascaded { Some(cascaded_values) => if cascaded_values.get("line-height") is Some(_) { author_line_height_present = true } None => () } match inline_css { Some(css) => for declaration in parse_inline_declarations_cached(css) { let (prop, _value) = declaration let prop = prop.to_lower() if prop == "line-height" { author_line_height_present = true } } None => () } // Keep this cache conservative: only reuse when stylesheet cascade and // inherited context cannot change the inline-only result. let mut inline_only_cache_css : String? = None let mut inline_only_cache_variant_key : String? = None match (cascaded, inline_css) { (None, Some(css)) => if string_map_is_empty(element_css_vars) && has_default_inherited_inline_context(parent_style) { inline_only_cache_css = Some(css) inline_only_cache_variant_key = Some( make_inline_only_style_cache_variant_key( selector_elem.tag_name, is_root, ctx.viewport_width, ctx.viewport_height, ), ) } _ => () } match (inline_only_cache_css, inline_only_cache_variant_key) { (Some(css), Some(variant_key)) => match @inline_style_cache.inline_only_style_cache_get(css, variant_key) { Some(cached) => return cached None => () } _ => () } // Start with default style let default_style = @style.Style::default() let mut style = { ..default_style // CSS `normal` behaves as flex-start for flex layout. The lower-level // layout default stays logical Start for direct layout tests. , justify_content: @types.FlexStart, } // Inherit properties from parent first match parent_style { Some(ps) => { let lh_ratio = if ps.font_size > 0.0 { ps.line_height / ps.font_size } else { 1.2 } // In-place field assignment on the uniquely-owned mutable `style` // avoids allocating (and later dropping) a whole fresh Style per spread; // per-element style resolution dominates the render, and the spread // churn was a large share of the refcount-drop cost. style.color = ps.color style.font_size = ps.font_size style.font_family = ps.font_family style.line_height = ps.font_size * lh_ratio style.white_space = ps.white_space style.writing_mode = ps.writing_mode style.direction = ps.direction style.pointer_events = ps.pointer_events style.letter_spacing = ps.letter_spacing style.word_spacing = ps.word_spacing } None => () } // Apply user-agent default style let ua_style = get_ua_default_style(selector_elem.tag_name) if ua_style.color != @types.Color::black() { style.color = ua_style.color } style.display = ua_style.display style.box_sizing = ua_style.box_sizing if ua_style.text_align != default_style.text_align { style.text_align = ua_style.text_align } if ua_style.vertical_align != default_style.vertical_align { style.vertical_align = ua_style.vertical_align } if ua_style.white_space != default_style.white_space { style.white_space = ua_style.white_space } if ua_style.overflow_x != default_style.overflow_x || ua_style.overflow_y != default_style.overflow_y { style.overflow_x = ua_style.overflow_x style.overflow_y = ua_style.overflow_y } style.margin = ua_style.margin style.padding = ua_style.padding if ua_style.font_size != default_style.font_size { style.font_size = ua_style.font_size } if ua_style.line_height != default_style.line_height { style.line_height = ua_style.line_height } if ua_style.font_family != "" { style.font_family = ua_style.font_family } if ua_style.font_weight != 400.0 { style.font_weight = ua_style.font_weight } style.border_spacing = ua_style.border_spacing style.border_collapse = ua_style.border_collapse style.text_decoration_underline = ua_style.text_decoration_underline style.text_decoration_line_through = ua_style.text_decoration_line_through style.text_decoration_overline = ua_style.text_decoration_overline style.min_height = ua_style.min_height if selector_elem.tag_name.to_lower() == "rt" { let inherited_font_size = match parent_style { Some(ps) => ps.font_size None => style.font_size } let rt_font_size = inherited_font_size * 0.5 style.font_size = rt_font_size style.line_height = rt_font_size * 1.2 } if uses_table_normal_line_height(selector_elem.tag_name) { style.line_height = style.font_size * normal_line_height_ratio } // Apply presentational HTML attributes (bgcolor, width, align, etc.) // These act as low-priority CSS and are overridden by stylesheets. match selector_elem.get_attribute("bgcolor") { Some(bg_value) => { let parsed = @css.parse_color(bg_value) match parsed.get_color() { Some(color) => style.background_color = color None => () } } None => () } match selector_elem.get_attribute("width") { Some(width_value) => { let tag = selector_elem.tag_name.to_lower() if tag == "table" || tag == "td" || tag == "th" || tag == "col" || tag == "colgroup" { let dim = if width_value.has_suffix("%") { match width_value.strip_suffix("%") { Some(num_str) => { let n = @string.parse_double(num_str.to_owned()) catch { _ => -1.0 } if n >= 0.0 { @types.Dimension::Percent(n / 100.0) } else { @types.Dimension::Auto } } None => @types.Dimension::Auto } } else { let n = @string.parse_double(width_value.to_string()) catch { _ => -1.0 } if n >= 0.0 { @types.Dimension::Length(n) } else { @types.Dimension::Auto } } if dim != @types.Dimension::Auto { style.width = dim } } } None => () } match selector_elem.get_attribute("align") { Some(align_value) => match align_value.to_lower() { "center" => style.text_align = @style.TextAlign::Center "right" => style.text_align = @style.TextAlign::Right "left" => style.text_align = @style.TextAlign::Left _ => () } None => () } // HTML valign attribute → vertical-align CSS match selector_elem.get_attribute("valign") { Some(valign_value) => match valign_value.to_lower() { "top" => style.vertical_align = @style.Top "middle" => style.vertical_align = @style.Middle "bottom" => style.vertical_align = @style.Bottom "baseline" => style.vertical_align = @style.Baseline _ => () } None => () } // HTML border attribute on table → border on all sides if selector_elem.tag_name.to_lower() == "table" { match selector_elem.get_attribute("border") { Some(border_value) => { // HTML presentational hint: a bare (`
`) or non-integer // value maps to a 1px border; an explicit number uses that width, and // `border="0"` parses to 0 and leaves the table without a border. let n = @string.parse_double(border_value.to_string()) catch { _ => 1.0 } if n > 0.0 { let bw = @types.Dimension::Length(n) style.border = { top: bw, right: bw, bottom: bw, left: bw } style.border_style = { top: @style.BorderStyle::Solid, right: @style.BorderStyle::Solid, bottom: @style.BorderStyle::Solid, left: @style.BorderStyle::Solid, } } } None => () } } // A `
` presentational hint also implies a 1px border on every // cell, independent of the table's own border width (Chromium maps the bare // attribute to a thin inset border on each `
`/``). Walk up to the // nearest table ancestor and, if it carries a non-zero `border`, give the // cell the implied 1px border. let cell_tag = selector_elem.tag_name.to_lower() if cell_tag == "td" || cell_tag == "th" { if table_ancestor_has_border(selector_elem) { let bw = @types.Dimension::Length(1.0) style.border = { top: bw, right: bw, bottom: bw, left: bw } style.border_style = { top: @style.BorderStyle::Solid, right: @style.BorderStyle::Solid, bottom: @style.BorderStyle::Solid, left: @style.BorderStyle::Solid, } } } // Apply font metrics first so em/ch/ex units in later properties // (width/height/gap/etc.) resolve against the element's own font. match cascaded { Some(cascaded_values) => { let mut font_present = false let mut font_order = 0 let mut font_value = "" let mut font_size_present = false let mut font_size_order = 0 let mut font_size_value = "" let mut line_height_present = false let mut line_height_order = 0 let mut line_height_value = "" match cascaded_values.get("font") { Some(decl) => match decl.value { @css.PropertyValue::Value(value) => { font_present = true font_order = decl.source_order font_value = value } _ => () } None => () } match cascaded_values.get("font-size") { Some(decl) => match decl.value { @css.PropertyValue::Value(value) => { font_size_present = true font_size_order = decl.source_order font_size_value = value } _ => () } None => () } match cascaded_values.get("line-height") { Some(decl) => match decl.value { @css.PropertyValue::Value(value) => { line_height_present = true line_height_order = decl.source_order line_height_value = value } _ => () } None => () } for idx = 0; idx < 3; idx = idx + 1 { let mut next_prop = "" let mut next_value = "" let mut next_order = 2147483647 if font_present && font_order < next_order { next_prop = "font" next_value = font_value next_order = font_order } if font_size_present && font_size_order < next_order { next_prop = "font-size" next_value = font_size_value next_order = font_size_order } if line_height_present && line_height_order < next_order { next_prop = "line-height" next_value = line_height_value } if next_prop == "" { break } style = apply_css_property_with_viewport( style, next_prop, next_value, ctx.viewport_width, ctx.viewport_height, element_css_vars, parent_style, ) match next_prop { "font" => font_present = false "font-size" => font_size_present = false "line-height" => line_height_present = false _ => () } } // Apply remaining cascaded values. // CascadedValues::each iterates in hash-map order, which does not // preserve CSS source order. Shorthand/longhand interplay (e.g. // `border: solid 10px` in one rule and `border-left-width: 100px` in a // higher-specificity rule) requires applying declarations in source // order, otherwise the shorthand can clobber the longhand. Collect and // sort by source_order before applying, matching @css's own // regular_declarations_in_source_order. let remaining_decls : Array[(String, @css.Declaration)] = [] cascaded_values.each(fn(prop, decl) { if prop == "font" || prop == "font-size" || prop == "line-height" { () } else { remaining_decls.push((prop, decl)) } }) remaining_decls.sort_by(fn(a, b) { a.1.source_order - b.1.source_order }) for entry in remaining_decls { let (prop, decl) = entry match decl.value { @css.PropertyValue::Value(value) => style = apply_css_property_with_viewport( style, prop, value, ctx.viewport_width, ctx.viewport_height, element_css_vars, parent_style, ) _ => () } } } None => () } // Preserve table layout let tag = selector_elem.tag_name if is_table_element(tag) { let keeps_non_table_display = style.display == @types.Contents || style.display == @types.Display::None if !is_table_display(style.display) && !keeps_non_table_display { style.display = ua_style.display } } // Size containment does not apply to table-cell elements, while // layout/paint/style containment still can. if style.display == @types.TableCell && style.contain.size { style.contain = { ..style.contain, size: false } } // Apply inline styles match inline_css { Some(css) => style = apply_inline_css_with_vars( style, css, element_css_vars, ctx.viewport_width, ctx.viewport_height, parent_style, ) None => () } style = apply_browser_normal_line_height_if_needed( style, parent_style, author_line_height_present, ) style = normalize_display_contents_for_unusual_html( selector_elem.tag_name, style, ) // Effects that establish a containing block for positioned descendants: // filter and backdrop-filter (non-none values). var() is resolved against // the element's custom properties first so `filter: var(--x)` that computes // to none / invalid does not mis-establish the containing block. let mut establishes_effect_cb = false match cascaded { Some(cascaded_values) => { match cascaded_values.get_value("filter") { Some(value) => if filter_value_establishes_effect_cb(value, element_css_vars) { establishes_effect_cb = true } None => () } match cascaded_values.get_value("backdrop-filter") { Some(value) => if filter_value_establishes_effect_cb(value, element_css_vars) { establishes_effect_cb = true } None => () } } None => () } match inline_css { Some(css) => if inline_css_establishes_effect_containing_block(css) { establishes_effect_cb = true } None => () } // @css sets has_filter from the raw declaration text without resolving // var(), so `filter: var(--x)` computing to none / invalid still flips it // true. Set it authoritatively from the var-aware determination above; this // covers every trigger @css uses (cascaded filter, cascaded backdrop-filter, // and the inline path), so it neither under- nor over-establishes the CB. style.has_filter = establishes_effect_cb style.contain = { ..style.contain, paint: style.contain.paint || establishes_effect_cb, } // Apply viewport dimensions if root if is_root { let is_out_of_flow_root = style.position == @types.Absolute || style.position == @types.Fixed if is_out_of_flow_root { style = resolve_out_of_flow_root_auto_size( style, ctx.viewport_width, ctx.viewport_height, ) } let has_horizontal_inset_pair = is_out_of_flow_root && inset_is_definite_for_root(style.inset.left, ctx.viewport_width) && inset_is_definite_for_root(style.inset.right, ctx.viewport_width) if !has_horizontal_inset_pair { match style.width { @types.Dimension::Auto => style.width = @types.Dimension::Length(ctx.viewport_width) _ => () } } } let adjusted = adjust_for_box_sizing(style) match (inline_only_cache_css, inline_only_cache_variant_key) { (Some(css), Some(variant_key)) => @inline_style_cache.inline_only_style_cache_put( css, variant_key, adjusted, ) _ => () } adjusted } ///| fn compute_element_css_vars_indexed( selector_elem : @css.Element, inline_css : String?, indexed_stylesheets : Array[@css.IndexedStylesheet], ctx : RenderContext, css_vars : Map[String, String], ) -> Map[String, String] { let cascaded = if indexed_stylesheets.length() > 0 { let media_env = @css.MediaEnvironment::with_color_scheme( ctx.viewport_width, ctx.viewport_height, ctx.color_scheme, ) Some( @css.cascade_element_indexed( selector_elem, indexed_stylesheets, [], Some(media_env), ), ) } else { None } compute_element_css_vars(cascaded, inline_css, css_vars) } ///| /// Apply a CSS property value to a style (internal) /// Uses default viewport (1920x1080) for vh/vw resolution fn apply_css_property( style : @style.Style, property : String, value : String, ) -> @style.Style { // Use direct property application to avoid string parsing overhead @css.apply_property_direct(style, property, value, @css.ComputeContext::new()) } ///| fn is_css_calc_space(c : Char) -> Bool { c == ' ' || c == '\n' || c == '\t' || c == '\r' || c == '\u{0C}' } ///| fn parse_number_with_suffix(token : String, suffix : String) -> Double? { match token.strip_suffix(suffix) { Some(raw) => { let parsed = @string.parse_double(raw.to_owned()) catch { _ => return None } Some(parsed) } None => None } } ///| fn parse_calc_length_or_percent_token( token : String, ctx : @css.ComputeContext, ) -> (Double, Double)? { let t = token.trim().to_owned() if t.is_empty() { return None } match parse_number_with_suffix(t, "%") { Some(n) => return Some((0.0, n / 100.0)) None => () } match parse_number_with_suffix(t, "px") { Some(n) => return Some((n, 0.0)) None => () } match parse_number_with_suffix(t, "vh") { Some(n) => return Some((ctx.viewport_height * n / 100.0, 0.0)) None => () } match parse_number_with_suffix(t, "vw") { Some(n) => return Some((ctx.viewport_width * n / 100.0, 0.0)) None => () } let parsed = @string.parse_double(t) catch { _ => return None } Some((parsed, 0.0)) } ///| fn parse_simple_calc_length_percent_terms( value : String, ctx : @css.ComputeContext, ) -> (Double, Double)? { let v = value.trim().to_owned() if !v.has_prefix("calc(") || !v.has_suffix(")") { return None } let inner = v.unsafe_substring(start=5, end=v.length() - 1) let mut i = 0 let mut sign = 1.0 let mut length_px = 0.0 let mut percent = 0.0 let mut saw_term = false while i < inner.length() { while i < inner.length() && is_css_calc_space(inner[i].to_int().unsafe_to_char()) { i = i + 1 } if i >= inner.length() { break } let c = inner[i].to_int().unsafe_to_char() if c == '+' { sign = 1.0 i = i + 1 continue } if c == '-' { sign = -1.0 i = i + 1 continue } let start = i while i < inner.length() { let ch = inner[i].to_int().unsafe_to_char() if ch == '+' || ch == '-' || is_css_calc_space(ch) { break } i = i + 1 } if start == i { return None } let token = inner.unsafe_substring(start~, end=i) match parse_calc_length_or_percent_token(token, ctx) { Some((term_px, term_percent)) => { length_px = length_px + sign * term_px percent = percent + sign * term_percent saw_term = true sign = 1.0 } None => return None } } if saw_term { Some((length_px, percent)) } else { None } } ///| fn mixed_calc_height_reference(ctx : @css.ComputeContext) -> Double { match ctx.parent_style { Some(parent) => match parent.height { @types.Length(h) => h @types.Percent(p) => ctx.viewport_height * p _ => ctx.viewport_height } None => ctx.viewport_height } } ///| fn resolve_axis_mixed_calc_dimension( property : String, value : String, ctx : @css.ComputeContext, ) -> @types.Dimension? { if property == "min-width" { return match parse_simple_calc_length_percent_terms(value, ctx) { Some((length_px, percent)) => if length_px.abs() < 0.0001 || percent.abs() < 0.0001 { None } else { Some(@types.Length(if length_px < 0.0 { 0.0 } else { length_px })) } None => None } } if property != "height" && property != "min-height" && property != "max-height" { return None } match parse_simple_calc_length_percent_terms(value, ctx) { Some((length_px, percent)) => if length_px.abs() < 0.0001 || percent.abs() < 0.0001 { None } else { let resolved = mixed_calc_height_reference(ctx) * percent + length_px Some(@types.Length(if resolved < 0.0 { 0.0 } else { resolved })) } None => None } } ///| /// Apply a CSS property value to a style with custom viewport and CSS variables fn apply_css_property_with_viewport( style : @style.Style, property : String, value : String, viewport_width : Double, viewport_height : Double, css_vars : Map[String, String], inherited_parent_style : @style.Style?, ) -> @style.Style { let inherited_font_size = match inherited_parent_style { Some(parent) => parent.font_size None => style.font_size } let context_font_size = if property == "font-size" || property == "font" { inherited_font_size } else { style.font_size } // Use direct property application to avoid string parsing overhead let ctx : @css.ComputeContext = { parent_style: inherited_parent_style, root_font_size: 16.0, font_size: context_font_size, viewport_width, viewport_height, custom_properties: css_vars, } let resolved = @css.apply_property_direct(style, property, value, ctx) match resolve_axis_mixed_calc_dimension(property, value, ctx) { Some(dim) => match property { "height" => { ..resolved, height: dim } "min-width" => { ..resolved, min_width: dim } "min-height" => { ..resolved, min_height: dim } "max-height" => { ..resolved, max_height: dim } _ => resolved } None => resolved } } ///| /// Apply a CSS property value to a style (public for testing) pub fn apply_css_property_debug( style : @style.Style, property : String, value : String, ) -> @style.Style { apply_css_property(style, property, value) } ///| /// Parse inline CSS and apply properties to style with CSS variables fn apply_inline_css_with_vars( target : @style.Style, inline_css : String, css_vars : Map[String, String], viewport_width : Double, viewport_height : Double, inherited_parent_style : @style.Style?, ) -> @style.Style { let mut result = target let declarations = parse_inline_declarations_cached(inline_css) for i = 0; i < declarations.length(); i = i + 1 { let (prop, value) = declarations[i] result = apply_css_property_with_viewport( result, prop, value, viewport_width, viewport_height, css_vars, inherited_parent_style, ) } result }