1use clap::{Parser as ClapParser, Subcommand, ValueEnum};
4
5#[derive(ClapParser, Debug)]
6#[command(name = "pptcli")]
7#[command(about = "PowerPoint Generator - Create, read, and update PowerPoint 2007+ (.pptx) files")]
8#[command(
9 long_about = "pptcli - A command-line tool for generating PowerPoint presentations from Markdown, webpages, or programmatically.
10
11Examples:
12 # Create a simple presentation
13 pptcli create output.pptx --title \"My Presentation\" --slides 5
14
15 # Convert Markdown to PowerPoint
16 pptcli md2ppt slides.md presentation.pptx
17
18 # Auto-generate output filename from Markdown
19 pptcli md2ppt slides.md
20
21 # Convert webpage to PowerPoint (requires --features web2ppt)
22 pptcli web2ppt https://example.com -o output.pptx
23
24 # Validate a PPTX file
25 pptcli validate presentation.pptx
26
27 # Show presentation information
28 pptcli info presentation.pptx"
29)]
30#[command(version)]
31pub struct Cli {
32 #[command(subcommand)]
33 pub command: Commands,
34}
35
36#[derive(Subcommand, Debug)]
37pub enum Commands {
38 #[command(
40 long_about = "Create a new PowerPoint presentation with the specified number of slides.
41
42Examples:
43 pptcli create output.pptx --title \"My Presentation\" --slides 5
44 pptcli create report.pptx --slides 10"
45 )]
46 Create {
47 #[arg(value_name = "FILE", help = "Path to the output PPTX file")]
49 output: String,
50
51 #[arg(long, help = "Title of the presentation (stored in metadata)")]
53 title: Option<String>,
54
55 #[arg(long, default_value_t = 1, help = "Number of blank slides to create")]
57 slides: usize,
58
59 #[arg(long, help = "Template PPTX file to use as base (not yet implemented)")]
61 template: Option<String>,
62 },
63
64 #[command(
66 name = "md2ppt",
67 alias = "from-md",
68 alias = "from-markdown",
69 long_about = "Convert a Markdown file to a PowerPoint presentation.
70
71Supported Markdown Features:
72 # Heading → New slide with title
73 ## Subheading → Bold bullet point
74 - Bullet → Bullet points (also *, +)
75 1. Numbered → Numbered list items
76 **bold** → Bold text
77 *italic* → Italic text
78 `code` → Inline code
79 > Blockquote → Speaker notes
80 | Table | → Tables (GFM style)
81 ```code``` → Code blocks (as shapes)
82 ```mermaid → Mermaid diagrams (12 types)
83 --- → Slide break (continuation)
84
85Example Markdown:
86 # Introduction
87 - Welcome to the presentation
88 - **Key point** with emphasis
89
90 # Data
91 | Name | Value |
92 |------|-------|
93 | A | 100 |
94
95 > Speaker notes go here
96
97Examples:
98 pptcli md2ppt slides.md presentation.pptx
99 pptcli md2ppt slides.md --title \"My Presentation\"
100 pptcli md2ppt slides.md # Auto-generates slides.pptx"
101 )]
102 Md2Ppt {
103 #[arg(value_name = "INPUT", help = "Path to the input Markdown file")]
105 input: String,
106
107 #[arg(value_name = "OUTPUT", help = "Path to the output PPTX file (default: INPUT.pptx)")]
109 output: Option<String>,
110
111 #[arg(long, help = "Title of the presentation (overrides Markdown content)")]
113 title: Option<String>,
114 },
115
116 #[command(
118 long_about = "Display information about a PPTX file.
119
120Shows file size, modification date, and basic metadata.
121
122Example:
123 pptcli info presentation.pptx"
124 )]
125 Info {
126 #[arg(value_name = "FILE", help = "Path to the PPTX file to inspect")]
128 file: String,
129 },
130
131 #[command(
133 long_about = "Validate a PPTX file structure and content.
134
135Checks for:
136- Valid ZIP structure
137- Required parts (presentation.xml, slide masters, etc.)
138- Content types
139- Relationships"
140 )]
141 Validate {
142 #[arg(value_name = "FILE")]
144 file: String,
145 },
146
147 #[command(
149 long_about = "Export PPTX to PDF, HTML, or images.
150
151Formats:
152- pdf: Requires LibreOffice installed
153- html: Self-contained HTML slideshow
154- png: Requires LibreOffice and pdftoppm"
155 )]
156 Export {
157 #[arg(value_name = "INPUT")]
159 input: String,
160
161 #[arg(value_name = "OUTPUT")]
163 output: String,
164
165 #[arg(long, value_enum)]
167 format: Option<ExportFormat>,
168 },
169
170 #[command(
172 long_about = "Merge multiple PPTX files into one.
173
174Slides from all input files will be appended in order."
175 )]
176 Merge {
177 #[arg(short, long)]
179 output: String,
180
181 #[arg(value_name = "INPUTS", required = true, num_args = 1..)]
183 inputs: Vec<String>,
184 },
185
186 #[command(
188 name = "pdf2ppt",
189 long_about = "Convert PDF pages to PowerPoint slides.
190
191Requires `pdftoppm` (poppler) installed.
192Each page becomes a slide with the page image."
193 )]
194 Pdf2Ppt {
195 #[arg(value_name = "INPUT")]
197 input: String,
198
199 #[arg(value_name = "OUTPUT")]
201 output: Option<String>,
202 },
203
204 #[cfg(feature = "web2ppt")]
206 #[command(
207 name = "web2ppt",
208 long_about = "Convert a webpage to a PowerPoint presentation.
209
210Extracts:
211- Title and headings
212- Text content
213- Images
214- Tables
215- Code blocks"
216 )]
217 Web2Ppt {
218 #[arg(value_name = "URL")]
220 url: String,
221
222 #[arg(short, long, default_value = "output.pptx")]
224 output: String,
225
226 #[arg(long)]
228 title: Option<String>,
229
230 #[arg(long, default_value_t = 20)]
232 max_slides: usize,
233
234 #[arg(long, default_value_t = 7)]
236 max_bullets: usize,
237
238 #[arg(long)]
240 no_images: bool,
241
242 #[arg(long)]
244 no_tables: bool,
245
246 #[arg(long)]
248 no_code: bool,
249
250 #[arg(long)]
252 no_source_url: bool,
253
254 #[arg(long, default_value_t = 30)]
256 timeout: u64,
257
258 #[arg(short, long)]
260 verbose: bool,
261 },
262}
263
264#[derive(ValueEnum, Clone, Debug)]
265pub enum ExportFormat {
266 Pdf,
267 Html,
268 Png,
269}
270
271#[derive(Debug, Clone)]
273pub struct CreateArgs {
274 pub output: String,
275 pub title: Option<String>,
276 pub slides: usize,
277 pub template: Option<String>,
278}
279
280#[derive(Debug, Clone)]
281pub struct FromMarkdownArgs {
282 pub input: String,
283 pub output: String,
284 pub title: Option<String>,
285}
286
287#[derive(Debug, Clone)]
288pub struct Md2PptArgs {
289 pub input: String,
290 pub output: Option<String>,
291 pub title: Option<String>,
292}
293
294#[derive(Debug, Clone)]
295pub struct InfoArgs {
296 pub file: String,
297}
298
299#[derive(Debug, Clone)]
300pub struct ValidateArgs {
301 pub file: String,
302}
303
304#[derive(Debug, Clone)]
305pub struct Web2PptArgs {
306 pub url: String,
307 pub output: String,
308 pub title: Option<String>,
309 pub max_slides: usize,
310 pub max_bullets: usize,
311 pub no_images: bool,
312 pub no_tables: bool,
313 pub no_code: bool,
314 pub no_source_url: bool,
315 pub timeout: u64,
316 pub verbose: bool,
317}
318
319#[derive(Debug, Clone)]
320pub struct ExportArgs {
321 pub input: String,
322 pub output: String,
323 pub format: Option<ExportFormat>,
324}
325
326#[derive(Debug, Clone)]
327pub struct MergeArgs {
328 pub output: String,
329 pub inputs: Vec<String>,
330}
331
332#[derive(Debug, Clone)]
333pub struct Pdf2PptArgs {
334 pub input: String,
335 pub output: Option<String>,
336}
337
338#[derive(Debug, Clone)]
339pub enum Command {
340 Create(CreateArgs),
341 FromMarkdown(FromMarkdownArgs),
342 Md2Ppt(Md2PptArgs),
343 Info(InfoArgs),
344 Validate(ValidateArgs),
345 Web2Ppt(Web2PptArgs),
346 Export(ExportArgs),
347 Merge(MergeArgs),
348 Pdf2Ppt(Pdf2PptArgs),
349}
350
351impl From<Commands> for Command {
352 fn from(cmd: Commands) -> Self {
353 match cmd {
354 Commands::Create { output, title, slides, template } => {
355 Command::Create(CreateArgs {
356 output,
357 title,
358 slides,
359 template,
360 })
361 }
362 Commands::Md2Ppt { input, output, title } => {
363 let output = output.unwrap_or_else(|| {
365 use std::path::Path;
366 let input_path = Path::new(&input);
367 if let Some(stem) = input_path.file_stem() {
368 if let Some(parent) = input_path.parent() {
369 if parent.as_os_str().is_empty() {
370 format!("{}.pptx", stem.to_string_lossy())
371 } else {
372 format!("{}/{}.pptx", parent.display(), stem.to_string_lossy())
373 }
374 } else {
375 format!("{}.pptx", stem.to_string_lossy())
376 }
377 } else {
378 format!("{}.pptx", input)
379 }
380 });
381
382 Command::FromMarkdown(FromMarkdownArgs {
383 input,
384 output,
385 title,
386 })
387 }
388 Commands::Info { file } => {
389 Command::Info(InfoArgs { file })
390 }
391 Commands::Validate { file } => {
392 Command::Validate(ValidateArgs { file })
393 }
394 Commands::Web2Ppt { url, output, title, max_slides, max_bullets, no_images, no_tables, no_code, no_source_url, timeout, verbose } => {
395 Command::Web2Ppt(Web2PptArgs {
396 url,
397 output,
398 title,
399 max_slides,
400 max_bullets,
401 no_images,
402 no_tables,
403 no_code,
404 no_source_url,
405 timeout,
406 verbose,
407 })
408 }
409 Commands::Export { input, output, format } => {
410 Command::Export(ExportArgs {
411 input,
412 output,
413 format,
414 })
415 }
416 Commands::Merge { output, inputs } => {
417 Command::Merge(MergeArgs {
418 output,
419 inputs,
420 })
421 }
422 Commands::Pdf2Ppt { input, output } => {
423 Command::Pdf2Ppt(Pdf2PptArgs {
424 input,
425 output,
426 })
427 }
428 }
429 }
430}
431
432pub struct LegacyParser;
434
435impl LegacyParser {
436 pub fn parse(args: &[String]) -> Result<Command, String> {
437 let cli = Cli::parse_from(std::iter::once(&"pptcli".to_string()).chain(args.iter()));
438 Ok(cli.command.into())
439 }
440}
441
442pub use LegacyParser as Parser;
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 #[test]
450 fn test_parse_create() {
451 let args = vec![
452 "pptcli".to_string(),
453 "create".to_string(),
454 "test.pptx".to_string(),
455 "--title".to_string(),
456 "My Presentation".to_string(),
457 ];
458 let cli = Cli::parse_from(args.iter());
459 match cli.command {
460 Commands::Create { output, title, .. } => {
461 assert_eq!(output, "test.pptx");
462 assert_eq!(title, Some("My Presentation".to_string()));
463 }
464 _ => panic!("Expected Create command"),
465 }
466 }
467
468 #[test]
469 fn test_parse_md2ppt_with_output() {
470 let args = vec![
471 "pptcli".to_string(),
472 "md2ppt".to_string(),
473 "input.md".to_string(),
474 "output.pptx".to_string(),
475 "--title".to_string(),
476 "From Markdown".to_string(),
477 ];
478 let cli = Cli::parse_from(args.iter());
479 match cli.command {
480 Commands::Md2Ppt { input, output, title } => {
481 assert_eq!(input, "input.md");
482 assert_eq!(output, Some("output.pptx".to_string()));
483 assert_eq!(title, Some("From Markdown".to_string()));
484 }
485 _ => panic!("Expected Md2Ppt command"),
486 }
487 }
488
489 #[test]
490 fn test_parse_md2ppt_auto_output() {
491 let args = vec![
492 "pptcli".to_string(),
493 "md2ppt".to_string(),
494 "input.md".to_string(),
495 "--title".to_string(),
496 "From Markdown".to_string(),
497 ];
498 let cli = Cli::parse_from(args.iter());
499 match cli.command {
500 Commands::Md2Ppt { input, output, title } => {
501 assert_eq!(input, "input.md");
502 assert_eq!(output, None);
503 assert_eq!(title, Some("From Markdown".to_string()));
504 }
505 _ => panic!("Expected Md2Ppt command"),
506 }
507 }
508
509 #[test]
510 fn test_parse_from_md_alias() {
511 let args = vec![
512 "pptcli".to_string(),
513 "from-md".to_string(),
514 "input.md".to_string(),
515 "output.pptx".to_string(),
516 ];
517 let cli = Cli::parse_from(args.iter());
518 match cli.command {
519 Commands::Md2Ppt { input, output, .. } => {
520 assert_eq!(input, "input.md");
521 assert_eq!(output, Some("output.pptx".to_string()));
522 }
523 _ => panic!("Expected Md2Ppt command via from-md alias"),
524 }
525 }
526
527 #[test]
528 fn test_parse_info() {
529 let args = vec![
530 "pptcli".to_string(),
531 "info".to_string(),
532 "test.pptx".to_string(),
533 ];
534 let cli = Cli::parse_from(args.iter());
535 match cli.command {
536 Commands::Info { file } => {
537 assert_eq!(file, "test.pptx");
538 }
539 _ => panic!("Expected Info command"),
540 }
541 }
542}