orbiton/templates/
project_templates.rs

1use anyhow::{Context, Result};
2use log::debug;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ProjectTemplate {
9    pub name: String,
10    pub description: String,
11    pub files: Vec<TemplateFile>,
12    pub dependencies: Vec<String>,
13    pub dev_dependencies: Vec<String>,
14    pub format: Option<ComponentFormat>, // Default to Legacy if None
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TemplateFile {
19    pub path: String,
20    pub content: String,
21}
22
23#[derive(Debug, Clone)]
24pub enum TemplateType {
25    Basic,
26    ComponentLibrary,
27    Advanced,
28}
29
30impl fmt::Display for TemplateType {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            Self::Basic => write!(f, "basic"),
34            Self::Advanced => write!(f, "advanced"),
35            Self::ComponentLibrary => write!(f, "component-library"),
36        }
37    }
38}
39
40impl TemplateType {
41    pub fn from_str(s: &str) -> Result<Self> {
42        match s.to_lowercase().as_str() {
43            "basic" => Ok(Self::Basic),
44            "advanced" => Ok(Self::Advanced),
45            "component-library" => Ok(Self::ComponentLibrary),
46            _ => Err(anyhow::anyhow!("Invalid template type: {}", s)),
47        }
48    }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52pub enum ComponentFormat {
53    #[serde(rename = "legacy")]
54    Legacy, // Old <script> format
55    #[serde(rename = "modern")]
56    Modern, // New <code> format with section tags
57    #[serde(rename = "markdown")]
58    Markdown, // Full Markdown format with code blocks
59}
60
61impl Default for ComponentFormat {
62    fn default() -> Self {
63        Self::Legacy
64    }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ComponentSection {
69    pub name: String, // e.g., "template", "style", "code", "tests", "markdown"
70    pub lang: String, // e.g., "html", "css", "rust", "markdown"
71    pub content: String,
72}
73
74pub struct TemplateManager {
75    templates_dir: std::path::PathBuf,
76}
77
78impl TemplateManager {
79    pub fn new() -> Result<Self> {
80        // List of possible template directories
81        let mut possible_dirs = Vec::new();
82
83        // Try relative to executable
84        if let Ok(exe) = std::env::current_exe() {
85            if let Some(dir) = exe.parent() {
86                possible_dirs.push(dir.join("templates"));
87            }
88        }
89
90        // Try relative to crate root (for development)
91        if let Ok(dir) = std::env::current_dir() {
92            possible_dirs.push(dir.join("templates"));
93        }
94
95        // Try relative to workspace root
96        if let Ok(dir) = std::env::current_dir() {
97            possible_dirs.extend(dir.ancestors().take(3).map(|p| p.join("templates")));
98        }
99
100        // Try relative to cargo manifest directory (for development)
101        if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
102            possible_dirs.push(PathBuf::from(dir).join("templates"));
103        }
104
105        for templates_dir in possible_dirs.iter() {
106            debug!("Checking for templates in {templates_dir:?}");
107            if templates_dir.exists() {
108                return Ok(Self {
109                    templates_dir: templates_dir.clone(),
110                });
111            }
112        }
113
114        Err(anyhow::anyhow!(
115            "Templates directory not found. Make sure the template files are properly installed. Looked in:\n{}",
116            possible_dirs
117                .iter()
118                .map(|p| format!("- {p:?}"))
119                .collect::<Vec<_>>()
120                .join("\n")
121        ))
122    }
123
124    pub fn list_templates(&self) -> Vec<TemplateType> {
125        vec![
126            TemplateType::Basic,
127            TemplateType::ComponentLibrary,
128            TemplateType::Advanced,
129        ]
130    }
131
132    pub fn generate_project(
133        &self,
134        name: &str,
135        template_type: TemplateType,
136        output_dir: &Path,
137    ) -> Result<()> {
138        let template_dir = self.templates_dir.join(template_type.to_string());
139        if !template_dir.exists() {
140            return Err(anyhow::anyhow!(
141                "Template directory not found: {template_dir:?}"
142            ));
143        }
144
145        let template_json = std::fs::read_to_string(template_dir.join("template.json"))
146            .with_context(|| format!("Failed to read template.json from {template_dir:?}"))?;
147
148        let mut template: ProjectTemplate =
149            serde_json::from_str(&template_json).context("Failed to parse template.json")?;
150
151        template.name = name.to_string();
152
153        for mut file in template.files {
154            // Convert file extension for Markdown components if needed
155            if template.format == Some(ComponentFormat::Markdown) && file.path.ends_with(".orbit") {
156                file.path = file.path.replace(".orbit", ".orbit.md");
157            }
158
159            let target_path = output_dir.join(&file.path);
160            if let Some(parent) = target_path.parent() {
161                std::fs::create_dir_all(parent)
162                    .with_context(|| format!("Failed to create directory {parent:?}"))?;
163            }
164
165            std::fs::write(&target_path, file.content)
166                .with_context(|| format!("Failed to write file: {target_path:?}"))?;
167        }
168
169        Ok(())
170    }
171
172    #[allow(dead_code)] // Used in other modules
173    pub fn parse_component_sections(
174        content: &str,
175        format: ComponentFormat,
176    ) -> Result<Vec<ComponentSection>> {
177        match format {
178            ComponentFormat::Legacy => Self::parse_legacy_format(content),
179            ComponentFormat::Modern => Self::parse_modern_format(content),
180            ComponentFormat::Markdown => Self::parse_markdown_format(content),
181        }
182    }
183
184    #[allow(dead_code)]
185    fn parse_markdown_format(content: &str) -> Result<Vec<ComponentSection>> {
186        let mut sections = Vec::new();
187        let mut current_section: Option<ComponentSection> = None;
188        let mut in_code_block = false;
189        let mut code_fence_count = 0;
190        let mut markdown_content = String::new();
191
192        for line in content.lines() {
193            if line.starts_with("```") {
194                code_fence_count += 1;
195                if code_fence_count % 2 == 1 {
196                    // Start of code block
197                    in_code_block = true;
198                    let lang = line.trim_start_matches('`').trim();
199                    if !lang.is_empty() {
200                        // Save any accumulated markdown content
201                        if !markdown_content.trim().is_empty() {
202                            sections.push(ComponentSection {
203                                name: "markdown".to_string(),
204                                lang: "markdown".to_string(),
205                                content: markdown_content.trim().to_string(),
206                            });
207                            markdown_content.clear();
208                        }
209
210                        if let Some(section) = current_section {
211                            sections.push(section);
212                        }
213                        current_section = Some(ComponentSection {
214                            name: Self::determine_section_name(lang),
215                            lang: lang.to_string(),
216                            content: String::new(),
217                        });
218                    }
219                } else {
220                    // End of code block
221                    in_code_block = false;
222                    if let Some(section) = current_section.take() {
223                        sections.push(section);
224                    }
225                }
226            } else if in_code_block {
227                if let Some(ref mut section) = current_section {
228                    section.content.push_str(line);
229                    section.content.push('\n');
230                }
231            } else {
232                // This is Markdown content outside code blocks
233                markdown_content.push_str(line);
234                markdown_content.push('\n');
235            }
236        }
237
238        Ok(sections)
239    }
240
241    #[allow(dead_code)]
242    fn parse_modern_format(content: &str) -> Result<Vec<ComponentSection>> {
243        let mut sections = Vec::new();
244        let mut current_section: Option<ComponentSection> = None;
245
246        for line in content.lines() {
247            if line.starts_with('<') && line.ends_with('>') {
248                // This line indicates a new section
249                if let Some(section) = current_section.take() {
250                    sections.push(section);
251                }
252
253                let lang = line.trim_matches('<').trim_matches('>').trim();
254                current_section = Some(ComponentSection {
255                    name: Self::determine_section_name(lang),
256                    lang: lang.to_string(),
257                    content: String::new(),
258                });
259            } else if let Some(ref mut section) = current_section {
260                section.content.push_str(line);
261                section.content.push('\n');
262            }
263        }
264
265        if let Some(section) = current_section {
266            sections.push(section);
267        }
268
269        Ok(sections)
270    }
271
272    #[allow(dead_code)]
273    fn parse_legacy_format(content: &str) -> Result<Vec<ComponentSection>> {
274        let mut sections = Vec::new();
275        let mut current_section: Option<ComponentSection> = None;
276
277        // Legacy format has <template>, <style>, and <script> tags
278        for line in content.lines() {
279            if line.contains("<template>") {
280                if let Some(section) = current_section.take() {
281                    sections.push(section);
282                }
283                current_section = Some(ComponentSection {
284                    name: "template".to_string(),
285                    lang: "html".to_string(),
286                    content: String::new(),
287                });
288            } else if line.contains("</template>") {
289                if let Some(section) = current_section.take() {
290                    sections.push(section);
291                }
292            } else if line.contains("<style>") {
293                if let Some(section) = current_section.take() {
294                    sections.push(section);
295                }
296                current_section = Some(ComponentSection {
297                    name: "style".to_string(),
298                    lang: "css".to_string(),
299                    content: String::new(),
300                });
301            } else if line.contains("</style>") {
302                if let Some(section) = current_section.take() {
303                    sections.push(section);
304                }
305            } else if line.contains("<script>") {
306                if let Some(section) = current_section.take() {
307                    sections.push(section);
308                }
309                current_section = Some(ComponentSection {
310                    name: "code".to_string(),
311                    lang: "rust".to_string(),
312                    content: String::new(),
313                });
314            } else if line.contains("</script>") {
315                if let Some(section) = current_section.take() {
316                    sections.push(section);
317                }
318            } else if let Some(ref mut section) = current_section {
319                section.content.push_str(line);
320                section.content.push('\n');
321            }
322        }
323
324        if let Some(section) = current_section {
325            sections.push(section);
326        }
327
328        Ok(sections)
329    }
330
331    #[allow(dead_code)]
332    fn determine_section_name(lang: &str) -> String {
333        match lang {
334            "html" | "template" => "template",
335            "css" | "style" => "style",
336            "rust" => "code",
337            "markdown" | "md" => "markdown",
338            _ => lang,
339        }
340        .to_string()
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use tempfile::tempdir; // Now properly imported from added dependency
348
349    #[test]
350    fn test_template_type_from_str() {
351        assert!(matches!(
352            TemplateType::from_str("basic"),
353            Ok(TemplateType::Basic)
354        ));
355        assert!(matches!(
356            TemplateType::from_str("advanced"),
357            Ok(TemplateType::Advanced)
358        ));
359        assert!(matches!(
360            TemplateType::from_str("component-library"),
361            Ok(TemplateType::ComponentLibrary)
362        ));
363        assert!(TemplateType::from_str("invalid").is_err());
364    }
365
366    #[test]
367    #[ignore] // Ignore this test as it requires template files to be installed
368    fn test_create_from_template() -> Result<()> {
369        let temp_dir = tempdir()?;
370        let template_manager = TemplateManager::new()?;
371
372        // Create a basic project
373        template_manager.generate_project("test-project", TemplateType::Basic, temp_dir.path())?;
374
375        // Verify created files
376        assert!(temp_dir.path().join("Cargo.toml").exists());
377        assert!(temp_dir.path().join("src/main.rs").exists());
378
379        Ok(())
380    }
381}