cadi_core/atomizer/languages/
css.rs1use crate::atomizer::{AtomizerConfig, ExtractedAtom, AtomKind};
4use crate::error::CadiResult;
5
6pub struct CssAtomizer {
8 _config: AtomizerConfig,
9}
10
11impl CssAtomizer {
12 pub fn new(config: AtomizerConfig) -> Self {
13 Self { _config: config }
14 }
15
16 #[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 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 #[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 assert!(atoms.iter().any(|a| a.name == ".header"));
133 assert!(atoms.iter().any(|a| a.name.contains("@media")));
134 }
135}