Skip to main content

ppt_rs/cli/
commands.rs

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