Skip to main content

ppt_rs/cli/
commands.rs

1//! CLI commands implementation
2
3use crate::generator;
4use std::fs;
5use std::path::PathBuf;
6
7pub struct CreateCommand;
8pub struct FromMarkdownCommand;
9pub struct InfoCommand;
10pub struct ValidateCommand;
11
12impl CreateCommand {
13    pub fn execute(
14        output: &str,
15        title: Option<&str>,
16        slides: usize,
17        _template: Option<&str>,
18    ) -> Result<(), String> {
19        // Create output directory if needed
20        if let Some(parent) = PathBuf::from(output).parent() {
21            if !parent.as_os_str().is_empty() {
22                fs::create_dir_all(parent)
23                    .map_err(|e| format!("Failed to create directory: {e}"))?;
24            }
25        }
26
27        let title = title.unwrap_or("Presentation");
28
29        // Generate proper PPTX file
30        let pptx_data = generator::create_pptx(title, slides)
31            .map_err(|e| format!("Failed to generate PPTX: {e}"))?;
32
33        // Write to file
34        fs::write(output, pptx_data).map_err(|e| format!("Failed to write file: {e}"))?;
35
36        Ok(())
37    }
38}
39
40impl FromMarkdownCommand {
41    pub fn execute(input: &str, output: &str, title: Option<&str>) -> Result<(), String> {
42        // Read markdown file
43        let md_content =
44            fs::read_to_string(input).map_err(|e| format!("Failed to read markdown file: {e}"))?;
45
46        // Parse markdown into slides using enhanced parser
47        let slides = super::markdown::parse_markdown(&md_content)?;
48
49        if slides.is_empty() {
50            return Err("No slides found in markdown file".to_string());
51        }
52
53        // Create output directory if needed
54        if let Some(parent) = PathBuf::from(output).parent() {
55            if !parent.as_os_str().is_empty() {
56                fs::create_dir_all(parent)
57                    .map_err(|e| format!("Failed to create directory: {e}"))?;
58            }
59        }
60
61        let title = title.unwrap_or("Presentation from Markdown");
62
63        // Generate PPTX with content
64        let pptx_data = generator::create_pptx_with_content(title, slides)
65            .map_err(|e| format!("Failed to generate PPTX: {e}"))?;
66
67        // Write to file
68        fs::write(output, pptx_data).map_err(|e| format!("Failed to write file: {e}"))?;
69
70        Ok(())
71    }
72}
73
74impl InfoCommand {
75    pub fn execute(file: &str) -> Result<(), String> {
76        let metadata = fs::metadata(file).map_err(|e| format!("File not found: {e}"))?;
77
78        let size = metadata.len();
79        let modified = metadata
80            .modified()
81            .ok()
82            .and_then(|t| t.elapsed().ok())
83            .map(|d| format!("{d:?} ago"))
84            .unwrap_or_else(|| "unknown".to_string());
85
86        println!("File Information");
87        println!("================");
88        println!("Path:     {file}");
89        println!("Size:     {size} bytes");
90        println!("Modified: {modified}");
91        let is_file = metadata.is_file();
92        println!("Is file:  {is_file}");
93
94        // Try to read and parse as XML
95        if let Ok(content) = fs::read_to_string(file) {
96            if content.starts_with("<?xml") {
97                println!("\nPresentation Information");
98                println!("========================");
99                if let Some(title_start) = content.find("<title>") {
100                    if let Some(title_end) = content[title_start + 7..].find("</title>") {
101                        let title = &content[title_start + 7..title_start + 7 + title_end];
102                        println!("Title: {title}");
103                    }
104                }
105                if let Some(slides_start) = content.find("count=\"") {
106                    let search_from = slides_start + 7;
107                    if let Some(slides_end) = content[search_from..].find("\"") {
108                        let count_str = &content[search_from..search_from + slides_end];
109                        println!("Slides: {count_str}");
110                    }
111                }
112            }
113        }
114
115        Ok(())
116    }
117}
118
119impl ValidateCommand {
120    /// Validate a PPTX file for ECMA-376 compliance
121    pub fn execute(file: &str) -> Result<(), String> {
122        use std::io::Read;
123        use zip::ZipArchive;
124
125        println!("Validating PPTX file: {file}");
126        println!("{}", "=".repeat(60));
127
128        // Check file exists
129        let metadata = fs::metadata(file).map_err(|e| format!("File not found: {e}"))?;
130
131        if !metadata.is_file() {
132            return Err(format!("Path is not a file: {file}"));
133        }
134
135        // Try to open as ZIP archive
136        let file_handle = fs::File::open(file).map_err(|e| format!("Failed to open file: {e}"))?;
137
138        let mut archive =
139            ZipArchive::new(file_handle).map_err(|e| format!("Invalid ZIP archive: {e}"))?;
140
141        println!("✓ File is a valid ZIP archive");
142        println!("  Total entries: {}", archive.len());
143
144        // Check required files
145        let mut issues = Vec::new();
146        let mut found_files = std::collections::HashSet::new();
147
148        // Collect all file names
149        for i in 0..archive.len() {
150            let file = archive
151                .by_index(i)
152                .map_err(|e| format!("Failed to read archive entry: {e}"))?;
153            found_files.insert(file.name().to_string());
154        }
155
156        // Required files for PPTX
157        let required_files = vec![
158            "[Content_Types].xml",
159            "_rels/.rels",
160            "ppt/presentation.xml",
161            "docProps/core.xml",
162        ];
163
164        println!("\nChecking required files...");
165        for required in &required_files {
166            if found_files.contains(*required) {
167                println!("  ✓ {}", required);
168            } else {
169                println!("  ✗ {} (missing)", required);
170                issues.push(format!("Missing required file: {}", required));
171            }
172        }
173
174        // Check XML validity
175        println!("\nChecking XML validity...");
176        for i in 0..archive.len() {
177            let mut file = archive
178                .by_index(i)
179                .map_err(|e| format!("Failed to read archive entry: {e}"))?;
180
181            let name = file.name().to_string();
182            if name.ends_with(".xml") || name.ends_with(".rels") {
183                let mut content = String::new();
184                file.read_to_string(&mut content)
185                    .map_err(|e| format!("Failed to read XML file {}: {e}", name))?;
186
187                // Basic XML validation (check for well-formedness)
188                if content.trim().is_empty() {
189                    issues.push(format!("Empty XML file: {}", name));
190                    println!("  ⚠ {} (empty)", name);
191                } else if !content.contains("<?xml") && !name.ends_with(".rels") {
192                    // .rels files don't always have XML declaration
193                    if !name.ends_with(".rels") {
194                        issues.push(format!("XML file missing declaration: {}", name));
195                        println!("  ⚠ {} (missing XML declaration)", name);
196                    }
197                } else {
198                    // Check for basic XML structure
199                    if content.contains("<") && content.contains(">") {
200                        println!("  ✓ {} (valid XML)", name);
201                    } else {
202                        issues.push(format!("Invalid XML structure: {}", name));
203                        println!("  ✗ {} (invalid XML)", name);
204                    }
205                }
206            }
207        }
208
209        // Check relationships
210        println!("\nChecking relationships...");
211        if found_files.contains("_rels/.rels") {
212            println!("  ✓ Package relationships found");
213        } else {
214            issues.push("Missing package relationships".to_string());
215            println!("  ✗ Package relationships missing");
216        }
217
218        // Summary
219        println!("\n{}", "=".repeat(60));
220        if issues.is_empty() {
221            println!("✓ Validation PASSED");
222            println!("  File appears to be a valid PPTX file");
223            println!("  ECMA-376 compliance: OK");
224        } else {
225            println!("✗ Validation FAILED");
226            println!("  Found {} issue(s):", issues.len());
227            for issue in &issues {
228                println!("    - {}", issue);
229            }
230            return Err(format!("Validation failed with {} issue(s)", issues.len()));
231        }
232
233        Ok(())
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use std::fs;
241    use std::path::Path;
242
243    #[test]
244    fn test_create_command() {
245        let output = "/tmp/test_presentation.pptx";
246        let result = CreateCommand::execute(output, Some("Test"), 3, None);
247        assert!(result.is_ok());
248        assert!(Path::new(output).exists());
249
250        // Cleanup
251        let _ = fs::remove_file(output);
252    }
253
254    #[test]
255    fn test_escape_xml() {
256        use crate::core::escape_xml;
257        assert_eq!(escape_xml("a & b"), "a &amp; b");
258        assert_eq!(escape_xml("<tag>"), "&lt;tag&gt;");
259        assert_eq!(escape_xml("\"quoted\""), "&quot;quoted&quot;");
260    }
261}