1use cssbox_core::style::ComputedStyle;
4
5use crate::css::{apply_declarations, parse_style_attribute, parse_stylesheet, CssRule};
6use crate::dom::{DomNodeId, DomTree};
7
8#[derive(Debug, Clone)]
10struct MatchedRule {
11 declarations: Vec<crate::css::CssDeclaration>,
12 specificity: Specificity,
13 source_order: usize,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18struct Specificity {
19 inline: bool,
20 id: u32,
21 class: u32,
22 type_sel: u32,
23}
24
25impl Specificity {
26 fn new(id: u32, class: u32, type_sel: u32) -> Self {
27 Self {
28 inline: false,
29 id,
30 class,
31 type_sel,
32 }
33 }
34}
35
36fn compute_specificity(selector: &str) -> Specificity {
38 let mut id_count = 0u32;
39 let mut class_count = 0u32;
40 let mut type_count = 0u32;
41
42 for part in selector.split(|c: char| c.is_whitespace() || c == '>' || c == '+' || c == '~') {
44 let part = part.trim();
45 if part.is_empty() {
46 continue;
47 }
48
49 for segment in split_selector_segments(part) {
50 if segment.starts_with('#') {
51 id_count += 1;
52 } else if segment.starts_with('.')
53 || segment.starts_with('[')
54 || segment.starts_with(':')
55 {
56 class_count += 1;
57 } else if segment == "*" {
58 } else {
60 type_count += 1;
61 }
62 }
63 }
64
65 Specificity::new(id_count, class_count, type_count)
66}
67
68fn split_selector_segments(selector: &str) -> Vec<&str> {
69 let mut segments = Vec::new();
70 let mut start = 0;
71 let bytes = selector.as_bytes();
72
73 for i in 1..bytes.len() {
74 if bytes[i] == b'#' || bytes[i] == b'.' || bytes[i] == b'[' || bytes[i] == b':' {
75 if start < i {
76 segments.push(&selector[start..i]);
77 }
78 start = i;
79 }
80 }
81 if start < selector.len() {
82 segments.push(&selector[start..]);
83 }
84
85 segments
86}
87
88fn selector_matches(tree: &DomTree, node: DomNodeId, selector: &str) -> bool {
90 let _dom_node = tree.node(node);
91
92 let parts: Vec<&str> = selector.split_whitespace().collect();
95
96 if parts.len() == 1 {
97 return simple_selector_matches(tree, node, parts[0]);
98 }
99
100 if parts.len() >= 2 {
102 let last = parts.last().unwrap();
103 if !simple_selector_matches(tree, node, last) {
104 return false;
105 }
106
107 let ancestor_selector = parts[..parts.len() - 1].join(" ");
109 let mut current = tree.parent(node);
110 while let Some(parent) = current {
111 if selector_matches(tree, parent, &ancestor_selector) {
112 return true;
113 }
114 current = tree.parent(parent);
115 }
116 return false;
117 }
118
119 false
120}
121
122fn simple_selector_matches(tree: &DomTree, node: DomNodeId, selector: &str) -> bool {
124 let dom_node = tree.node(node);
125
126 if selector == "*" {
127 return dom_node.is_element();
128 }
129
130 let segments = split_selector_segments(selector);
131
132 for segment in &segments {
133 if let Some(id) = segment.strip_prefix('#') {
134 match dom_node.get_attribute("id") {
136 Some(v) if v == id => {}
137 _ => return false,
138 }
139 } else if let Some(class) = segment.strip_prefix('.') {
140 match dom_node.get_attribute("class") {
142 Some(classes) => {
143 if !classes.split_whitespace().any(|c| c == class) {
144 return false;
145 }
146 }
147 None => return false,
148 }
149 } else if segment.starts_with('[') {
150 let inner = segment.trim_start_matches('[').trim_end_matches(']');
152 if let Some((attr, val)) = inner.split_once('=') {
153 let val = val.trim_matches('"').trim_matches('\'');
154 match dom_node.get_attribute(attr) {
155 Some(v) if v == val => {}
156 _ => return false,
157 }
158 } else if dom_node.get_attribute(inner).is_none() {
159 return false;
160 }
161 } else {
162 match dom_node.tag_name() {
164 Some(tag) if tag.eq_ignore_ascii_case(segment) => {}
165 _ => return false,
166 }
167 }
168 }
169
170 !segments.is_empty()
171}
172
173pub fn resolve_styles(tree: &DomTree, stylesheets: &[String]) -> Vec<(DomNodeId, ComputedStyle)> {
175 let mut rules: Vec<(CssRule, usize)> = Vec::new();
177 for (i, css) in stylesheets.iter().enumerate() {
178 for rule in parse_stylesheet(css) {
179 rules.push((rule, i));
180 }
181 }
182
183 let mut styles = Vec::new();
184
185 for node_id in tree.iter_dfs() {
186 let dom_node = tree.node(node_id);
187 if !dom_node.is_element() {
188 continue;
189 }
190
191 let mut style = default_style_for_tag(dom_node.tag_name().unwrap_or(""));
192
193 let mut matched: Vec<MatchedRule> = Vec::new();
195
196 for (order, (rule, _sheet_idx)) in rules.iter().enumerate() {
197 for selector in rule.selector.split(',') {
199 let selector = selector.trim();
200 if selector_matches(tree, node_id, selector) {
201 matched.push(MatchedRule {
202 declarations: rule.declarations.clone(),
203 specificity: compute_specificity(selector),
204 source_order: order,
205 });
206 }
207 }
208 }
209
210 matched.sort_by(|a, b| {
212 a.specificity
213 .cmp(&b.specificity)
214 .then(a.source_order.cmp(&b.source_order))
215 });
216
217 for rule in &matched {
219 apply_declarations(&mut style, &rule.declarations);
220 }
221
222 if let Some(inline_style) = dom_node.get_attribute("style") {
224 let inline_decls = parse_style_attribute(inline_style);
225 apply_declarations(&mut style, &inline_decls);
226 }
227
228 styles.push((node_id, style));
229 }
230
231 styles
232}
233
234fn default_style_for_tag(tag: &str) -> ComputedStyle {
236 use cssbox_core::style::Display;
237 use cssbox_core::values::LengthPercentageAuto;
238
239 let mut style = ComputedStyle::default();
240
241 match tag.to_lowercase().as_str() {
242 "html" | "body" | "div" | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol"
243 | "li" | "article" | "section" | "nav" | "aside" | "header" | "footer" | "main"
244 | "figure" | "figcaption" | "blockquote" | "pre" | "hr" | "form" | "fieldset"
245 | "legend" | "details" | "summary" | "dl" | "dt" | "dd" | "address" => {
246 style.display = Display::BLOCK;
247 }
248 "table" => {
249 style.display = Display::TABLE;
250 }
251 "tr" => {
252 style.display = Display::TABLE_ROW;
253 }
254 "td" | "th" => {
255 style.display = Display::TABLE_CELL;
256 }
257 "thead" => {
258 style.display = Display::TABLE_HEADER_GROUP;
259 }
260 "tbody" => {
261 style.display = Display::TABLE_ROW_GROUP;
262 }
263 "tfoot" => {
264 style.display = Display::TABLE_FOOTER_GROUP;
265 }
266 "colgroup" => {
267 style.display = Display::TABLE_COLUMN_GROUP;
268 }
269 "col" => {
270 style.display = Display::TABLE_COLUMN;
271 }
272 "caption" => {
273 style.display = Display::TABLE_CAPTION;
274 }
275 _ => {
276 style.display = Display::INLINE;
277 }
278 }
279
280 match tag.to_lowercase().as_str() {
282 "body" => {
283 style.margin_top = LengthPercentageAuto::px(8.0);
284 style.margin_right = LengthPercentageAuto::px(8.0);
285 style.margin_bottom = LengthPercentageAuto::px(8.0);
286 style.margin_left = LengthPercentageAuto::px(8.0);
287 }
288 "p" | "blockquote" | "figure" | "ul" | "ol" | "dl" => {
289 style.margin_top = LengthPercentageAuto::px(16.0);
290 style.margin_bottom = LengthPercentageAuto::px(16.0);
291 }
292 "h1" => {
293 style.margin_top = LengthPercentageAuto::px(21.44);
294 style.margin_bottom = LengthPercentageAuto::px(21.44);
295 }
296 _ => {}
297 }
298
299 style
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_specificity_calculation() {
308 assert!(compute_specificity("#id") > compute_specificity(".class"));
309 assert!(compute_specificity(".class") > compute_specificity("div"));
310 assert!(compute_specificity("div.class") > compute_specificity("div"));
311 }
312
313 #[test]
314 fn test_simple_selector_matching() {
315 let mut tree = DomTree::new();
316 let root = tree.root();
317 let mut attrs = std::collections::HashMap::new();
318 attrs.insert("id".to_string(), "test".to_string());
319 attrs.insert("class".to_string(), "box red".to_string());
320 let div = tree.add_element(root, "div", attrs);
321
322 assert!(simple_selector_matches(&tree, div, "div"));
323 assert!(simple_selector_matches(&tree, div, "#test"));
324 assert!(simple_selector_matches(&tree, div, ".box"));
325 assert!(simple_selector_matches(&tree, div, ".red"));
326 assert!(simple_selector_matches(&tree, div, "div.box"));
327 assert!(!simple_selector_matches(&tree, div, "span"));
328 assert!(!simple_selector_matches(&tree, div, ".missing"));
329 }
330
331 #[test]
332 fn test_default_style_for_div() {
333 let style = default_style_for_tag("div");
334 assert_eq!(style.display, cssbox_core::style::Display::BLOCK);
335 }
336}