cadi_core/atomizer/languages/
jsx.rs1use crate::atomizer::{AtomizerConfig, ExtractedAtom, AtomKind};
4use crate::error::CadiResult;
5
6pub struct JSXAtomizer {
8 _config: AtomizerConfig,
9}
10
11impl JSXAtomizer {
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_javascript::language())?;
23 let tree = parser
24 .parse(source, None)
25 .ok_or_else(|| crate::error::CadiError::AtomizerError("Parse failed".into()))?;
26
27 let query_src = r#"
28 (function_declaration
29 name: (identifier) @fn_name
30 ) @function
31
32 (lexical_declaration
33 (variable_declarator
34 name: (identifier) @var_name
35 value: (arrow_function) @arrow_fn
36 )
37 ) @var_fn
38
39 (export_statement (function_declaration name: (identifier) @exported_fn_name)) @exported_function
40
41 (class_declaration
42 name: (identifier) @class_name
43 ) @class
44
45 (jsx_element) @jsx
46 "#;
47
48 let query = Query::new(&tree_sitter_javascript::language(), query_src)?;
49 let mut cursor = QueryCursor::new();
50
51 let mut atoms = Vec::new();
52
53 for m in cursor.matches(&query, tree.root_node(), source.as_bytes()) {
54 let mut name = "unknown".to_string();
55 let mut kind = AtomKind::Function;
56 let mut atom_node = None;
57
58 for capture in m.captures {
59 let capture_name = query.capture_names()[capture.index as usize];
60 match capture_name {
61 "fn_name" | "var_name" | "exported_fn_name" | "class_name" => {
62 name = capture.node.utf8_text(source.as_bytes()).unwrap_or("unknown").to_string();
63 }
64 "function" | "var_fn" | "exported_function" => {
65 kind = AtomKind::Function;
66 atom_node = Some(capture.node);
67 }
68 "class" => {
69 kind = AtomKind::Class;
70 atom_node = Some(capture.node);
71 }
72 "jsx" => {
73 let node = capture.node;
75 let start = node.start_byte();
76 let end = node.end_byte();
77
78 atoms.push(ExtractedAtom {
79 name: "jsx_fragment".to_string(),
80 kind: AtomKind::Module,
81 source: source[start..end].to_string(),
82 start_byte: start,
83 end_byte: end,
84 start_line: node.start_position().row + 1,
85 end_line: node.end_position().row + 1,
86 defines: Vec::new(),
87 references: Vec::new(),
88 doc_comment: None,
89 visibility: crate::atomizer::extractor::Visibility::Public,
90 parent: None,
91 decorators: Vec::new(),
92 });
93 }
94 _ => {}
95 }
96 }
97
98 if let Some(node) = atom_node {
99 let start = node.start_byte();
100 let end = node.end_byte();
101 let start_point = node.start_position();
102 let end_point = node.end_position();
103
104 atoms.push(ExtractedAtom {
105 name: name.clone(),
106 kind,
107 source: source[start..end].to_string(),
108 start_byte: start,
109 end_byte: end,
110 start_line: start_point.row + 1,
111 end_line: end_point.row + 1,
112 defines: vec![name.clone()],
113 references: Vec::new(),
114 doc_comment: None,
115 visibility: crate::atomizer::extractor::Visibility::Public,
116 parent: None,
117 decorators: Vec::new(),
118 });
119 }
120 }
121
122 Ok(atoms)
123 }
124
125 #[cfg(not(feature = "ast-parsing"))]
127 pub fn extract(&self, source: &str) -> CadiResult<Vec<ExtractedAtom>> {
128 use crate::atomizer::AtomExtractor;
129 AtomExtractor::new("javascript", self.config.clone()).extract(source)
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::atomizer::AtomizerConfig;
138
139 #[test]
140 fn test_jsx_extraction_component() {
141 let source = r#"
142 export function Hello() { return <div>Hello</div> }
143 const X = () => <span>Hi</span>;
144 class C extends React.Component { render() { return <p/> } }
145 "#;
146
147 let atomizer = JSXAtomizer::new(AtomizerConfig::default());
148 let atoms = atomizer.extract(source).unwrap();
149
150 assert!(atoms.iter().any(|a| a.name == "Hello"));
151 assert!(atoms.iter().any(|a| a.name == "X"));
152 assert!(atoms.iter().any(|a| a.name == "C"));
153 }
154}