use crate::types::*;
use super::node_text;
pub fn extract(tree: &tree_sitter::Tree, source: &[u8]) -> (Vec<Symbol>, Vec<Import>) {
let root = tree.root_node();
let mut symbols = Vec::new();
let mut imports = Vec::new();
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
match child.kind() {
"rule_set" => {
extract_rule_set(&child, source, &mut symbols);
}
"import_statement" => {
extract_import(&child, source, &mut imports);
}
"keyframes_statement" => {
extract_keyframes(&child, source, &mut symbols);
}
"media_statement" => {
extract_media(&child, source, &mut symbols);
}
_ => {}
}
}
(symbols, imports)
}
fn extract_rule_set(node: &tree_sitter::Node, source: &[u8], symbols: &mut Vec<Symbol>) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "selectors" {
let text = node_text(&child, source);
for selector in text.split(',') {
let selector = selector.trim();
if selector.starts_with('.') || selector.starts_with('#') {
symbols.push(Symbol {
name: selector.to_string(),
kind: SymbolKind::Const,
span: Span::from_node(node),
signature: selector.to_string(),
doc_comment: None,
parent: None,
children: Vec::new(),
});
}
}
}
}
}
fn extract_import(node: &tree_sitter::Node, source: &[u8], imports: &mut Vec<Import>) {
let text = node_text(node, source);
let path = extract_import_path(text);
imports.push(Import {
path,
alias: None,
span: Span::from_node(node),
});
}
fn extract_import_path(text: &str) -> String {
if let Some(start) = text.find("url(") {
let rest = &text[start + 4..];
let inner = rest.trim_start_matches(|c: char| c == '"' || c == '\'');
if let Some(end) = inner.find(|c: char| c == '"' || c == '\'' || c == ')') {
return inner[..end].to_string();
}
}
if let Some(start) = text.find('"') {
if let Some(end) = text[start + 1..].find('"') {
return text[start + 1..start + 1 + end].to_string();
}
}
if let Some(start) = text.find('\'') {
if let Some(end) = text[start + 1..].find('\'') {
return text[start + 1..start + 1 + end].to_string();
}
}
text.to_string()
}
fn extract_keyframes(node: &tree_sitter::Node, source: &[u8], symbols: &mut Vec<Symbol>) {
let name = find_child_by_kind(node, "keyframes_name")
.map(|n| node_text(&n, source));
if let Some(name) = name {
symbols.push(Symbol {
name: name.to_string(),
kind: SymbolKind::Function,
span: Span::from_node(node),
signature: format!("@keyframes {}", name),
doc_comment: None,
parent: None,
children: Vec::new(),
});
}
}
fn find_child_by_kind<'a>(node: &'a tree_sitter::Node<'a>, kind: &str) -> Option<tree_sitter::Node<'a>> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == kind {
return Some(child);
}
}
None
}
fn extract_media(node: &tree_sitter::Node, source: &[u8], symbols: &mut Vec<Symbol>) {
let text = node_text(node, source);
let query = if let Some(brace) = text.find('{') {
text[..brace].trim().to_string()
} else {
text.to_string()
};
symbols.push(Symbol {
name: query.clone(),
kind: SymbolKind::Module,
span: Span::from_node(node),
signature: query,
doc_comment: None,
parent: None,
children: Vec::new(),
});
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_css(source: &str) -> (Vec<Symbol>, Vec<Import>) {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&tree_sitter_css::LANGUAGE.into()).unwrap();
let tree = parser.parse(source, None).unwrap();
extract(&tree, source.as_bytes())
}
#[test]
fn test_css_extraction() {
let source = r#"
@import "reset.css";
@import url("fonts.css");
.container {
display: flex;
}
#header {
background: blue;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@media (max-width: 768px) {
.container {
flex-direction: column;
}
}
"#;
let (symbols, imports) = parse_css(source);
assert_eq!(imports.len(), 2);
assert!(imports.iter().any(|i| i.path == "reset.css"));
assert!(imports.iter().any(|i| i.path == "fonts.css"));
let consts: Vec<_> = symbols.iter().filter(|s| s.kind == SymbolKind::Const).collect();
assert!(consts.iter().any(|s| s.name == ".container"));
assert!(consts.iter().any(|s| s.name == "#header"));
let funcs: Vec<_> = symbols.iter().filter(|s| s.kind == SymbolKind::Function).collect();
assert_eq!(funcs.len(), 1);
assert_eq!(funcs[0].name, "fadeIn");
let modules: Vec<_> = symbols.iter().filter(|s| s.kind == SymbolKind::Module).collect();
assert_eq!(modules.len(), 1);
assert!(modules[0].name.contains("@media"));
}
}