use cssbox_core::style::ComputedStyle;
use crate::css::{apply_declarations, parse_style_attribute, parse_stylesheet, CssRule};
use crate::dom::{DomNodeId, DomTree};
#[derive(Debug, Clone)]
struct MatchedRule {
declarations: Vec<crate::css::CssDeclaration>,
specificity: Specificity,
source_order: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Specificity {
inline: bool,
id: u32,
class: u32,
type_sel: u32,
}
impl Specificity {
fn new(id: u32, class: u32, type_sel: u32) -> Self {
Self {
inline: false,
id,
class,
type_sel,
}
}
}
fn compute_specificity(selector: &str) -> Specificity {
let mut id_count = 0u32;
let mut class_count = 0u32;
let mut type_count = 0u32;
for part in selector.split(|c: char| c.is_whitespace() || c == '>' || c == '+' || c == '~') {
let part = part.trim();
if part.is_empty() {
continue;
}
for segment in split_selector_segments(part) {
if segment.starts_with('#') {
id_count += 1;
} else if segment.starts_with('.')
|| segment.starts_with('[')
|| segment.starts_with(':')
{
class_count += 1;
} else if segment == "*" {
} else {
type_count += 1;
}
}
}
Specificity::new(id_count, class_count, type_count)
}
fn split_selector_segments(selector: &str) -> Vec<&str> {
let mut segments = Vec::new();
let mut start = 0;
let bytes = selector.as_bytes();
for i in 1..bytes.len() {
if bytes[i] == b'#' || bytes[i] == b'.' || bytes[i] == b'[' || bytes[i] == b':' {
if start < i {
segments.push(&selector[start..i]);
}
start = i;
}
}
if start < selector.len() {
segments.push(&selector[start..]);
}
segments
}
fn selector_matches(tree: &DomTree, node: DomNodeId, selector: &str) -> bool {
let _dom_node = tree.node(node);
let parts: Vec<&str> = selector.split_whitespace().collect();
if parts.len() == 1 {
return simple_selector_matches(tree, node, parts[0]);
}
if parts.len() >= 2 {
let last = parts.last().unwrap();
if !simple_selector_matches(tree, node, last) {
return false;
}
let ancestor_selector = parts[..parts.len() - 1].join(" ");
let mut current = tree.parent(node);
while let Some(parent) = current {
if selector_matches(tree, parent, &ancestor_selector) {
return true;
}
current = tree.parent(parent);
}
return false;
}
false
}
fn simple_selector_matches(tree: &DomTree, node: DomNodeId, selector: &str) -> bool {
let dom_node = tree.node(node);
if selector == "*" {
return dom_node.is_element();
}
let segments = split_selector_segments(selector);
for segment in &segments {
if let Some(id) = segment.strip_prefix('#') {
match dom_node.get_attribute("id") {
Some(v) if v == id => {}
_ => return false,
}
} else if let Some(class) = segment.strip_prefix('.') {
match dom_node.get_attribute("class") {
Some(classes) => {
if !classes.split_whitespace().any(|c| c == class) {
return false;
}
}
None => return false,
}
} else if segment.starts_with('[') {
let inner = segment.trim_start_matches('[').trim_end_matches(']');
if let Some((attr, val)) = inner.split_once('=') {
let val = val.trim_matches('"').trim_matches('\'');
match dom_node.get_attribute(attr) {
Some(v) if v == val => {}
_ => return false,
}
} else if dom_node.get_attribute(inner).is_none() {
return false;
}
} else {
match dom_node.tag_name() {
Some(tag) if tag.eq_ignore_ascii_case(segment) => {}
_ => return false,
}
}
}
!segments.is_empty()
}
pub fn resolve_styles(tree: &DomTree, stylesheets: &[String]) -> Vec<(DomNodeId, ComputedStyle)> {
let mut rules: Vec<(CssRule, usize)> = Vec::new();
for (i, css) in stylesheets.iter().enumerate() {
for rule in parse_stylesheet(css) {
rules.push((rule, i));
}
}
let mut styles = Vec::new();
for node_id in tree.iter_dfs() {
let dom_node = tree.node(node_id);
if !dom_node.is_element() {
continue;
}
let mut style = default_style_for_tag(dom_node.tag_name().unwrap_or(""));
let mut matched: Vec<MatchedRule> = Vec::new();
for (order, (rule, _sheet_idx)) in rules.iter().enumerate() {
for selector in rule.selector.split(',') {
let selector = selector.trim();
if selector_matches(tree, node_id, selector) {
matched.push(MatchedRule {
declarations: rule.declarations.clone(),
specificity: compute_specificity(selector),
source_order: order,
});
}
}
}
matched.sort_by(|a, b| {
a.specificity
.cmp(&b.specificity)
.then(a.source_order.cmp(&b.source_order))
});
for rule in &matched {
apply_declarations(&mut style, &rule.declarations);
}
if let Some(inline_style) = dom_node.get_attribute("style") {
let inline_decls = parse_style_attribute(inline_style);
apply_declarations(&mut style, &inline_decls);
}
styles.push((node_id, style));
}
styles
}
fn default_style_for_tag(tag: &str) -> ComputedStyle {
use cssbox_core::style::Display;
use cssbox_core::values::LengthPercentageAuto;
let mut style = ComputedStyle::default();
match tag.to_lowercase().as_str() {
"html" | "body" | "div" | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol"
| "li" | "article" | "section" | "nav" | "aside" | "header" | "footer" | "main"
| "figure" | "figcaption" | "blockquote" | "pre" | "hr" | "form" | "fieldset"
| "legend" | "details" | "summary" | "dl" | "dt" | "dd" | "address" => {
style.display = Display::BLOCK;
}
"table" => {
style.display = Display::TABLE;
}
"tr" => {
style.display = Display::TABLE_ROW;
}
"td" | "th" => {
style.display = Display::TABLE_CELL;
}
"thead" => {
style.display = Display::TABLE_HEADER_GROUP;
}
"tbody" => {
style.display = Display::TABLE_ROW_GROUP;
}
"tfoot" => {
style.display = Display::TABLE_FOOTER_GROUP;
}
"colgroup" => {
style.display = Display::TABLE_COLUMN_GROUP;
}
"col" => {
style.display = Display::TABLE_COLUMN;
}
"caption" => {
style.display = Display::TABLE_CAPTION;
}
_ => {
style.display = Display::INLINE;
}
}
match tag.to_lowercase().as_str() {
"body" => {
style.margin_top = LengthPercentageAuto::px(8.0);
style.margin_right = LengthPercentageAuto::px(8.0);
style.margin_bottom = LengthPercentageAuto::px(8.0);
style.margin_left = LengthPercentageAuto::px(8.0);
}
"p" | "blockquote" | "figure" | "ul" | "ol" | "dl" => {
style.margin_top = LengthPercentageAuto::px(16.0);
style.margin_bottom = LengthPercentageAuto::px(16.0);
}
"h1" => {
style.margin_top = LengthPercentageAuto::px(21.44);
style.margin_bottom = LengthPercentageAuto::px(21.44);
}
_ => {}
}
style
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_specificity_calculation() {
assert!(compute_specificity("#id") > compute_specificity(".class"));
assert!(compute_specificity(".class") > compute_specificity("div"));
assert!(compute_specificity("div.class") > compute_specificity("div"));
}
#[test]
fn test_simple_selector_matching() {
let mut tree = DomTree::new();
let root = tree.root();
let mut attrs = std::collections::HashMap::new();
attrs.insert("id".to_string(), "test".to_string());
attrs.insert("class".to_string(), "box red".to_string());
let div = tree.add_element(root, "div", attrs);
assert!(simple_selector_matches(&tree, div, "div"));
assert!(simple_selector_matches(&tree, div, "#test"));
assert!(simple_selector_matches(&tree, div, ".box"));
assert!(simple_selector_matches(&tree, div, ".red"));
assert!(simple_selector_matches(&tree, div, "div.box"));
assert!(!simple_selector_matches(&tree, div, "span"));
assert!(!simple_selector_matches(&tree, div, ".missing"));
}
#[test]
fn test_default_style_for_div() {
let style = default_style_for_tag("div");
assert_eq!(style.display, cssbox_core::style::Display::BLOCK);
}
}