1use 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 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)
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 let md_content = fs::read_to_string(input)
49 .map_err(|e| format!("Failed to read markdown file: {e}"))?;
50
51 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 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 let pptx_data = generator::create_pptx_with_content(title, slides)
70 .map_err(|e| format!("Failed to generate PPTX: {e}"))?;
71
72 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 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 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 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 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 let mut issues = Vec::new();
156 let mut found_files = std::collections::HashSet::new();
157
158 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 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 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 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 if !name.ends_with(".rels") {
202 issues.push(format!("XML file missing declaration: {}", name));
203 println!(" ⚠ {} (missing XML declaration)", name);
204 }
205 } else {
206 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 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 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("&", "&")
248 .replace("<", "<")
249 .replace(">", ">")
250 .replace("\"", """)
251 .replace("'", "'")
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 let _ = fs::remove_file(output);
269 }
270
271 #[test]
272 fn test_escape_xml() {
273 assert_eq!(escape_xml("a & b"), "a & b");
274 assert_eq!(escape_xml("<tag>"), "<tag>");
275 assert_eq!(escape_xml("\"quoted\""), ""quoted"");
276 }
277}