Skip to main content

normalize_languages/
css.rs

1//! CSS language support with symbol extraction.
2//!
3//! CSS symbols: rule_set (selectors → Class), media/supports/keyframes → Module,
4//! declarations → Variable. Nested rule_sets inside at-rules become children.
5
6use crate::{Language, LanguageSymbols};
7use tree_sitter::Node;
8
9/// CSS language support.
10pub struct Css;
11
12impl Language for Css {
13    fn name(&self) -> &'static str {
14        "CSS"
15    }
16    fn extensions(&self) -> &'static [&'static str] {
17        &["css"]
18    }
19    fn grammar_name(&self) -> &'static str {
20        "css"
21    }
22
23    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
24        Some(self)
25    }
26
27    fn refine_kind(
28        &self,
29        node: &Node,
30        _content: &str,
31        tag_kind: crate::SymbolKind,
32    ) -> crate::SymbolKind {
33        match node.kind() {
34            // At-rules containing blocks are containers
35            "media_statement" | "supports_statement" | "keyframes_statement" => {
36                crate::SymbolKind::Module
37            }
38            _ => tag_kind,
39        }
40    }
41
42    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
43        match node.kind() {
44            "rule_set" => {
45                // Extract the selectors text
46                let mut cursor = node.walk();
47                for child in node.children(&mut cursor) {
48                    if child.kind() == "selectors" {
49                        return Some(content[child.byte_range()].trim());
50                    }
51                }
52                None
53            }
54            "media_statement" => {
55                // Extract feature_query or keyword after @media
56                extract_at_rule_name(node, content, "@media")
57            }
58            "supports_statement" => extract_at_rule_name(node, content, "@supports"),
59            "keyframes_statement" => {
60                // keyframes_name child
61                let mut cursor = node.walk();
62                for child in node.children(&mut cursor) {
63                    if child.kind() == "keyframes_name" {
64                        return Some(content[child.byte_range()].trim());
65                    }
66                }
67                None
68            }
69            "declaration" => {
70                // property_name child
71                let mut cursor = node.walk();
72                for child in node.children(&mut cursor) {
73                    if child.kind() == "property_name" {
74                        return Some(content[child.byte_range()].trim());
75                    }
76                }
77                None
78            }
79            _ => None,
80        }
81    }
82
83    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
84        match node.kind() {
85            "rule_set" | "media_statement" | "supports_statement" | "keyframes_statement" => {
86                let mut cursor = node.walk();
87                for child in node.children(&mut cursor) {
88                    if child.kind() == "block" || child.kind() == "keyframe_block_list" {
89                        return Some(child);
90                    }
91                }
92                None
93            }
94            _ => None,
95        }
96    }
97
98    fn build_signature(&self, node: &Node, content: &str) -> String {
99        if let Some(name) = self.node_name(node, content) {
100            match node.kind() {
101                "rule_set" => format!("{} {{ … }}", name),
102                "media_statement" => format!("@media {} {{ … }}", name),
103                "supports_statement" => format!("@supports {} {{ … }}", name),
104                "keyframes_statement" => format!("@keyframes {} {{ … }}", name),
105                "declaration" => {
106                    // Find value after property_name
107                    let mut cursor = node.walk();
108                    let mut found_name = false;
109                    for child in node.children(&mut cursor) {
110                        if child.kind() == "property_name" {
111                            found_name = true;
112                        } else if found_name && child.kind() != ":" && child.kind() != ";" {
113                            let val = content[child.byte_range()].trim();
114                            if val.len() > 40 {
115                                return format!("{}: {}…", name, &val[..37]);
116                            }
117                            return format!("{}: {}", name, val);
118                        }
119                    }
120                    name.to_string()
121                }
122                _ => name.to_string(),
123            }
124        } else {
125            content[node.byte_range()]
126                .lines()
127                .next()
128                .unwrap_or("")
129                .trim()
130                .to_string()
131        }
132    }
133}
134
135impl LanguageSymbols for Css {}
136
137/// Extract the text between an at-rule keyword and its block.
138fn extract_at_rule_name<'a>(node: &Node, content: &'a str, keyword: &str) -> Option<&'a str> {
139    let full = &content[node.byte_range()];
140    let after_keyword = full.strip_prefix(keyword)?.trim_start();
141    // Take everything up to the opening brace
142    let name = after_keyword.split('{').next()?.trim();
143    if name.is_empty() {
144        return None;
145    }
146    // Find the offset within the node and return a reference into content
147    let start = node.start_byte() + full.find(name)?;
148    let end = start + name.len();
149    Some(&content[start..end])
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::validate_unused_kinds_audit;
156
157    #[test]
158    fn unused_node_kinds_audit() {
159        #[rustfmt::skip]
160        let documented_unused: &[&str] = &[
161            "binary_expression", "block", "call_expression", "charset_statement",
162            "class_name", "class_selector",
163            "function_name",
164            "identifier", "import_statement", "important", "important_value",
165            "keyframe_block", "keyframe_block_list",
166            "namespace_statement", "postcss_statement",
167            "pseudo_class_selector", "scope_statement",
168        ];
169        validate_unused_kinds_audit(&Css, documented_unused)
170            .expect("CSS unused node kinds audit failed");
171    }
172}