1use regex::Regex;
2
3use crate::dom_tree::DomTree;
4use crate::types::{CssVariable, DOMNodeInfo};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct Specificity {
8 pub ids: u32,
9 pub classes: u32,
10 pub elements: u32,
11}
12
13impl Specificity {
14 pub fn new(ids: u32, classes: u32, elements: u32) -> Self {
15 Self {
16 ids,
17 classes,
18 elements,
19 }
20 }
21}
22
23pub fn calculate_specificity(selector: &str) -> Specificity {
24 let selector = selector.trim();
25 if selector.is_empty() || selector == "*" {
26 return Specificity::new(0, 0, 0);
27 }
28
29 let selectors: Vec<&str> = selector
30 .split(',')
31 .map(|s| s.trim())
32 .filter(|s| !s.is_empty())
33 .collect();
34 if selectors.len() > 1 {
35 let mut best = Specificity::new(0, 0, 0);
36 for sel in selectors {
37 let spec = calculate_specificity(sel);
38 if compare_specificity(spec, best) > 0 {
39 best = spec;
40 }
41 }
42 return best;
43 }
44
45 let mut working = selector.to_string();
46 let pseudo_element_re = Regex::new(r"::[a-zA-Z-]+").unwrap();
47 let pseudo_elements = pseudo_element_re.find_iter(&working).count() as u32;
48 working = pseudo_element_re.replace_all(&working, "").to_string();
49
50 let id_re = Regex::new(r"#[a-zA-Z0-9_-]+").unwrap();
51 let ids = id_re.find_iter(&working).count() as u32;
52 working = id_re.replace_all(&working, "").to_string();
53
54 let class_re = Regex::new(r"\.[a-zA-Z0-9_-]+").unwrap();
55 let classes = class_re.find_iter(&working).count() as u32;
56 working = class_re.replace_all(&working, "").to_string();
57
58 let attr_re = Regex::new(r#"\[(?:[^\]"']|"[^"]*"|'[^']*')*\]"#).unwrap();
59 let attrs = attr_re.find_iter(&working).count() as u32;
60 working = attr_re.replace_all(&working, "").to_string();
61
62 let pseudo_class_re = Regex::new(r":[a-zA-Z-]+(\([^)]*\))?").unwrap();
63 let pseudo_classes = pseudo_class_re.find_iter(&working).count() as u32;
64 working = pseudo_class_re.replace_all(&working, "").to_string();
65
66 let mut elements = pseudo_elements;
67 working = working.replace(['>', '+', '~', ' '], " ");
68 for part in working.split_whitespace() {
69 if !part.is_empty() && part != "*" {
70 elements += 1;
71 }
72 }
73
74 Specificity::new(ids, classes + attrs + pseudo_classes, elements)
75}
76
77pub fn compare_specificity(a: Specificity, b: Specificity) -> i32 {
78 if a.ids != b.ids {
79 return if a.ids > b.ids { 1 } else { -1 };
80 }
81 if a.classes != b.classes {
82 return if a.classes > b.classes { 1 } else { -1 };
83 }
84 if a.elements != b.elements {
85 return if a.elements > b.elements { 1 } else { -1 };
86 }
87 0
88}
89
90pub fn format_specificity(spec: Specificity) -> String {
91 format!("({},{},{})", spec.ids, spec.classes, spec.elements)
92}
93
94pub fn matches_context(
95 definition_selector: &str,
96 usage_context: &str,
97 dom_tree: Option<&DomTree>,
98 dom_node: Option<&DOMNodeInfo>,
99) -> bool {
100 if let (Some(tree), Some(node)) = (dom_tree, dom_node) {
101 if let Some(node_index) = node.node_index {
102 return tree.matches_selector(node_index, definition_selector);
103 }
104 }
105
106 let def_trim = definition_selector.trim();
107 let usage_trim = usage_context.trim();
108
109 if def_trim == ":root" {
110 return true;
111 }
112
113 if def_trim == usage_trim {
114 return true;
115 }
116
117 let def_parts: Vec<&str> = def_trim.split(&[' ', '>', '+', '~'][..]).collect();
118 let usage_parts: Vec<&str> = usage_trim.split(&[' ', '>', '+', '~'][..]).collect();
119
120 def_parts.iter().any(|def_part| {
121 usage_parts.iter().any(|usage_part| {
122 !def_part.is_empty()
123 && !usage_part.is_empty()
124 && (usage_part.contains(def_part) || def_part.contains(usage_part))
125 })
126 })
127}
128
129pub fn sort_by_cascade(variables: &mut [CssVariable]) {
132 variables.sort_by(|a, b| {
133 if a.important != b.important {
134 return if a.important {
135 std::cmp::Ordering::Less
136 } else {
137 std::cmp::Ordering::Greater
138 };
139 }
140
141 if a.inline != b.inline {
142 return if a.inline {
143 std::cmp::Ordering::Less
144 } else {
145 std::cmp::Ordering::Greater
146 };
147 }
148
149 let spec_a = calculate_specificity(&a.selector);
150 let spec_b = calculate_specificity(&b.selector);
151 let spec_cmp = compare_specificity(spec_a, spec_b);
152 if spec_cmp != 0 {
153 return if spec_cmp > 0 {
154 std::cmp::Ordering::Less
155 } else {
156 std::cmp::Ordering::Greater
157 };
158 }
159
160 b.source_position.cmp(&a.source_position)
161 });
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn basic_specificity_calculation() {
170 let root = calculate_specificity(":root");
171 assert_eq!(root.ids, 0);
172 assert_eq!(root.classes, 1);
173 assert_eq!(root.elements, 0);
174 assert_eq!(format_specificity(root), "(0,1,0)");
175 }
176
177 #[test]
178 fn element_selector_specificity() {
179 let div = calculate_specificity("div");
180 assert_eq!(div.ids, 0);
181 assert_eq!(div.classes, 0);
182 assert_eq!(div.elements, 1);
183 }
184
185 #[test]
186 fn class_selector_specificity() {
187 let class = calculate_specificity(".button");
188 assert_eq!(class.ids, 0);
189 assert_eq!(class.classes, 1);
190 assert_eq!(class.elements, 0);
191 }
192
193 #[test]
194 fn id_selector_specificity() {
195 let id = calculate_specificity("#main");
196 assert_eq!(id.ids, 1);
197 assert_eq!(id.classes, 0);
198 assert_eq!(id.elements, 0);
199 }
200
201 #[test]
202 fn complex_selector_specificity() {
203 let spec = calculate_specificity("div.button#submit");
204 assert_eq!(spec.ids, 1);
205 assert_eq!(spec.classes, 1);
206 assert_eq!(spec.elements, 1);
207 }
208
209 #[test]
210 fn specificity_comparison() {
211 let root = calculate_specificity(":root");
212 let div = calculate_specificity("div");
213 let cls = calculate_specificity(".button");
214 let id = calculate_specificity("#main");
215
216 assert_eq!(compare_specificity(div, root), -1);
217 assert_eq!(compare_specificity(cls, div), 1);
218 assert_eq!(compare_specificity(id, cls), 1);
219 assert_eq!(compare_specificity(root, root), 0);
220 }
221
222 #[test]
223 fn context_matching_basics() {
224 assert!(matches_context(":root", "div", None, None));
225 assert!(matches_context("div", "div", None, None));
226 assert!(matches_context(":root", ".button", None, None));
227 }
228}