Skip to content

Commit e551fc5

Browse files
committed
feat(ie-css): implement selectors, specificity, and cascade #69
Selector engine: - CompoundSelector with type/id/classes/attributes/pseudo-classes - Combinators: Descendant, Child, NextSibling, SubsequentSibling - Attribute selectors: [attr], [attr=val], ~=, |=, ^=, $=, *= - Pseudo-classes: :hover, :focus, :active, :first-child, :last-child, :root, :empty, :not() with recursive parsing - Character-based selector parser handling all CSS selector syntax - Right-to-left matching against DOM tree Specificity: - Specificity(a, b, c): IDs, classes/attrs/pseudo, types - Universal selector doesn't count, :not() contents do Cascade (cascade.rs): - Collects matching declarations from stylesheets - Sorts by: !important > origin (Author > UA) > specificity > order - Returns HashMap<PropertyId, CssValue> per node 20 new tests (17 selector + 3 cascade), 67 total ie-css tests
1 parent fab78c9 commit e551fc5

3 files changed

Lines changed: 1069 additions & 5 deletions

File tree

crates/ie-css/src/cascade.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use std::collections::HashMap;
2+
3+
use ie_dom::{Document, NodeId};
4+
5+
use crate::parser::Stylesheet;
6+
use crate::selector::{Specificity, matches, parse_selector_list, specificity};
7+
use crate::values::{CssValue, PropertyId};
8+
9+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10+
pub enum Origin {
11+
UserAgent,
12+
Author,
13+
}
14+
15+
struct CascadeEntry {
16+
property: PropertyId,
17+
value: CssValue,
18+
important: bool,
19+
origin: Origin,
20+
specificity: Specificity,
21+
source_order: usize,
22+
}
23+
24+
/// Resolve cascaded values for a node
25+
pub fn cascade(
26+
stylesheets: &[(Stylesheet, Origin)],
27+
node: NodeId,
28+
doc: &Document,
29+
) -> HashMap<PropertyId, CssValue> {
30+
let mut entries: Vec<CascadeEntry> = Vec::new();
31+
let mut order = 0;
32+
33+
for (stylesheet, origin) in stylesheets {
34+
for rule in &stylesheet.rules {
35+
let selectors: Vec<_> = rule
36+
.selectors
37+
.iter()
38+
.flat_map(|s| parse_selector_list(s))
39+
.collect();
40+
41+
let max_specificity = selectors
42+
.iter()
43+
.filter(|sel| matches(sel, node, doc))
44+
.map(specificity)
45+
.max();
46+
47+
if let Some(spec) = max_specificity {
48+
for decl in &rule.declarations {
49+
entries.push(CascadeEntry {
50+
property: decl.property,
51+
value: decl.value.clone(),
52+
important: decl.important,
53+
origin: *origin,
54+
specificity: spec,
55+
source_order: order,
56+
});
57+
order += 1;
58+
}
59+
}
60+
}
61+
}
62+
63+
// Sort: important wins, then origin, then specificity, then source order
64+
entries.sort_by(|a, b| {
65+
match (a.important, b.important) {
66+
(true, false) => return std::cmp::Ordering::Greater,
67+
(false, true) => return std::cmp::Ordering::Less,
68+
_ => {}
69+
}
70+
a.origin
71+
.cmp(&b.origin)
72+
.then(a.specificity.cmp(&b.specificity))
73+
.then(a.source_order.cmp(&b.source_order))
74+
});
75+
76+
// Last entry for each property wins
77+
let mut result = HashMap::new();
78+
for entry in entries {
79+
result.insert(entry.property, entry.value);
80+
}
81+
result
82+
}
83+
84+
#[cfg(test)]
85+
mod tests {
86+
use super::*;
87+
use crate::parser::parse_stylesheet;
88+
use crate::values::{CssColor, CssValue, PropertyId};
89+
use ie_dom::Document;
90+
91+
fn make_doc_with_div(class: &str, id: &str) -> (Document, NodeId) {
92+
let mut doc = Document::new();
93+
let root = doc.root;
94+
let div = doc.create_element("div");
95+
doc.append_child(root, div).unwrap();
96+
doc.set_attribute(div, "class", class);
97+
doc.set_attribute(div, "id", id);
98+
(doc, div)
99+
}
100+
101+
#[test]
102+
fn cascade_higher_specificity_wins() {
103+
let (doc, div) = make_doc_with_div("main", "app");
104+
let ss = parse_stylesheet("div { color: red; } #app { color: blue; }");
105+
let sheets = vec![(ss, Origin::Author)];
106+
let result = cascade(&sheets, div, &doc);
107+
assert_eq!(
108+
result.get(&PropertyId::Color),
109+
Some(&CssValue::Color(CssColor::rgb(0, 0, 255)))
110+
);
111+
}
112+
113+
#[test]
114+
fn cascade_later_rule_wins_same_specificity() {
115+
let (doc, div) = make_doc_with_div("", "");
116+
let ss = parse_stylesheet("div { color: red; } div { color: blue; }");
117+
let sheets = vec![(ss, Origin::Author)];
118+
let result = cascade(&sheets, div, &doc);
119+
assert_eq!(
120+
result.get(&PropertyId::Color),
121+
Some(&CssValue::Color(CssColor::rgb(0, 0, 255)))
122+
);
123+
}
124+
125+
#[test]
126+
fn cascade_important_overrides() {
127+
let (doc, div) = make_doc_with_div("", "app");
128+
let ss = parse_stylesheet("div { color: red !important; } #app { color: blue; }");
129+
let sheets = vec![(ss, Origin::Author)];
130+
let result = cascade(&sheets, div, &doc);
131+
assert_eq!(
132+
result.get(&PropertyId::Color),
133+
Some(&CssValue::Color(CssColor::rgb(255, 0, 0)))
134+
);
135+
}
136+
}

crates/ie-css/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@
33
//! CSS parser and style resolution engine.
44
//! Targets latest CSS spec only — no vendor prefixes, no legacy properties.
55
6+
pub mod cascade;
67
pub mod parser;
78
pub mod selector;
89
pub mod style;
910
pub mod tokenizer;
1011
pub mod values;
1112

13+
pub use cascade::cascade;
1214
pub use parser::{Declaration, Rule, Stylesheet, parse_declarations, parse_stylesheet};
15+
pub use selector::{
16+
Selector, Specificity, matches as selector_matches, parse_selector, parse_selector_list,
17+
specificity,
18+
};
1319
pub use style::ComputedStyle;
1420
pub use tokenizer::{CssToken, CssTokenizer};
1521
pub use values::{CssColor, CssValue, LengthUnit, PropertyId};

0 commit comments

Comments
 (0)