layout-cat 0.1.0

Box-model layout: cascade + block layout over a dom-cat tree using css-cat stylesheets. Produces a LayoutBox tree with positions and dimensions. No mut, no Rc/Arc, no interior mutability, no panics, exhaustive matches. Fourth sub-crate of a Servo-replacement webview runtime targeting Tauri.
//! Apply CSS rules to DOM elements to produce a per-element
//! `ComputedStyle` map.
//!
//! Specificity ordering follows CSS3: rules are kept in source order
//! and later wins on tie.  Cross-rule specificity (ID > class > type) is
//! deferred to v0.2; v0 uses source order and inline `style="..."` last.

use std::collections::BTreeMap;

use css_cat::{ComplexSelector, Declaration, Item, Stylesheet, Value};
use dom_cat::{Document, Node, NodeId};

use crate::color::{self, Color};
use crate::length::LengthValue;
use crate::style::{ComputedStyle, Display};

/// Compute the style of every element in `doc` against `sheet`.
#[must_use]
pub fn cascade(doc: &Document, sheet: &Stylesheet) -> BTreeMap<NodeId, ComputedStyle> {
    doc.arena()
        .ids()
        .filter(|id| matches!(doc.get(*id), Some(Node::Element(_))))
        .map(|id| (id, compute_for(doc, sheet, id)))
        .collect()
}

fn compute_for(doc: &Document, sheet: &Stylesheet, id: NodeId) -> ComputedStyle {
    let from_rules = apply_rules(doc, sheet, id, ComputedStyle::default());
    apply_inline_style(doc, id, from_rules)
}

fn apply_rules(
    doc: &Document,
    sheet: &Stylesheet,
    id: NodeId,
    base: ComputedStyle,
) -> ComputedStyle {
    sheet.rules().iter().fold(base, |style, item| match item {
        Item::Rule(rule) => {
            if rule_matches(doc, id, rule.selectors().selectors()) {
                rule.declarations().iter().fold(style, apply_declaration)
            } else {
                style
            }
        }
        Item::AtRule(_) => style,
    })
}

fn rule_matches(doc: &Document, id: NodeId, selectors: &[ComplexSelector]) -> bool {
    selectors
        .iter()
        .any(|sel| dom_cat::matcher::matches(doc, id, sel))
}

fn apply_inline_style(doc: &Document, id: NodeId, style: ComputedStyle) -> ComputedStyle {
    doc.get(id)
        .and_then(Node::as_element)
        .and_then(|e| e.attribute("style"))
        .map_or(style.clone(), |inline| parse_inline(inline, style))
}

fn parse_inline(source: &str, base: ComputedStyle) -> ComputedStyle {
    // Wrap the inline source as a synthetic rule so we can reuse the CSS
    // declaration parser: `* { <inline> }`.
    let wrapped = format!("*{{{source}}}");
    let parsed = css_cat::parse(&wrapped).ok();
    parsed.map_or(base.clone(), |sheet| {
        sheet
            .rules()
            .iter()
            .find_map(|item| match item {
                Item::Rule(r) => Some(r.declarations().to_vec()),
                Item::AtRule(_) => None,
            })
            .map_or(base.clone(), |declarations| {
                declarations.iter().fold(base, apply_declaration)
            })
    })
}

