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>, }
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, #[serde(rename = "modern")]
56 Modern, #[serde(rename = "markdown")]
58 Markdown, }
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, pub lang: String, 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 let mut possible_dirs = Vec::new();
82
83 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 if let Ok(dir) = std::env::current_dir() {
92 possible_dirs.push(dir.join("templates"));
93 }
94
95 if let Ok(dir) = std::env::current_dir() {
97 possible_dirs.extend(dir.ancestors().take(3).map(|p| p.join("templates")));
98 }
99
100 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 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)] 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 in_code_block = true;
198 let lang = line.trim_start_matches('`').trim();
199 if !lang.is_empty() {
200 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 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 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 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 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; #[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] fn test_create_from_template() -> Result<()> {
369 let temp_dir = tempdir()?;
370 let template_manager = TemplateManager::new()?;
371
372 template_manager.generate_project("test-project", TemplateType::Basic, temp_dir.path())?;
374
375 assert!(temp_dir.path().join("Cargo.toml").exists());
377 assert!(temp_dir.path().join("src/main.rs").exists());
378
379 Ok(())
380 }
381}