Skip to main content

aptu_coder_core/languages/
css.rs

1// SPDX-FileCopyrightText: 2026 aptu-coder contributors
2// SPDX-License-Identifier: Apache-2.0
3//! CSS language handler for tree-sitter-css.
4//!
5//! Extracts CSS rule sets (selectors) as function-equivalent elements and
6//! `@import` statements as imports.
7
8/// Tree-sitter query for extracting CSS rule sets (selectors) as elements.
9///
10/// Each `rule_set` node's `selectors` child is captured as the element name.
11pub const ELEMENT_QUERY: &str = r"
12(rule_set (selectors) @func_name) @function
13";
14
15/// Tree-sitter query for extracting CSS `@import` statements.
16///
17/// Captures the string value (URL) inside each `import_statement`.
18pub const IMPORT_QUERY: &str = r"
19(import_statement (string_value) @import_path)
20";
21
22/// Tree-sitter call query for CSS (empty -- no call sites in CSS).
23pub const CALL_QUERY: &str = "";
24
25#[cfg(all(test, feature = "lang-css"))]
26mod tests {
27    use tree_sitter::{Parser, StreamingIterator};
28
29    fn parse_and_query(src: &str, query_str: &str, capture_name: &str) -> Vec<String> {
30        let language = tree_sitter_css::LANGUAGE;
31        let mut parser = Parser::new();
32        parser
33            .set_language(&language.into())
34            .expect("failed to set language");
35        let tree = parser.parse(src, None).expect("parse failed");
36        let query = tree_sitter::Query::new(&language.into(), query_str).expect("invalid query");
37        let mut cursor = tree_sitter::QueryCursor::new();
38        let mut matches = cursor.matches(&query, tree.root_node(), src.as_bytes());
39        let capture_idx = query
40            .capture_index_for_name(capture_name)
41            .expect("capture not found");
42        let mut results = Vec::new();
43        while let Some(m) = matches.next() {
44            for cap in m.captures {
45                if cap.index == capture_idx {
46                    let text = &src[cap.node.start_byte()..cap.node.end_byte()];
47                    results.push(text.trim().to_owned());
48                }
49            }
50        }
51        results
52    }
53
54    /// CSS rule sets are extracted with the correct selector text.
55    #[test]
56    fn test_css_rule_set_element_extraction() {
57        let src = "body { color: red; }\n.container { margin: 0; }\n#header { font-size: 16px; }\n";
58        let names = parse_and_query(src, super::ELEMENT_QUERY, "func_name");
59        assert_eq!(names, vec!["body", ".container", "#header"]);
60    }
61
62    /// CSS @import statements are extracted as imports (string_value includes quotes).
63    #[test]
64    fn test_css_import_extraction() {
65        let src = "@import \"reset.css\";\n@import \"theme.css\";\nbody { color: red; }\n";
66        let imports = parse_and_query(src, super::IMPORT_QUERY, "import_path");
67        assert_eq!(imports, vec!["\"reset.css\"", "\"theme.css\""]);
68    }
69
70    /// An empty CSS file returns empty analysis without errors.
71    #[test]
72    fn test_css_empty_file() {
73        let names = parse_and_query("", super::ELEMENT_QUERY, "func_name");
74        assert!(names.is_empty());
75        let imports = parse_and_query("", super::IMPORT_QUERY, "import_path");
76        assert!(imports.is_empty());
77    }
78}