ppt_rs/cli/
parser.rs

1//! Command-line argument parser using clap
2
3use 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    /// Create a new presentation
39    #[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        /// Output file path (.pptx)
48        #[arg(value_name = "FILE", help = "Path to the output PPTX file")]
49        output: String,
50        
51        /// Presentation title
52        #[arg(long, help = "Title of the presentation (stored in metadata)")]
53        title: Option<String>,
54        
55        /// Number of slides to create
56        #[arg(long, default_value_t = 1, help = "Number of blank slides to create")]
57        slides: usize,
58        
59        /// Template file to use
60        #[arg(long, help = "Template PPTX file to use as base (not yet implemented)")]
61        template: Option<String>,
62    },
63    
64    /// Generate PPTX from Markdown file
65    #[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        /// Input markdown file
104        #[arg(value_name = "INPUT", help = "Path to the input Markdown file")]
105        input: String,
106        
107        /// Output PPTX file (optional: auto-generated from input if not provided)
108        #[arg(value_name = "OUTPUT", help = "Path to the output PPTX file (default: INPUT.pptx)")]
109        output: Option<String>,
110        
111        /// Presentation title
112        #[arg(long, help = "Title of the presentation (overrides Markdown content)")]
113        title: Option<String>,
114    },
115    
116    /// Show presentation information
117    #[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        /// PPTX file to inspect
127        #[arg(value_name = "FILE", help = "Path to the PPTX file to inspect")]
128        file: String,
129    },
130    
131    /// Validate a PPTX file
132    #[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        /// PPTX file to validate
143        #[arg(value_name = "FILE")]
144        file: String,
145    },
146    
147    /// Export presentation to other formats
148    #[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        /// Input PPTX file
158        #[arg(value_name = "INPUT")]
159        input: String,
160        
161        /// Output file path
162        #[arg(value_name = "OUTPUT")]
163        output: String,
164        
165        /// Output format (overrides extension)
166        #[arg(long, value_enum)]
167        format: Option<ExportFormat>,
168    },
169    
170    /// Merge multiple presentations
171    #[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        /// Output PPTX file
178        #[arg(short, long)]
179        output: String,
180        
181        /// Input PPTX files
182        #[arg(value_name = "INPUTS", required = true, num_args = 1..)]
183        inputs: Vec<String>,
184    },
185    
186    /// Convert PDF to PowerPoint
187    #[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        /// Input PDF file
196        #[arg(value_name = "INPUT")]
197        input: String,
198        
199        /// Output PPTX file
200        #[arg(value_name = "OUTPUT")]
201        output: Option<String>,
202    },
203
204    /// Generate PPTX from webpage (requires web2ppt feature)
205    #[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        /// URL to convert
219        #[arg(value_name = "URL")]
220        url: String,
221        
222        /// Output file path (.pptx)
223        #[arg(short, long, default_value = "output.pptx")]
224        output: String,
225        
226        /// Presentation title (overrides page title)
227        #[arg(long)]
228        title: Option<String>,
229        
230        /// Maximum number of slides to generate
231        #[arg(long, default_value_t = 20)]
232        max_slides: usize,
233        
234        /// Maximum bullet points per slide
235        #[arg(long, default_value_t = 7)]
236        max_bullets: usize,
237        
238        /// Disable image extraction
239        #[arg(long)]
240        no_images: bool,
241        
242        /// Disable table extraction
243        #[arg(long)]
244        no_tables: bool,
245        
246        /// Disable code block extraction
247        #[arg(long)]
248        no_code: bool,
249        
250        /// Don't add source URL to last slide
251        #[arg(long)]
252        no_source_url: bool,
253        
254        /// Request timeout in seconds
255        #[arg(long, default_value_t = 30)]
256        timeout: u64,
257        
258        /// Enable verbose logging
259        #[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// Legacy types for backward compatibility with existing command execution code
272#[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                // If output is not provided, auto-generate it
364                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
432// Legacy Parser for backward compatibility
433pub 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
442// Alias for backward compatibility
443pub 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}