use crate::indexer::languages::Language;
use tree_sitter::Node;
pub struct Svelte {}
impl Language for Svelte {
fn name(&self) -> &'static str {
"svelte"
}
fn get_ts_language(&self) -> tree_sitter::Language {
tree_sitter_svelte_ng::LANGUAGE.into()
}
fn get_meaningful_kinds(&self) -> Vec<&'static str> {
vec![
"script_element",
"style_element",
"element", ]
}
fn extract_symbols(&self, node: Node, contents: &str) -> Vec<String> {
let mut symbols = Vec::new();
match node.kind() {
"script_element" => {
self.extract_script_content_symbols(node, contents, &mut symbols);
}
"style_element" => {
self.extract_style_content_symbols(node, contents, &mut symbols);
}
"element" => {
self.extract_meaningful_element_symbols(node, contents, &mut symbols);
}
_ => self.extract_identifiers(node, contents, &mut symbols),
}
symbols.sort();
symbols.dedup();
symbols
}
fn extract_identifiers(&self, node: Node, contents: &str, symbols: &mut Vec<String>) {
let kind = node.kind();
if (kind.contains("identifier") || kind.contains("name"))
&& !kind.contains("property")
&& kind != "tag_name"
{
if let Ok(text) = node.utf8_text(contents.as_bytes()) {
let text = text.trim();
if !text.is_empty() && !symbols.contains(&text.to_string()) {
if !self.is_svelte_keyword(text) && !self.is_html_tag(text) {
symbols.push(text.to_string());
}
}
}
}
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
self.extract_identifiers(cursor.node(), contents, symbols);
if !cursor.goto_next_sibling() {
break;
}
}
}
}
fn are_node_types_equivalent(&self, type1: &str, type2: &str) -> bool {
if type1 == type2 {
return true;
}
let semantic_groups = [
&[
"function_declaration",
"method_definition",
"arrow_function",
] as &[&str],
&[
"variable_declaration",
"lexical_declaration",
"reactive_declaration",
],
&["reactive_statement", "reactive_declaration"],
&["component", "element"],
&["script_element", "style_element"],
];
for group in &semantic_groups {
let contains_type1 = group.contains(&type1);
let contains_type2 = group.contains(&type2);
if contains_type1 && contains_type2 {
return true;
}
}
false
}
fn get_node_type_description(&self, node_type: &str) -> &'static str {
match node_type {
"function_declaration" | "method_definition" | "arrow_function" => {
"function declarations"
}
"variable_declaration" | "lexical_declaration" => "variable declarations",
"reactive_statement" | "reactive_declaration" => "reactive declarations",
"class_declaration" => "class declarations",
"component" | "element" => "component declarations",
"script_element" => "script blocks",
"style_element" => "style blocks",
_ => "declarations",
}
}
fn extract_imports_exports(&self, node: Node, contents: &str) -> (Vec<String>, Vec<String>) {
let mut imports = Vec::new();
let mut exports = Vec::new();
if node.kind() == "script_element" {
let script_content = self.get_script_text_content(node, contents);
let (script_imports, script_exports) = Self::parse_js_imports_exports(&script_content);
imports.extend(script_imports);
exports.extend(script_exports);
}
(imports, exports)
}
fn resolve_import(
&self,
import_path: &str,
source_file: &str,
all_files: &[String],
) -> Option<String> {
let js = super::javascript::JavaScript {};
js.resolve_import(import_path, source_file, all_files)
}
fn get_file_extensions(&self) -> Vec<&'static str> {
vec!["svelte"]
}
}
impl Svelte {
fn extract_script_content_symbols(
&self,
node: Node,
contents: &str,
symbols: &mut Vec<String>,
) {
for child in node.children(&mut node.walk()) {
if child.kind() == "raw_text" {
if let Ok(script_content) = child.utf8_text(contents.as_bytes()) {
self.extract_js_patterns_from_text(script_content, symbols);
}
}
}
}
fn extract_style_content_symbols(&self, node: Node, contents: &str, symbols: &mut Vec<String>) {
for child in node.children(&mut node.walk()) {
if child.kind() == "raw_text" {
if let Ok(style_content) = child.utf8_text(contents.as_bytes()) {
self.extract_css_patterns_from_text(style_content, symbols);
}
}
}
}
fn extract_meaningful_element_symbols(
&self,
node: Node,
contents: &str,
symbols: &mut Vec<String>,
) {
let mut has_svelte_attributes = false;
for child in node.children(&mut node.walk()) {
if child.kind() == "start_tag" {
for tag_child in child.children(&mut child.walk()) {
if tag_child.kind() == "attribute" {
for attr_child in tag_child.children(&mut tag_child.walk()) {
if attr_child.kind() == "attribute_name" {
if let Ok(attr_name) = attr_child.utf8_text(contents.as_bytes()) {
if attr_name.starts_with("on:")
|| attr_name.starts_with("bind:")
|| attr_name.starts_with("use:")
|| attr_name.starts_with("transition:")
{
has_svelte_attributes = true;
symbols.push(attr_name.to_string());
}
}
}
}
}
else if tag_child.kind() == "tag_name" {
if let Ok(tag_name) = tag_child.utf8_text(contents.as_bytes()) {
if tag_name.chars().next().is_some_and(|c| c.is_uppercase())
&& !self.is_html_tag(tag_name)
{
symbols.push(tag_name.to_string());
has_svelte_attributes = true;
}
}
}
}
}
}
if !has_svelte_attributes {}
}
fn extract_js_patterns_from_text(&self, text: &str, symbols: &mut Vec<String>) {
let lines: Vec<&str> = text.lines().collect();
for line in lines {
let line = line.trim();
if line.starts_with("function ") {
if let Some(name) = self.extract_function_name(line) {
symbols.push(name);
}
}
else if line.starts_with("let ")
|| line.starts_with("const ")
|| line.starts_with("var ")
{
if let Some(name) = self.extract_variable_name(line) {
symbols.push(name);
}
}
else if line.starts_with("export let ") || line.starts_with("export const ") {
if let Some(name) = self.extract_export_name(line) {
symbols.push(name);
}
}
else if line.trim_start().starts_with("$:") {
if let Some(name) = self.extract_reactive_name(line) {
symbols.push(name);
}
}
}
}
fn extract_css_patterns_from_text(&self, text: &str, symbols: &mut Vec<String>) {
let lines: Vec<&str> = text.lines().collect();
for line in lines {
let line = line.trim();
if line.contains('{') && !line.trim_start().starts_with("/*") {
let selector_part = line.split('{').next().unwrap_or("").trim();
if !selector_part.is_empty() && !selector_part.contains(':') {
for selector in selector_part.split(',') {
let selector = selector.trim();
if !selector.is_empty() {
symbols.push(selector.to_string());
}
}
}
}
}
}
fn extract_function_name(&self, line: &str) -> Option<String> {
if let Some(start) = line.find("function ") {
let after_function = &line[start + 9..];
if let Some(end) = after_function.find('(') {
let name = after_function[..end].trim();
if !name.is_empty() && !self.is_svelte_keyword(name) {
return Some(name.to_string());
}
}
}
None
}
fn extract_variable_name(&self, line: &str) -> Option<String> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 && (parts[0] == "let" || parts[0] == "const" || parts[0] == "var") {
let name = parts[1].trim_end_matches('=').trim_end_matches(',').trim();
if !name.is_empty() && !self.is_svelte_keyword(name) {
return Some(name.to_string());
}
}
None
}
fn extract_export_name(&self, line: &str) -> Option<String> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 5 && parts[0] == "export" && (parts[1] == "let" || parts[1] == "const") {
let name = parts[2].trim_end_matches('=').trim_end_matches(',').trim();
if !name.is_empty() && !self.is_svelte_keyword(name) {
return Some(name.to_string());
}
}
None
}
fn extract_reactive_name(&self, line: &str) -> Option<String> {
if let Some(colon_pos) = line.find(':') {
let after_colon = &line[colon_pos + 1..].trim();
if let Some(eq_pos) = after_colon.find('=') {
let name = after_colon[..eq_pos].trim();
if !name.is_empty() && !self.is_svelte_keyword(name) {
return Some(name.to_string());
}
}
else if after_colon.starts_with("if ") {
return Some("reactive_if".to_string());
}
}
None
}
fn is_svelte_keyword(&self, text: &str) -> bool {
matches!(
text,
"export" | "import" | "let" | "const" | "var" | "function" | "class" | "if" | "else"
| "for" | "while" | "do" | "switch" | "case" | "default" | "break" | "continue"
| "return" | "try" | "catch" | "finally" | "throw" | "new" | "this" | "super"
| "true" | "false" | "null" | "undefined" | "typeof" | "instanceof" | "in" | "of"
| "bind" | "on" | "use" | "transition" | "out" | "animate"
)
}
fn is_html_tag(&self, text: &str) -> bool {
matches!(
text.to_lowercase().as_str(),
"div"
| "span" | "p"
| "a" | "img"
| "h1" | "h2"
| "h3" | "h4"
| "h5" | "h6"
| "ul" | "ol"
| "li" | "table"
| "tr" | "td"
| "th" | "thead"
| "tbody" | "tfoot"
| "form" | "input"
| "button" | "select"
| "option" | "textarea"
| "label" | "header"
| "footer" | "nav"
| "main" | "section"
| "article" | "aside"
| "html" | "head"
| "body" | "title"
| "meta" | "link"
| "script" | "style"
)
}
fn get_script_text_content(&self, node: Node, contents: &str) -> String {
if let Ok(text) = node.utf8_text(contents.as_bytes()) {
let text = text.trim();
if text.starts_with("<script") && text.ends_with("</script>") {
if let Some(tag_end) = text.find('>') {
let inner = &text[tag_end + 1..];
if let Some(closing_start) = inner.rfind("</script>") {
return inner[..closing_start].trim().to_string();
}
}
}
text.to_string()
} else {
String::new()
}
}
fn parse_js_imports_exports(content: &str) -> (Vec<String>, Vec<String>) {
let mut imports = Vec::new();
let mut exports = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("import ") {
if let Some(from_pos) = trimmed.find(" from ") {
let import_path = &trimmed[from_pos + 6..].trim();
if let Some(path) = Self::extract_quoted_string(import_path) {
imports.push(path);
}
}
}
if let Some(export_content) = trimmed.strip_prefix("export ") {
if trimmed.contains("export default") {
exports.push("default".to_string());
} else {
let export_part = export_content.trim();
if let Some(var_part) = export_part
.strip_prefix("let ")
.or_else(|| export_part.strip_prefix("const "))
.or_else(|| export_part.strip_prefix("var "))
{
let var_name = var_part
.trim()
.split(|c: char| c == '=' || c == ';' || c == ':' || c.is_whitespace())
.next()
.unwrap_or("")
.trim();
if !var_name.is_empty() {
exports.push(var_name.to_string());
}
} else if let Some(func_part) = export_part.strip_prefix("function ") {
if let Some(name_end) = func_part.find('(') {
let func_name = func_part[..name_end].trim();
if !func_name.is_empty() {
exports.push(func_name.to_string());
}
}
} else if let Some(class_part) = export_part.strip_prefix("class ") {
if let Some(name) = class_part.split_whitespace().next() {
let class_name = name.trim_end_matches('{');
if !class_name.is_empty() {
exports.push(class_name.to_string());
}
}
} else {
if let Some(name) = export_part.split_whitespace().next() {
if !name.starts_with('{') {
exports.push(name.to_string());
}
}
}
}
}
}
(imports, exports)
}
fn extract_quoted_string(text: &str) -> Option<String> {
let text = text.trim();
if (text.starts_with('"') && text.ends_with('"'))
|| (text.starts_with('\'') && text.ends_with('\''))
{
Some(text[1..text.len() - 1].to_string())
} else {
None
}
}
}