1use 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 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 let pptx_data = generator::create_pptx(title, slides)
31 .map_err(|e| format!("Failed to generate PPTX: {e}"))?;
32
33 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 let md_content =
44 fs::read_to_string(input).map_err(|e| format!("Failed to read markdown file: {e}"))?;
45
46 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 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 let pptx_data = generator::create_pptx_with_content(title, slides)
65 .map_err(|e| format!("Failed to generate PPTX: {e}"))?;
66
67 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 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 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 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 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 let mut issues = Vec::new();
146 let mut found_files = std::collections::HashSet::new();
147
148 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 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 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 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 if !name.ends_with(".rels") {
194 issues.push(format!("XML file missing declaration: {}", name));
195 println!(" ⚠ {} (missing XML declaration)", name);
196 }
197 } else {
198 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 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 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 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 & b");
258 assert_eq!(escape_xml("<tag>"), "<tag>");
259 assert_eq!(escape_xml("\"quoted\""), ""quoted"");
260 }
261}