use crate::generator;
use std::fs;
use std::path::PathBuf;
pub struct CreateCommand;
pub struct FromMarkdownCommand;
pub struct FromHtmlCommand;
pub struct InfoCommand;
pub struct ValidateCommand;
impl CreateCommand {
pub fn execute(
output: &str,
title: Option<&str>,
slides: usize,
_template: Option<&str>,
) -> Result<(), String> {
if let Some(parent) = PathBuf::from(output).parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create directory: {e}"))?;
}
}
let title = title.unwrap_or("Presentation");
let pptx_data = generator::create_pptx(title, slides)
.map_err(|e| format!("Failed to generate PPTX: {e}"))?;
fs::write(output, pptx_data).map_err(|e| format!("Failed to write file: {e}"))?;
Ok(())
}
}
impl FromMarkdownCommand {
pub fn execute(input: &str, output: &str, title: Option<&str>) -> Result<(), String> {
let md_content =
fs::read_to_string(input).map_err(|e| format!("Failed to read markdown file: {e}"))?;
let slides = super::markdown::parse_markdown(&md_content)?;
if slides.is_empty() {
return Err("No slides found in markdown file".to_string());
}
if let Some(parent) = PathBuf::from(output).parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create directory: {e}"))?;
}
}
let title = title.unwrap_or("Presentation from Markdown");
let pptx_data = generator::create_pptx_with_content(title, slides)
.map_err(|e| format!("Failed to generate PPTX: {e}"))?;
fs::write(output, pptx_data).map_err(|e| format!("Failed to write file: {e}"))?;
Ok(())
}
}
impl FromHtmlCommand {
pub fn execute(
input: &str,
output: &str,
title: Option<&str>,
max_slides: usize,
max_bullets: usize,
no_images: bool,
no_tables: bool,
no_code: bool,
) -> Result<(), String> {
let html_content =
std::fs::read_to_string(input).map_err(|e| format!("Failed to read HTML file: {e}"))?;
let options = crate::import::HtmlParseOptions::new()
.max_slides(max_slides)
.max_bullets(max_bullets)
.include_images(!no_images)
.include_tables(!no_tables)
.include_code(!no_code);
let slides = crate::import::parse_html_with_options(&html_content, options)?;
if slides.is_empty() {
return Err("No slides found in HTML file".to_string());
}
if let Some(parent) = PathBuf::from(output).parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create directory: {e}"))?;
}
}
let title = title.unwrap_or("Presentation from HTML");
let pptx_data = generator::create_pptx_with_content(title, slides)
.map_err(|e| format!("Failed to generate PPTX: {e}"))?;
fs::write(output, pptx_data).map_err(|e| format!("Failed to write file: {e}"))?;
Ok(())
}
}
impl InfoCommand {
pub fn execute(file: &str) -> Result<(), String> {
let metadata = fs::metadata(file).map_err(|e| format!("File not found: {e}"))?;
let size = metadata.len();
let modified = metadata
.modified()
.ok()
.and_then(|t| t.elapsed().ok())
.map(|d| format!("{d:?} ago"))
.unwrap_or_else(|| "unknown".to_string());
println!("File Information");
println!("================");
println!("Path: {file}");
println!("Size: {size} bytes");
println!("Modified: {modified}");
let is_file = metadata.is_file();
println!("Is file: {is_file}");
if let Ok(content) = fs::read_to_string(file) {
if content.starts_with("<?xml") {
println!("\nPresentation Information");
println!("========================");
if let Some(title_start) = content.find("<title>") {
if let Some(title_end) = content[title_start + 7..].find("</title>") {
let title = &content[title_start + 7..title_start + 7 + title_end];
println!("Title: {title}");
}
}
if let Some(slides_start) = content.find("count=\"") {
let search_from = slides_start + 7;
if let Some(slides_end) = content[search_from..].find("\"") {
let count_str = &content[search_from..search_from + slides_end];
println!("Slides: {count_str}");
}
}
}
}
Ok(())
}
}
impl ValidateCommand {
pub fn execute(file: &str) -> Result<(), String> {
use std::io::Read;
use zip::ZipArchive;
println!("Validating PPTX file: {file}");
println!("{}", "=".repeat(60));
let metadata = fs::metadata(file).map_err(|e| format!("File not found: {e}"))?;
if !metadata.is_file() {
return Err(format!("Path is not a file: {file}"));
}
let file_handle = fs::File::open(file).map_err(|e| format!("Failed to open file: {e}"))?;
let mut archive =
ZipArchive::new(file_handle).map_err(|e| format!("Invalid ZIP archive: {e}"))?;
println!("✓ File is a valid ZIP archive");
println!(" Total entries: {}", archive.len());
let mut issues = Vec::new();
let mut found_files = std::collections::HashSet::new();
for i in 0..archive.len() {
let file = archive
.by_index(i)
.map_err(|e| format!("Failed to read archive entry: {e}"))?;
found_files.insert(file.name().to_string());
}
let required_files = vec![
"[Content_Types].xml",
"_rels/.rels",
"ppt/presentation.xml",
"docProps/core.xml",
];
println!("\nChecking required files...");
for required in &required_files {
if found_files.contains(*required) {
println!(" ✓ {}", required);
} else {
println!(" ✗ {} (missing)", required);
issues.push(format!("Missing required file: {}", required));
}
}
println!("\nChecking XML validity...");
for i in 0..archive.len() {
let mut file = archive
.by_index(i)
.map_err(|e| format!("Failed to read archive entry: {e}"))?;
let name = file.name().to_string();
if name.ends_with(".xml") || name.ends_with(".rels") {
let mut content = String::new();
file.read_to_string(&mut content)
.map_err(|e| format!("Failed to read XML file {}: {e}", name))?;
if content.trim().is_empty() {
issues.push(format!("Empty XML file: {}", name));
println!(" ⚠ {} (empty)", name);
} else if !content.contains("<?xml") && !name.ends_with(".rels") {
if !name.ends_with(".rels") {
issues.push(format!("XML file missing declaration: {}", name));
println!(" ⚠ {} (missing XML declaration)", name);
}
} else {
if content.contains("<") && content.contains(">") {
println!(" ✓ {} (valid XML)", name);
} else {
issues.push(format!("Invalid XML structure: {}", name));
println!(" ✗ {} (invalid XML)", name);
}
}
}
}
println!("\nChecking relationships...");
if found_files.contains("_rels/.rels") {
println!(" ✓ Package relationships found");
} else {
issues.push("Missing package relationships".to_string());
println!(" ✗ Package relationships missing");
}
println!("\n{}", "=".repeat(60));
if issues.is_empty() {
println!("✓ Validation PASSED");
println!(" File appears to be a valid PPTX file");
println!(" ECMA-376 compliance: OK");
} else {
println!("✗ Validation FAILED");
println!(" Found {} issue(s):", issues.len());
for issue in &issues {
println!(" - {}", issue);
}
return Err(format!("Validation failed with {} issue(s)", issues.len()));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::Path;
#[test]
fn test_create_command() {
let output = "/tmp/test_presentation.pptx";
let result = CreateCommand::execute(output, Some("Test"), 3, None);
assert!(result.is_ok());
assert!(Path::new(output).exists());
let _ = fs::remove_file(output);
}
#[test]
fn test_escape_xml() {
use crate::core::escape_xml;
assert_eq!(escape_xml("a & b"), "a & b");
assert_eq!(escape_xml("<tag>"), "<tag>");
assert_eq!(escape_xml("\"quoted\""), ""quoted"");
}
}