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