css_variable_lsp/
specificity.rs

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
129/// Sort variables by cascade rules (winner first):
130/// !important > inline > specificity > source order (later wins)
131pub 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}