1use regex::Regex;
2use std::sync::LazyLock;
3
4use crate::dom_tree::DomTree;
5use crate::types::{CssVariable, DOMNodeInfo};
6
7static PSEUDO_ELEMENT_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"::[a-zA-Z-]+").unwrap());
9static ID_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"#[a-zA-Z0-9_-]+").unwrap());
10static CLASS_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\.[a-zA-Z0-9_-]+").unwrap());
11static ATTR_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"\[[^\]'"']*\]"#).unwrap());
12static NOT_RE: LazyLock<Regex> =
13 LazyLock::new(|| Regex::new(r":not\((?:[^()]|\([^)]*\))+\)").unwrap());
14static IS_RE: LazyLock<Regex> =
15 LazyLock::new(|| Regex::new(r":is\((?:[^()]|\([^)]*\))+\)").unwrap());
16static WHERE_RE: LazyLock<Regex> =
17 LazyLock::new(|| Regex::new(r":where\((?:[^()]|\([^)]*\))+\)").unwrap());
18static PSEUDO_CLASS_RE: LazyLock<Regex> =
19 LazyLock::new(|| Regex::new(r":[a-zA-Z-]+(\([^)]*\))?").unwrap());
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct Specificity {
23 pub ids: u32,
24 pub classes: u32,
25 pub elements: u32,
26}
27
28impl Specificity {
29 pub fn new(ids: u32, classes: u32, elements: u32) -> Self {
30 Self {
31 ids,
32 classes,
33 elements,
34 }
35 }
36}
37
38fn extract_pseudo_arg<'a>(selector: &'a str, prefix: &[u8]) -> Option<&'a str> {
48 let bytes = selector.as_bytes();
49 let len = bytes.len();
50 let prefix_len = prefix.len();
51
52 for i in 0..len {
53 if bytes[i] != b':' || i + prefix_len > len {
54 continue;
55 }
56 let mut match_prefix = true;
57 for j in 0..prefix_len {
58 if bytes[i + j] != prefix[j] {
59 match_prefix = false;
60 break;
61 }
62 }
63 if !match_prefix {
64 continue;
65 }
66
67 let mut depth = 1;
69 let mut pos = i + prefix_len;
70 while pos < len {
71 match bytes[pos] {
72 b'(' => {
73 depth += 1;
74 }
75 b')' => {
76 depth -= 1;
77 if depth == 0 {
78 return Some(&selector[i + prefix_len..pos]);
79 }
80 }
81 _ => {}
82 }
83 pos += 1;
84 }
85 return None;
86 }
87 None
88}
89
90fn extract_is_arg(selector: &str) -> Option<&str> {
92 extract_pseudo_arg(selector, b":is(")
93}
94
95fn extract_not_arg(selector: &str) -> Option<&str> {
97 extract_pseudo_arg(selector, b":not(")
98}
99
100fn specificity_of_pseudo_argument(arg: &str) -> Specificity {
103 let parts: Vec<&str> = arg
104 .split(',')
105 .map(|s| s.trim())
106 .filter(|s| !s.is_empty())
107 .collect();
108 if parts.is_empty() {
109 return Specificity::new(0, 0, 0);
110 }
111 let mut max_ids: u32 = 0;
112 let mut max_classes: u32 = 0;
113 let mut max_elements: u32 = 0;
114 for part in parts {
115 let spec = specificity_of_selector_part(part);
116 max_ids = max_ids.max(spec.ids);
117 max_classes = max_classes.max(spec.classes);
118 max_elements = max_elements.max(spec.elements);
119 }
120 Specificity::new(max_ids, max_classes, max_elements)
121}
122
123fn specificity_of_selector_part(selector: &str) -> Specificity {
126 let selector = selector.trim();
127 if selector.is_empty() || selector == "*" {
128 return Specificity::new(0, 0, 0);
129 }
130 let mut working = selector.to_string();
131
132 let pseudo_elements = PSEUDO_ELEMENT_RE.find_iter(&working).count() as u32;
133 working = PSEUDO_ELEMENT_RE.replace_all(&working, "").to_string();
134
135 let not_arg = extract_not_arg(selector);
137 let (extra_ids, extra_classes, extra_elements) = if let Some(arg) = not_arg {
138 let spec = specificity_of_pseudo_argument(arg);
139 (spec.ids, spec.classes, spec.elements)
140 } else {
141 (0, 0, 0)
142 };
143 working = NOT_RE.replace_all(&working, "").to_string();
145
146 let is_arg = extract_is_arg(selector);
149 let (is_ids, is_classes, is_elements) = if let Some(arg) = is_arg {
150 let spec = specificity_of_pseudo_argument(arg);
151 (spec.ids, spec.classes, spec.elements)
152 } else {
153 (0, 0, 0)
154 };
155 working = IS_RE.replace_all(&working, "").to_string();
157
158 working = WHERE_RE.replace_all(&working, "").to_string();
161
162 let ids = ID_RE.find_iter(&working).count() as u32;
163 working = ID_RE.replace_all(&working, "").to_string();
164
165 let classes = CLASS_RE.find_iter(&working).count() as u32;
166 working = CLASS_RE.replace_all(&working, "").to_string();
167
168 working = ATTR_RE.replace_all(&working, "").to_string();
169
170 working = PSEUDO_CLASS_RE.replace_all(&working, "").to_string();
171
172 let mut elements = pseudo_elements + extra_elements + is_elements;
173 working = working.replace(['>', '+', '~', ' '], " ");
174 for part in working.split_whitespace() {
175 if !part.is_empty() && part != "*" {
176 elements += 1;
177 }
178 }
179 Specificity::new(
180 ids + extra_ids + is_ids,
181 classes + extra_classes + is_classes,
182 elements,
183 )
184}
185
186pub fn calculate_specificity(selector: &str) -> Specificity {
187 let selector = selector.trim();
188 if selector.is_empty() || selector == "*" {
189 return Specificity::new(0, 0, 0);
190 }
191
192 let selectors: Vec<&str> = selector
193 .split(',')
194 .map(|s| s.trim())
195 .filter(|s| !s.is_empty())
196 .collect();
197 if selectors.len() > 1
201 && !selector.contains(":not(")
202 && !selector.contains(":is(")
203 && !selector.contains(":where(")
204 {
205 let mut best = Specificity::new(0, 0, 0);
206 for sel in selectors {
207 let spec = calculate_specificity(sel);
208 if compare_specificity(spec, best) > 0 {
209 best = spec;
210 }
211 }
212 return best;
213 }
214
215 let mut working = selector.to_string();
216
217 let pseudo_elements = PSEUDO_ELEMENT_RE.find_iter(&working).count() as u32;
218 working = PSEUDO_ELEMENT_RE.replace_all(&working, "").to_string();
219
220 let not_arg = extract_not_arg(selector);
223 let (not_ids, not_classes, not_elements) = if let Some(arg) = not_arg {
224 let spec = specificity_of_pseudo_argument(arg);
225 (spec.ids, spec.classes, spec.elements)
226 } else {
227 (0, 0, 0)
228 };
229 working = NOT_RE.replace_all(&working, "").to_string();
232
233 let is_arg = if not_arg.is_none() {
237 extract_is_arg(selector)
238 } else {
239 None
240 };
241 let (is_ids, is_classes, is_elements) = if let Some(arg) = is_arg {
242 let spec = specificity_of_pseudo_argument(arg);
243 (spec.ids, spec.classes, spec.elements)
244 } else {
245 (0, 0, 0)
246 };
247 working = IS_RE.replace_all(&working, "").to_string();
249
250 working = WHERE_RE.replace_all(&working, "").to_string();
253
254 let ids = ID_RE.find_iter(&working).count() as u32;
255 working = ID_RE.replace_all(&working, "").to_string();
256
257 let classes = CLASS_RE.find_iter(&working).count() as u32;
258 working = CLASS_RE.replace_all(&working, "").to_string();
259
260 let attrs = ATTR_RE.find_iter(&working).count() as u32;
261 working = ATTR_RE.replace_all(&working, "").to_string();
262
263 let pseudo_classes = PSEUDO_CLASS_RE.find_iter(&working).count() as u32;
264 working = PSEUDO_CLASS_RE.replace_all(&working, "").to_string();
265
266 let mut elements = pseudo_elements;
267 working = working.replace(['>', '+', '~', ' '], " ");
268 for part in working.split_whitespace() {
269 if !part.is_empty() && part != "*" {
270 elements += 1;
271 }
272 }
273
274 Specificity::new(
275 ids + not_ids + is_ids,
276 classes + attrs + pseudo_classes + not_classes + is_classes,
277 elements + not_elements + is_elements,
278 )
279}
280
281pub fn compare_specificity(a: Specificity, b: Specificity) -> i32 {
282 if a.ids != b.ids {
283 return if a.ids > b.ids { 1 } else { -1 };
284 }
285 if a.classes != b.classes {
286 return if a.classes > b.classes { 1 } else { -1 };
287 }
288 if a.elements != b.elements {
289 return if a.elements > b.elements { 1 } else { -1 };
290 }
291 0
292}
293
294pub fn format_specificity(spec: Specificity) -> String {
295 format!("({},{},{})", spec.ids, spec.classes, spec.elements)
296}
297
298pub fn matches_context(
299 definition_selector: &str,
300 usage_context: &str,
301 dom_tree: Option<&DomTree>,
302 dom_node: Option<&DOMNodeInfo>,
303) -> bool {
304 if let (Some(tree), Some(node)) = (dom_tree, dom_node) {
305 if let Some(node_index) = node.node_index {
306 return tree.matches_selector(node_index, definition_selector);
307 }
308 }
309
310 let def_trim = definition_selector.trim();
311 let usage_trim = usage_context.trim();
312
313 if def_trim == ":root" {
314 return true;
315 }
316
317 if def_trim == usage_trim {
318 return true;
319 }
320
321 let def_parts: Vec<&str> = def_trim.split(&[' ', '>', '+', '~'][..]).collect();
322 let usage_parts: Vec<&str> = usage_trim.split(&[' ', '>', '+', '~'][..]).collect();
323
324 def_parts.iter().any(|def_part| {
325 usage_parts.iter().any(|usage_part| {
326 !def_part.is_empty()
327 && !usage_part.is_empty()
328 && (usage_part.contains(def_part) || def_part.contains(usage_part))
329 })
330 })
331}
332
333pub fn sort_by_cascade(variables: &mut [CssVariable]) {
336 variables.sort_by(|a, b| {
337 if a.important != b.important {
338 return if a.important {
339 std::cmp::Ordering::Less
340 } else {
341 std::cmp::Ordering::Greater
342 };
343 }
344
345 if a.inline != b.inline {
346 return if a.inline {
347 std::cmp::Ordering::Less
348 } else {
349 std::cmp::Ordering::Greater
350 };
351 }
352
353 let spec_a = calculate_specificity(&a.selector);
354 let spec_b = calculate_specificity(&b.selector);
355 let spec_cmp = compare_specificity(spec_a, spec_b);
356 if spec_cmp != 0 {
357 return if spec_cmp > 0 {
358 std::cmp::Ordering::Less
359 } else {
360 std::cmp::Ordering::Greater
361 };
362 }
363
364 b.source_position.cmp(&a.source_position)
365 });
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn basic_specificity_calculation() {
374 let root = calculate_specificity(":root");
375 assert_eq!(root.ids, 0);
376 assert_eq!(root.classes, 1);
377 assert_eq!(root.elements, 0);
378 assert_eq!(format_specificity(root), "(0,1,0)");
379 }
380
381 #[test]
382 fn element_selector_specificity() {
383 let div = calculate_specificity("div");
384 assert_eq!(div.ids, 0);
385 assert_eq!(div.classes, 0);
386 assert_eq!(div.elements, 1);
387 }
388
389 #[test]
390 fn class_selector_specificity() {
391 let class = calculate_specificity(".button");
392 assert_eq!(class.ids, 0);
393 assert_eq!(class.classes, 1);
394 assert_eq!(class.elements, 0);
395 }
396
397 #[test]
398 fn id_selector_specificity() {
399 let id = calculate_specificity("#main");
400 assert_eq!(id.ids, 1);
401 assert_eq!(id.classes, 0);
402 assert_eq!(id.elements, 0);
403 }
404
405 #[test]
406 fn complex_selector_specificity() {
407 let spec = calculate_specificity("div.button#submit");
408 assert_eq!(spec.ids, 1);
409 assert_eq!(spec.classes, 1);
410 assert_eq!(spec.elements, 1);
411 }
412
413 #[test]
414 fn specificity_comparison() {
415 let root = calculate_specificity(":root");
416 let div = calculate_specificity("div");
417 let cls = calculate_specificity(".button");
418 let id = calculate_specificity("#main");
419
420 assert_eq!(compare_specificity(div, root), -1);
421 assert_eq!(compare_specificity(cls, div), 1);
422 assert_eq!(compare_specificity(id, cls), 1);
423 assert_eq!(compare_specificity(root, root), 0);
424 }
425
426 #[test]
427 fn context_matching_basics() {
428 assert!(matches_context(":root", "div", None, None));
429 assert!(matches_context("div", "div", None, None));
430 assert!(matches_context(":root", ".button", None, None));
431 }
432
433 #[test]
434 fn not_with_class_specificity() {
435 let spec = calculate_specificity(":not(.foo)");
437 assert_eq!(spec.ids, 0);
438 assert_eq!(spec.classes, 1);
439 assert_eq!(spec.elements, 0);
440 }
441
442 #[test]
443 fn not_with_id_specificity() {
444 let spec = calculate_specificity(":not(#bar)");
446 assert_eq!(spec.ids, 1);
447 assert_eq!(spec.classes, 0);
448 assert_eq!(spec.elements, 0);
449 }
450
451 #[test]
452 fn not_with_multiple_args_specificity() {
453 let spec = calculate_specificity(":not(.foo, #bar)");
455 assert_eq!(spec.ids, 1);
456 assert_eq!(spec.classes, 1);
457 assert_eq!(spec.elements, 0);
458 }
459
460 #[test]
461 fn not_with_complex_selector_specificity() {
462 let spec = calculate_specificity(":not(.foo.bar)");
464 assert_eq!(spec.ids, 0);
465 assert_eq!(spec.classes, 2);
466 assert_eq!(spec.elements, 0);
467 }
468
469 #[test]
470 fn not_with_element_specificity() {
471 let spec = calculate_specificity(":not(div)");
473 assert_eq!(spec.ids, 0);
474 assert_eq!(spec.classes, 0);
475 assert_eq!(spec.elements, 1);
476 }
477
478 #[test]
479 fn not_preserves_other_selectors() {
480 let spec = calculate_specificity(".foo:not(#bar)");
482 assert_eq!(spec.ids, 1);
483 assert_eq!(spec.classes, 1);
484 assert_eq!(spec.elements, 0);
485 }
486
487 #[test]
488 fn not_nested_is_specificity() {
489 let spec = calculate_specificity(":not(:is(.foo))");
493 assert_eq!(spec.ids, 0);
494 assert_eq!(spec.classes, 1);
495 assert_eq!(spec.elements, 0);
496 }
497
498 #[test]
499 fn where_zero_specificity() {
500 let spec = calculate_specificity(":where(.foo)");
502 assert_eq!(spec.ids, 0);
503 assert_eq!(spec.classes, 0);
504 assert_eq!(spec.elements, 0);
505 }
506
507 #[test]
508 fn where_nested_in_not() {
509 let spec = calculate_specificity(":not(:where(.foo))");
511 assert_eq!(spec.ids, 0);
512 assert_eq!(spec.classes, 0);
513 assert_eq!(spec.elements, 0);
514 }
515
516 #[test]
517 fn is_with_class_specificity() {
518 let spec = calculate_specificity(":is(.foo)");
520 assert_eq!(spec.ids, 0);
521 assert_eq!(spec.classes, 1);
522 assert_eq!(spec.elements, 0);
523 }
524
525 #[test]
526 fn is_with_id_specificity() {
527 let spec = calculate_specificity(":is(#bar)");
529 assert_eq!(spec.ids, 1);
530 assert_eq!(spec.classes, 0);
531 assert_eq!(spec.elements, 0);
532 }
533
534 #[test]
535 fn is_with_multiple_args_specificity() {
536 let spec = calculate_specificity(":is(.foo, #bar)");
538 assert_eq!(spec.ids, 1);
539 assert_eq!(spec.classes, 1);
540 assert_eq!(spec.elements, 0);
541 }
542}