use std::collections::HashMap;
use std::sync::Arc;
use indexmap::IndexMap;
use crate::tdom::node::{Node, NodeArena, NodeId, NodeType};
use super::{
matcher::selector_chain_matches_with_state,
selector::{DynamicState, PseudoClassSet, Selector},
stylesheet::{CssRule, parse_declaration_splits},
};
#[derive(Debug, Clone, Default)]
pub(crate) struct CascadeIndex {
rank: Vec<u32>,
by_tag: HashMap<NodeType, Vec<u32>>,
by_class: HashMap<String, Vec<u32>>,
by_id: HashMap<String, Vec<u32>>,
wildcard: Vec<u32>,
}
impl CascadeIndex {
pub(crate) fn build(css: &CssRule) -> Self {
let count = css.0.len();
let mut order: Vec<u32> = (0..count as u32).collect();
order.sort_by_key(|&rule| css.0[rule as usize].selector.specificity());
let mut rank = vec![0u32; count];
for (position, &rule) in order.iter().enumerate() {
rank[rule as usize] = position as u32;
}
let mut by_tag: HashMap<NodeType, Vec<u32>> = HashMap::new();
let mut by_class: HashMap<String, Vec<u32>> = HashMap::new();
let mut by_id: HashMap<String, Vec<u32>> = HashMap::new();
let mut wildcard: Vec<u32> = Vec::new();
for rule in 0..count as u32 {
let Some(compound) = css.0[rule as usize].selector.compounds.last() else {
continue;
};
let mut keyed = false;
for part in compound {
match part {
Selector::Tag(tag) => {
by_tag.entry(*tag).or_default().push(rule);
keyed = true;
}
Selector::Class(class) => {
by_class.entry(class.clone()).or_default().push(rule);
keyed = true;
}
Selector::Id(id) => {
by_id.entry(id.clone()).or_default().push(rule);
keyed = true;
}
_ => {}
}
}
if !keyed {
wildcard.push(rule);
}
}
CascadeIndex {
rank,
by_tag,
by_class,
by_id,
wildcard,
}
}
fn candidates_for(&self, node: &Node, out: &mut Vec<u32>, seen: &mut [u32], stamp: u32) {
out.clear();
let empty: Vec<u32> = Vec::new();
let mut push_bucket = |bucket: &Vec<u32>| {
for &rule in bucket {
if seen[rule as usize] != stamp {
seen[rule as usize] = stamp;
out.push(rule);
}
}
};
push_bucket(&self.wildcard);
push_bucket(self.by_tag.get(&node.node_type).unwrap_or(&empty));
if let Some(classes) = node.attributes.get("class") {
for class in classes.split_whitespace() {
push_bucket(self.by_class.get(class).unwrap_or(&empty));
}
}
if let Some(id) = node.get_id() {
push_bucket(self.by_id.get(id).unwrap_or(&empty));
}
out.sort_unstable_by_key(|&rule| self.rank[rule as usize]);
}
}
fn apply_inline_style_at(arena: &mut NodeArena, node_id: NodeId, important: bool) {
let Some(style) = arena[node_id].attributes.get("style").cloned() else {
return;
};
let (normal, imp) = parse_declaration_splits(&style);
let subset = if important { imp } else { normal };
if !subset.0.is_empty() {
arena[node_id].css_attr.add(&subset);
}
}
pub fn css_into_node_with_state(
arena: &mut NodeArena,
root_id: NodeId,
css: &CssRule,
state: &DynamicState,
) {
let index = css.cascade_index();
let mut seen = vec![0u32; css.0.len()];
let mut candidates: Vec<u32> = Vec::new();
let mut stack = vec![root_id];
let mut stamp: u32 = 0;
while let Some(node_id) = stack.pop() {
stamp = stamp.wrapping_add(1);
if stamp == 0 {
seen.fill(0);
stamp = 1;
}
index.candidates_for(&arena[node_id], &mut candidates, &mut seen, stamp);
for &rule in &candidates {
let entry = &css.0[rule as usize];
if entry.normal.0.is_empty() {
continue;
}
if selector_chain_matches_with_state(arena, node_id, &entry.selector, state) {
arena[node_id].css_attr.add(&entry.normal);
}
}
apply_inline_style_at(arena, node_id, false);
for &rule in &candidates {
let entry = &css.0[rule as usize];
if entry.important.0.is_empty() {
continue;
}
if selector_chain_matches_with_state(arena, node_id, &entry.selector, state) {
arena[node_id].css_attr.add(&entry.important);
}
}
apply_inline_style_at(arena, node_id, true);
stack.extend(arena[node_id].children.iter().copied());
}
inherit_css(arena, root_id);
}
pub fn css_into_node_with_hover(
arena: &mut NodeArena,
root_id: NodeId,
css: &CssRule,
hovered: Option<NodeId>,
) {
css_into_node_with_state(
arena,
root_id,
css,
&DynamicState {
hovered,
focused: None,
},
);
}
pub fn css_into_node(arena: &mut NodeArena, root_id: NodeId, css: &CssRule) {
css_into_node_with_state(arena, root_id, css, &DynamicState::default());
}
pub fn css_pseudo_rule_set(css: &CssRule) -> PseudoClassSet {
css.0.iter().fold(PseudoClassSet::EMPTY, |set, entry| {
set.union(PseudoClassSet::from_selector(&entry.selector))
})
}
pub fn reset_css_attr(arena: &mut NodeArena, root_id: NodeId) {
let mut stack = vec![root_id];
while let Some(id) = stack.pop() {
arena[id].css_attr.0.clear();
stack.extend(arena[id].children.iter().copied());
}
}
pub const SUPPORTED_PROPERTIES: &[&str] = &[
"color",
"background-color",
"background",
"caret-color",
"text-align",
"font-weight",
"font-style",
"text-decoration",
"font-size",
"width",
"height",
"min-width",
"max-width",
"min-height",
"max-height",
"box-sizing",
"border-radius",
"text-transform",
"line-height",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"border",
"border-top",
"border-right",
"border-bottom",
"border-left",
"border-width",
"border-style",
"border-color",
"border-top-width",
"border-top-style",
"border-top-color",
"border-right-width",
"border-right-style",
"border-right-color",
"border-bottom-width",
"border-bottom-style",
"border-bottom-color",
"border-left-width",
"border-left-style",
"border-left-color",
"border-radius",
"border-top-left-radius",
"border-top-right-radius",
"border-bottom-right-radius",
"border-bottom-left-radius",
"display",
"flex",
"flex-flow",
"flex-direction",
"flex-wrap",
"justify-content",
"align-items",
"align-self",
"align-content",
"flex-grow",
"flex-shrink",
"flex-basis",
"order",
"gap",
"row-gap",
"column-gap",
"place-content",
"place-items",
"place-self",
"white-space",
"word-break",
"overflow-wrap",
"word-wrap",
"position",
"z-index",
"top",
"right",
"bottom",
"left",
"columns",
"column-width",
"column-count",
"column-gap",
"column-fill",
"column-rule",
"column-rule-width",
"column-rule-style",
"column-rule-color",
"visibility",
"overflow",
"overflow-x",
"overflow-y",
];
pub const INHERITED_PROPS: &[&str] = &[
"color",
"text-align",
"font-weight",
"font-style",
"text-decoration",
"font-size",
"text-transform",
"line-height",
"visibility",
"white-space",
"word-break",
"overflow-wrap",
];
fn inherit_css(arena: &mut NodeArena, root_id: NodeId) {
type InheritedMap = IndexMap<Arc<str>, Arc<str>>;
let mut stack: Vec<(NodeId, Arc<InheritedMap>)> =
vec![(root_id, Arc::new(InheritedMap::new()))];
while let Some((node_id, parent_attrs)) = stack.pop() {
{
let node = &mut arena[node_id];
for key in INHERITED_PROPS {
if let Some(value) = parent_attrs.get(*key) {
node.css_attr
.0
.entry(Arc::from(*key))
.or_insert_with(|| value.clone());
}
}
}
let merged = {
let node = &arena[node_id];
let diverges = INHERITED_PROPS.iter().any(|key| {
node.css_attr
.0
.get(*key)
.is_some_and(|value| parent_attrs.get(*key) != Some(value))
});
if diverges {
let mut map = (*parent_attrs).clone();
for key in INHERITED_PROPS {
if let Some(value) = node.css_attr.0.get(*key) {
map.insert(Arc::from(*key), value.clone());
}
}
Arc::new(map)
} else {
Arc::clone(&parent_attrs)
}
};
for child in arena[node_id].children.iter().rev() {
stack.push((*child, Arc::clone(&merged)));
}
}
}