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},
values::Viewport,
};
#[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 add_prefixed(
target: &mut crate::html::css_parse::properties::CssAttr,
css: &crate::html::css_parse::properties::CssAttr,
pe: crate::html::css_parse::PseudoElement,
) {
let suffix = match pe {
crate::html::css_parse::PseudoElement::Before => "::before",
crate::html::css_parse::PseudoElement::After => "::after",
crate::html::css_parse::PseudoElement::SliderThumb => "::slider-thumb",
crate::html::css_parse::PseudoElement::SliderTrack => "::slider-track",
crate::html::css_parse::PseudoElement::SliderProgress => "::slider-progress",
};
for (k, v) in &css.0 {
let key: Arc<str> = Arc::from(format!("{k}{suffix}"));
target.0.insert(key, Arc::clone(v));
}
}
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);
}
}
fn substitute_vars(input: &str, vars: &HashMap<String, String>, depth: usize) -> String {
const MAX_DEPTH: usize = 16;
if depth > MAX_DEPTH || !input.contains("var(") {
return input.to_string();
}
let mut result = String::with_capacity(input.len());
let mut rest = input;
while let Some(pos) = rest.find("var(") {
result.push_str(&rest[..pos]);
let after = &rest[pos + 4..]; let Some(close) = find_matching_paren(after) else {
result.push_str("var(");
rest = after;
continue;
};
let inner = &after[..close];
rest = &after[close + 1..];
let (name, fallback) = split_var_args(inner);
let name = name.trim();
if let Some(value) = vars.get(name) {
let resolved = substitute_vars(value, vars, depth + 1);
result.push_str(&resolved);
} else if let Some(fb) = fallback {
let resolved = substitute_vars(fb, vars, depth + 1);
let trimmed = fb_style(&resolved);
result.push_str(&trimmed);
} else {
}
}
result.push_str(rest);
result
}
fn fb_style(s: &str) -> String {
s.trim().to_string()
}
fn find_matching_paren(s: &str) -> Option<usize> {
let bytes = s.as_bytes();
let mut depth = 1usize;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
fn split_var_args(inner: &str) -> (&str, Option<&str>) {
let bytes = inner.as_bytes();
let mut depth = 0usize;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'(' => depth += 1,
b')' => depth -= 1,
b',' if depth == 0 => {
return (&inner[..i], Some(inner[i + 1..].trim()));
}
_ => {}
}
}
(inner, None)
}
fn resolve_custom_props(
css_attr: &crate::html::css_parse::properties::CssAttr,
parent_vars: HashMap<String, String>,
) -> HashMap<String, String> {
let mut vars = parent_vars;
for (k, v) in &css_attr.0 {
if k.starts_with("--") {
vars.insert(k.to_string(), v.to_string());
}
}
let entries: Vec<(String, String)> = vars.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
for (k, raw) in entries {
if raw.contains("var(") {
let resolved = substitute_vars(&raw, &vars, 0);
vars.insert(k, resolved);
}
}
vars
}
fn substitute_property_vars(
css_attr: &mut crate::html::css_parse::properties::CssAttr,
vars: &HashMap<String, String>,
) {
let keys: Vec<Arc<str>> = css_attr
.0
.keys()
.filter(|k| !k.starts_with("--"))
.cloned()
.collect();
for key in keys {
if let Some(v) = css_attr.0.get_mut(&key)
&& v.contains("var(")
{
let resolved = substitute_vars(v, vars, 0);
*v = Arc::from(resolved.as_str());
}
}
}
pub fn css_into_node_with_state(
arena: &mut NodeArena,
root_id: NodeId,
css: &CssRule,
state: &DynamicState,
) {
let index = css.cascade_index();
let media_active: Vec<bool> = css
.0
.iter()
.map(|e| e.media.as_ref().is_none_or(|m| m.matches(&state.viewport)))
.collect();
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;
let mut var_scopes: HashMap<NodeId, HashMap<String, String>> = HashMap::new();
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);
candidates.retain(|&rule| media_active[rule as usize]);
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) {
if let Some(pe) = entry.selector.pseudo_element {
add_prefixed(&mut arena[node_id].css_attr, &entry.normal, pe);
} else {
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) {
if let Some(pe) = entry.selector.pseudo_element {
add_prefixed(&mut arena[node_id].css_attr, &entry.important, pe);
} else {
arena[node_id].css_attr.add(&entry.important);
}
}
}
apply_inline_style_at(arena, node_id, true);
let parent_vars = var_scopes.remove(&node_id).unwrap_or_default();
let node_vars = resolve_custom_props(&arena[node_id].css_attr, parent_vars);
substitute_property_vars(&mut arena[node_id].css_attr, &node_vars);
for &child in &arena[node_id].children {
var_scopes.insert(child, node_vars.clone());
stack.push(child);
}
}
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,
viewport: Viewport { cols: 0, rows: 0 },
},
);
}
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",
"background-clip",
"caret-color",
"accent-color",
"text-align",
"font-weight",
"font-style",
"text-decoration",
"font-size",
"width",
"height",
"min-width",
"max-width",
"min-height",
"max-height",
"box-sizing",
"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",
"place-content",
"place-items",
"place-self",
"grid",
"grid-template",
"grid-template-columns",
"grid-template-rows",
"grid-template-areas",
"grid-area",
"grid-column",
"grid-row",
"grid-column-start",
"grid-column-end",
"grid-row-start",
"grid-row-end",
"grid-auto-columns",
"grid-auto-rows",
"grid-auto-flow",
"justify-items",
"justify-self",
"white-space",
"word-break",
"overflow-wrap",
"word-wrap",
"letter-spacing",
"word-spacing",
"text-indent",
"text-overflow",
"user-select",
"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",
"list-style",
"list-style-type",
"table-layout",
"caption-side",
"empty-cells",
"font",
];
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",
"list-style-type",
"empty-cells",
"letter-spacing",
"word-spacing",
"text-indent",
"accent-color",
];
fn inherit_css(arena: &mut NodeArena, root_id: NodeId) {
const INITIAL_SENTINEL: &str = "\u{1}initial";
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 parent_id = arena.get(node_id).and_then(|n| n.parent);
let mut resolves: Vec<(Arc<str>, Option<Arc<str>>)> = Vec::new();
{
let node = &arena[node_id];
for (key, value) in node.css_attr.0.iter() {
let text = value.trim();
if text.eq_ignore_ascii_case("inherit") {
let parent_value = parent_id
.and_then(|pid| arena.get(pid))
.and_then(|p| p.css_attr.0.get(key).cloned());
resolves.push((key.clone(), parent_value));
} else if text.eq_ignore_ascii_case("initial") {
resolves.push((key.clone(), None));
}
}
}
if !resolves.is_empty() {
let node = &mut arena[node_id];
for (key, value) in resolves {
match value {
Some(v) => {
node.css_attr.0.insert(key, v);
}
None => {
node.css_attr.0.insert(key, Arc::from(INITIAL_SENTINEL));
}
}
}
}
}
{
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)));
}
}
}