1use crate::generator;
4use std::fs;
5use std::path::PathBuf;
6
7pub struct CreateCommand;
8pub struct FromMarkdownCommand;
9pub struct FromHtmlCommand;
10pub struct InfoCommand;
11pub struct ValidateCommand;
12
13impl CreateCommand {
14 pub fn execute(
15 output: &str,
16 title: Option<&str>,
17 slides: usize,
18 _template: Option<&str>,
19 ) -> Result<(), String> {
20 if let Some(parent) = PathBuf::from(output).parent() {
22 if !parent.as_os_str().is_empty() {
23 fs::create_dir_all(parent)
24 .map_err(|e| format!("Failed to create directory: {e}"))?;
25 }
26 }
27
28 let title = title.unwrap_or("Presentation");
29
30 let pptx_data = generator::create_pptx(title, slides)
32 .map_err(|e| format!("Failed to generate PPTX: {e}"))?;
33
34 fs::write(output, pptx_data).map_err(|e| format!("Failed to write file: {e}"))?;
36
37 Ok(())
38 }
39}
40
41impl FromMarkdownCommand {
42 pub fn execute(input: &str, output: &str, title: Option<&str>) -> Result<(), String> {
43 let md_content =
45 fs::read_to_string(input).map_err(|e| format!("Failed to read markdown file: {e}"))?;
46
47 let slides = super::markdown::parse_markdown(&md_content)?;
49
50 if slides.is_empty() {
51 return Err("No slides found in markdown file".to_string());
52 }
53
54 if let Some(parent) = PathBuf::from(output).parent() {
56 if !parent.as_os_str().is_empty() {
57 fs::create_dir_all(parent)
58 .map_err(|e| format!("Failed to create directory: {e}"))?;
59 }
60 }
61
62 let title = title.unwrap_or("Presentation from Markdown");
63
64 let pptx_data = generator::create_pptx_with_content(title, slides)
66 .map_err(|e| format!("Failed to generate PPTX: {e}"))?;
67
68 fs::write(output, pptx_data).map_err(|e| format!("Failed to write file: {e}"))?;
70
71 Ok(())
72 }
73}
74
75impl FromHtmlCommand {
76 pub fn execute(
77 input: &str,
78 output: &str,
79 title: Option<&str>,
80 max_slides: usize,
81 max_bullets: usize,
82 no_images: bool,
83 no_tables: bool,
84 no_code: bool,
85 ) -> Result<(), String> {
86 let html_content =
88 std::fs::read_to_string(input).map_err(|e| format!("Failed to read HTML file: {e}"))?;
89
90 let options = crate::import::HtmlParseOptions::new()
92 .max_slides(max_slides)
93 .max_bullets(max_bullets)
94 .include_images(!no_images)
95 .include_tables(!no_tables)
96 .include_code(!no_code);
97
98 let slides = crate::import::parse_html_with_options(&html_content, options)?;
100
101 if slides.is_empty() {
102 return Err("No slides found in HTML file".to_string());
103 }
104
105 if let Some(parent) = PathBuf::from(output).parent() {
107 if !parent.as_os_str().is_empty() {
108 fs::create_dir_all(parent)
109 .map_err(|e| format!("Failed to create directory: {e}"))?;
110 }
111 }
112
113 let title = title.unwrap_or("Presentation from HTML");
114
115 let pptx_data = generator::create_pptx_with_content(title, slides)
117 .map_err(|e| format!("Failed to generate PPTX: {e}"))?;
118
119 fs::write(output, pptx_data).map_err(|e| format!("Failed to write file: {e}"))?;
121
122 Ok(())
123 }
124}
125
126impl InfoCommand {
127 pub fn execute(file: &str) -> Result<(), String> {
128 let metadata = fs::metadata(file).map_err(|e| format!("File not found: {e}"))?;
129
130 let size = metadata.len();
131 let modified = metadata
132 .modified()
133 .ok()
134 .and_then(|t| t.elapsed().ok())
135 .map(|d| format!("{d:?} ago"))
136 .unwrap_or_else(|| "unknown".to_string());
137
138 println!("File Information");
139 println!("================");
140 println!("Path: {file}");
141 println!("Size: {size} bytes");
142 println!("Modified: {modified}");
143 let is_file = metadata.is_file();
144 println!("Is file: {is_file}");
145
146 if let Ok(content) = fs::read_to_string(file) {
148 if content.starts_with("<?xml") {
149 println!("\nPresentation Information");
150 println!("========================");
151 if let Some(title_start) = content.find("<title>") {
152 if let Some(title_end) = content[title_start + 7..].find("</title>") {
153 let title = &content[title_start + 7..title_start + 7 + title_end];
154 println!("Title: {title}");
155 }
156 }
157 if let Some(slides_start) = content.find("count=\"") {
158 let search_from = slides_start + 7;
159 if let Some(slides_end) = content[search_from..].find("\"") {
160 let count_str = &content[search_from..search_from + slides_end];
161 println!("Slides: {count_str}");
162 }
163 }
164 }
165 }
166
167 Ok(())
168 }
169}
170
171impl ValidateCommand {
172 pub fn execute(file: &str) -> Result<(), String> {
174 use std::io::Read;
175 use zip::ZipArchive;
176
177 println!("Validating PPTX file: {file}");
178 println!("{}", "=".repeat(60));
179
180 let metadata = fs::metadata(file).map_err(|e| format!("File not found: {e}"))?;
182
183 if !metadata.is_file() {
184 return Err(format!("Path is not a file: {file}"));
185 }
186
187 let file_handle = fs::File::open(file).map_err(|e| format!("Failed to open file: {e}"))?;
189
190 let mut archive =
191 ZipArchive::new(file_handle).map_err(|e| format!("Invalid ZIP archive: {e}"))?;
192
193 println!("✓ File is a valid ZIP archive");
194 println!(" Total entries: {}", archive.len());
195
196 let mut issues = Vec::new();
198 let mut found_files = std::collections::HashSet::new();
199
200 for i in 0..archive.len() {
202 let file = archive
203 .by_index(i)
204 .map_err(|e| format!("Failed to read archive entry: {e}"))?;
205 found_files.insert(file.name().to_string());
206 }
207
208 let required_files = vec![
210 "[Content_Types].xml",
211 "_rels/.rels",
212 "ppt/presentation.xml",
213 "docProps/core.xml",
214 ];
215
216 println!("\nChecking required files...");
217 for required in &required_files {
218 if found_files.contains(*required) {
219 println!(" ✓ {}", required);
220 } else {
221 println!(" ✗ {} (missing)", required);
222 issues.push(format!("Missing required file: {}", required));
223 }
224 }
225
226 println!("\nChecking XML validity...");
228 for i in 0..archive.len() {
229 let mut file = archive
230 .by_index(i)
231 .map_err(|e| format!("Failed to read archive entry: {e}"))?;
232
233 let name = file.name().to_string();
234 if name.ends_with(".xml") || name.ends_with(".rels") {
235 let mut content = String::new();
236 file.read_to_string(&mut content)
237 .map_err(|e| format!("Failed to read XML file {}: {e}", name))?;
238
239 if content.trim().is_empty() {
241 issues.push(format!("Empty XML file: {}", name));
242 println!(" ⚠ {} (empty)", name);
243 } else if !content.contains("<?xml") && !name.ends_with(".rels") {
244 if !name.ends_with(".rels") {
246 issues.push(format!("XML file missing declaration: {}", name));
247 println!(" ⚠ {} (missing XML declaration)", name);
248 }
249 } else {
250 if content.contains("<") && content.contains(">") {
252 println!(" ✓ {} (valid XML)", name);
253 } else {
254 issues.push(format!("Invalid XML structure: {}", name));
255 println!(" ✗ {} (invalid XML)", name);
256 }
257 }
258 }
259 }
260
261 println!("\nChecking relationships...");
263 if found_files.contains("_rels/.rels") {
264 println!(" ✓ Package relationships found");
265 } else {
266 issues.push("Missing package relationships".to_string());
267 println!(" ✗ Package relationships missing");
268 }
269
270 println!("\n{}", "=".repeat(60));
272 if issues.is_empty() {
273 println!("✓ Validation PASSED");
274 println!(" File appears to be a valid PPTX file");
275 println!(" ECMA-376 compliance: OK");
276 } else {
277 println!("✗ Validation FAILED");
278 println!(" Found {} issue(s):", issues.len());
279 for issue in &issues {
280 println!(" - {}", issue);
281 }
282 return Err(format!("Validation failed with {} issue(s)", issues.len()));
283 }
284
285 Ok(())
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use std::fs;
293 use std::path::Path;
294
295 #[test]
296 fn test_create_command() {
297 let output = "/tmp/test_presentation.pptx";
298 let result = CreateCommand::execute(output, Some("Test"), 3, None);
299 assert!(result.is_ok());
300 assert!(Path::new(output).exists());
301
302 let _ = fs::remove_file(output);
304 }
305
306 #[test]
307 fn test_escape_xml() {
308 use crate::core::escape_xml;
309 assert_eq!(escape_xml("a & b"), "a & b");
310 assert_eq!(escape_xml("<tag>"), "<tag>");
311 assert_eq!(escape_xml("\"quoted\""), ""quoted"");
312 }
313}