tcss_core/
resolver.rs

1//! Path and URL resolution for TCSS imports
2//!
3//! This module handles resolving import paths to actual file locations:
4//! - Relative paths: './colors.tcss', '../theme.tcss'
5//! - Absolute paths: '/styles/base.tcss'
6//! - URLs: 'https://cdn.tcss.io/animations.tcss'
7//! - Package imports: '@tcss/core', 'package-name/file.tcss'
8
9use std::path::{Path, PathBuf};
10
11/// Type of import path
12#[derive(Debug, Clone, PartialEq)]
13pub enum ImportType {
14    /// Relative file path (./file.tcss, ../file.tcss)
15    Relative,
16    
17    /// Absolute file path (/path/to/file.tcss)
18    Absolute,
19    
20    /// HTTP/HTTPS URL
21    Url,
22    
23    /// Package import (@tcss/core, package-name)
24    Package,
25}
26
27/// Resolved import information
28#[derive(Debug, Clone)]
29pub struct ResolvedImport {
30    /// Original import path
31    pub original_path: String,
32    
33    /// Resolved absolute path or URL
34    pub resolved_path: String,
35    
36    /// Type of import
37    pub import_type: ImportType,
38}
39
40/// Import path resolver
41pub struct Resolver {
42    /// Base directory for resolving relative paths
43    base_dir: PathBuf,
44    
45    /// Package resolution paths (like node_modules)
46    package_paths: Vec<PathBuf>,
47}
48
49impl Resolver {
50    /// Create a new resolver with a base directory
51    pub fn new<P: AsRef<Path>>(base_dir: P) -> Self {
52        Self {
53            base_dir: base_dir.as_ref().to_path_buf(),
54            package_paths: vec![
55                PathBuf::from("node_modules"),
56                PathBuf::from("tcss_modules"),
57            ],
58        }
59    }
60    
61    /// Add a package resolution path
62    pub fn add_package_path<P: AsRef<Path>>(&mut self, path: P) {
63        self.package_paths.push(path.as_ref().to_path_buf());
64    }
65    
66    /// Resolve an import path
67    pub fn resolve(&self, import_path: &str) -> Result<ResolvedImport, String> {
68        let import_type = self.detect_import_type(import_path);
69        
70        let resolved_path = match import_type {
71            ImportType::Relative => self.resolve_relative(import_path)?,
72            ImportType::Absolute => self.resolve_absolute(import_path)?,
73            ImportType::Url => import_path.to_string(),
74            ImportType::Package => self.resolve_package(import_path)?,
75        };
76        
77        Ok(ResolvedImport {
78            original_path: import_path.to_string(),
79            resolved_path,
80            import_type,
81        })
82    }
83    
84    /// Detect the type of import path
85    fn detect_import_type(&self, path: &str) -> ImportType {
86        if path.starts_with("http://") || path.starts_with("https://") {
87            ImportType::Url
88        } else if path.starts_with("./") || path.starts_with("../") {
89            ImportType::Relative
90        } else if path.starts_with('/') {
91            ImportType::Absolute
92        } else {
93            ImportType::Package
94        }
95    }
96    
97    /// Resolve a relative path
98    fn resolve_relative(&self, path: &str) -> Result<String, String> {
99        let full_path = self.base_dir.join(path);
100        
101        // Normalize the path
102        let normalized = self.normalize_path(&full_path)?;
103        
104        // Check if file exists (optional, can be disabled for flexibility)
105        if !normalized.exists() {
106            return Err(format!("Import file not found: {}", path));
107        }
108        
109        normalized
110            .to_str()
111            .ok_or_else(|| format!("Invalid path: {}", path))
112            .map(|s| s.to_string())
113    }
114    
115    /// Resolve an absolute path
116    fn resolve_absolute(&self, path: &str) -> Result<String, String> {
117        let full_path = PathBuf::from(path);
118        
119        if !full_path.exists() {
120            return Err(format!("Import file not found: {}", path));
121        }
122        
123        full_path
124            .to_str()
125            .ok_or_else(|| format!("Invalid path: {}", path))
126            .map(|s| s.to_string())
127    }
128    
129    /// Resolve a package import
130    fn resolve_package(&self, path: &str) -> Result<String, String> {
131        // Split package name and file path
132        let (package_name, file_path) = if let Some(slash_pos) = path.find('/') {
133            (&path[..slash_pos], Some(&path[slash_pos + 1..]))
134        } else {
135            (path, None)
136        };
137        
138        // Try each package path
139        for pkg_dir in &self.package_paths {
140            let mut package_path = pkg_dir.join(package_name);
141            
142            // If no file path specified, look for index.tcss or main file
143            if let Some(file) = file_path {
144                package_path = package_path.join(file);
145            } else {
146                // Try index.tcss first
147                let index_path = package_path.join("index.tcss");
148                if index_path.exists() {
149                    package_path = index_path;
150                }
151            }
152
153            if package_path.exists() {
154                return package_path
155                    .to_str()
156                    .ok_or_else(|| format!("Invalid package path: {}", path))
157                    .map(|s| s.to_string());
158            }
159        }
160
161        Err(format!("Package not found: {}", path))
162    }
163
164    /// Normalize a path (resolve .. and .)
165    fn normalize_path(&self, path: &Path) -> Result<PathBuf, String> {
166        path.canonicalize()
167            .map_err(|e| format!("Failed to normalize path: {}", e))
168    }
169
170    /// Change the base directory
171    pub fn set_base_dir<P: AsRef<Path>>(&mut self, base_dir: P) {
172        self.base_dir = base_dir.as_ref().to_path_buf();
173    }
174
175    /// Get the current base directory
176    pub fn base_dir(&self) -> &Path {
177        &self.base_dir
178    }
179}
180
181impl Default for Resolver {
182    fn default() -> Self {
183        Self::new(".")
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_detect_import_type() {
193        let resolver = Resolver::new(".");
194
195        assert_eq!(resolver.detect_import_type("./file.tcss"), ImportType::Relative);
196        assert_eq!(resolver.detect_import_type("../file.tcss"), ImportType::Relative);
197        assert_eq!(resolver.detect_import_type("/abs/path.tcss"), ImportType::Absolute);
198        assert_eq!(resolver.detect_import_type("https://example.com/file.tcss"), ImportType::Url);
199        assert_eq!(resolver.detect_import_type("@tcss/core"), ImportType::Package);
200        assert_eq!(resolver.detect_import_type("package-name"), ImportType::Package);
201    }
202
203    #[test]
204    fn test_add_package_path() {
205        let mut resolver = Resolver::new(".");
206        resolver.add_package_path("custom_modules");
207
208        assert_eq!(resolver.package_paths.len(), 3);
209    }
210}
211