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};
#[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 {
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,
})
}