fn apply_declaration(style: ComputedStyle, decl: &Declaration) -> ComputedStyle {
    let values = decl.values();
    let (mt, mr, mb, ml) = style.margins();
    let (pt, pr, pb, pl) = style.padding();
    let (bt, br, bb, bl) = style.border_widths();
    match decl.property() {
        "display" => style.with_display(parse_display(values)),
        "width" => style.with_width(parse_length(values)),
        "height" => style.with_height(parse_length(values)),
        "margin" => {
            let (t, r, b, l) = shorthand_sides(values);
            style.with_margins(t, r, b, l)
        }
        "margin-top" => style.with_margins(parse_length(values), mr, mb, ml),
        "margin-right" => style.with_margins(mt, parse_length(values), mb, ml),
        "margin-bottom" => style.with_margins(mt, mr, parse_length(values), ml),
        "margin-left" => style.with_margins(mt, mr, mb, parse_length(values)),
        "padding" => {
            let (t, r, b, l) = shorthand_sides(values);
            style.with_padding(t, r, b, l)
        }
        "padding-top" => style.with_padding(parse_length(values), pr, pb, pl),
        "padding-right" => style.with_padding(pt, parse_length(values), pb, pl),
        "padding-bottom" => style.with_padding(pt, pr, parse_length(values), pl),
        "padding-left" => style.with_padding(pt, pr, pb, parse_length(values)),
        "border-width" => {
            let (t, r, b, l) = shorthand_sides(values);
            style.with_border_widths(t, r, b, l)
        }
        "border-top-width" => style.with_border_widths(parse_length(values), br, bb, bl),
        "border-right-width" => style.with_border_widths(bt, parse_length(values), bb, bl),
        "border-bottom-width" => style.with_border_widths(bt, br, parse_length(values), bl),
        "border-left-width" => style.with_border_widths(bt, br, bb, parse_length(values)),
        "color" => parse_color(values).map_or(style.clone(), |c| style.with_color(c)),
        "background-color" | "background" => {
            parse_color(values).map_or(style.clone(), |c| style.with_background_color(c))
        }
        "font-size" => style.with_font_size(parse_length(values)),
        _other => style,
    }
}

fn shorthand_sides(values: &[Value]) -> (LengthValue, LengthValue, LengthValue, LengthValue) {
    let lengths: Vec<LengthValue> = values
        .iter()
        .filter(|v| !matches!(v, Value::Comma | Value::Slash))
        .map(|v| length_from(std::slice::from_ref(v)))
        .collect();
    let zero = LengthValue::Px(0.0);
    let get = |i: usize| lengths.get(i).copied().unwrap_or(zero);
    match lengths.len() {
        0 => (zero, zero, zero, zero),
        1 => {
            let v = get(0);
            (v, v, v, v)
        }
        2 => {
            let v = get(0);
            let h = get(1);
            (v, h, v, h)
        }
        3 => (get(0), get(1), get(2), get(1)),
        _other => (get(0), get(1), get(2), get(3)),
    }
}

fn parse_display(values: &[Value]) -> Display {
    values
        .iter()
        .find_map(|v| match v {
            Value::Ident(name) => match name.as_str() {
                "block" => Some(Display::Block),
                "inline" => Some(Display::Inline),
                "none" => Some(Display::None),
                _other => None,
            },
            _other => None,
        })
        .unwrap_or(Display::Block)
}

fn parse_length(values: &[Value]) -> LengthValue {
    length_from(values)
}

fn length_from(values: &[Value]) -> LengthValue {
    values
        .iter()
        .find_map(value_to_length)
        .unwrap_or(LengthValue::Px(0.0))
}

fn value_to_length(value: &Value) -> Option<LengthValue> {
    match value {
        Value::Ident(name) if name == "auto" => Some(LengthValue::Auto),
        Value::Integer(n) => Some(LengthValue::Px(integer_to_f64(*n))),
        Value::Number(n) => Some(LengthValue::Px(*n)),
        Value::Percentage(p) => Some(LengthValue::Percent(*p)),
        Value::Dimension { value, unit } => match unit.as_str() {
            "px" => Some(LengthValue::Px(*value)),
            "em" => Some(LengthValue::Em(*value)),
            _other => Some(LengthValue::Px(*value)),
        },
        _other => None,
    }
}

fn integer_to_f64(n: i64) -> f64 {
    i32::try_from(n).map_or_else(|_| n.to_string().parse::<f64>().unwrap_or(0.0), f64::from)
}

fn parse_color(values: &[Value]) -> Option<Color> {
    values.iter().find_map(|v| match v {
        Value::Ident(name) => color::named(name),
        Value::HashColor(hex) => Color::from_hex(hex),
        _other => None,
    })
}