cadi_core/atomizer/languages/
css.rs

1//! CSS-specific atomizer
2
3use crate::atomizer::{AtomizerConfig, ExtractedAtom, AtomKind};
4use crate::error::CadiResult;
5
6/// CSS-specific atomizer with Tree-sitter support
7pub struct CssAtomizer {
8    _config: AtomizerConfig,
9}
10
11impl CssAtomizer {
12    pub fn new(config: AtomizerConfig) -> Self {
13        Self { _config: config }
14    }
15
16    /// Extract atoms using Tree-sitter (when feature enabled)
17    #[cfg(feature = "ast-parsing")]
18    pub fn extract(&self, source: &str) -> CadiResult<Vec<ExtractedAtom>> {
19        use tree_sitter::{Parser, Query, QueryCursor};
20        
21        let mut parser = Parser::new();
22        parser.set_language(&tree_sitter_css::language())?;
23        
24        let tree = parser.parse(source, None)
25            .ok_or_else(|| crate::error::CadiError::AtomizerError("Parse failed".into()))?;
26        
27        let mut atoms = Vec::new();
28        
29        // Tree-sitter queries for CSS
30        let query_src = r#"
31            (rule_set
32                (selectors) @selector
33            ) @rule
34            
35            (media_statement) @media
36            
37            (keyframe_block_list) @keyframes
38        "#;
39        
40        let query = Query::new(&tree_sitter_css::language(), query_src)?;
41        let mut cursor = QueryCursor::new();
42        
43        let matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
44        
45        for m in matches {
46            let mut name = "rule".to_string();
47            let mut kind = AtomKind::Constant;
48            let mut atom_node = None;
49
50            for capture in m.captures {
51                let capture_name = query.capture_names()[capture.index as usize];
52                match capture_name {
53                    "selector" => {
54                        name = capture.node.utf8_text(source.as_bytes()).unwrap_or("rule").trim().to_string();
55                    }
56                    "rule" => {
57                        kind = AtomKind::Constant;
58                        atom_node = Some(capture.node);
59                    }
60                    "media" => {
61                        name = capture.node.child(0).map(|n| n.utf8_text(source.as_bytes()).unwrap_or("@media").to_string()).unwrap_or("@media".to_string());
62                        kind = AtomKind::Constant;
63                        atom_node = Some(capture.node);
64                    }
65                    "keyframes" => {
66                        name = "@keyframes".to_string();
67                        kind = AtomKind::Constant;
68                        atom_node = Some(capture.node);
69                    }
70                    _ => {}
71                }
72            }
73
74            if let Some(node) = atom_node {
75                let start_byte = node.start_byte();
76                let end_byte = node.end_byte();
77                let start_point = node.start_position();
78                let end_point = node.end_position();
79
80                atoms.push(ExtractedAtom {
81                    name,
82                    kind,
83                    source: source[start_byte..end_byte].to_string(),
84                    start_byte,
85                    end_byte,
86                    start_line: start_point.row + 1,
87                    end_line: end_point.row + 1,
88                    defines: vec![],
89                    references: Vec::new(),
90                    doc_comment: None,
91                    visibility: crate::atomizer::extractor::Visibility::Public,
92                    parent: None,
93                    decorators: Vec::new(),
94                });
95            }
96        }
97        
98        Ok(atoms)
99    }
100    
101    /// Fallback extraction without Tree-sitter
102    #[cfg(not(feature = "ast-parsing"))]
103    pub fn extract(&self, source: &str) -> CadiResult<Vec<ExtractedAtom>> {
104        use crate::atomizer::AtomExtractor;
105        AtomExtractor::new("css", self._config.clone()).extract(source)
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::atomizer::AtomizerConfig;
113
114    #[test]
115    fn test_css_extraction() {
116        let source = r#"
117            .header {
118                color: red;
119            }
120
121            @media (max-width: 600px) {
122                body {
123                    background: blue;
124                }
125            }
126        "#;
127
128        let atomizer = CssAtomizer::new(AtomizerConfig::default());
129        let atoms = atomizer.extract(source).unwrap();
130
131        // Should find .header and @media
132        assert!(atoms.iter().any(|a| a.name == ".header"));
133        assert!(atoms.iter().any(|a| a.name.contains("@media")));
134    }
135}