papr-pdf 0.1.0

A fast, keyboard-first CLI tool to compile Markdown, LaTeX, and plain text files into publication-grade PDFs.
//! Core compilation orchestrator and dispatch logic.

pub mod html_to_pdf;
pub mod latex;
pub mod markdown;
pub mod text;

use crate::cli::{DocumentFormat, Theme};
use anyhow::Result;
use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Instant;

/// Detects the target document format based on the file extension.
pub fn detect_format_from_path(path: &Path) -> DocumentFormat {
    match path.extension().and_then(|e| e.to_str()).map(|e| e.to_lowercase()).as_deref() {
        Some("md") | Some("markdown") | Some("mdown") => DocumentFormat::Markdown,
        Some("tex") | Some("latex") | Some("ltx") => DocumentFormat::Latex,
        _ => DocumentFormat::PlainText,
    }
}

/// Resolves the final destination path for the PDF output file.
pub fn determine_output_path(input_path: &Path, explicit_output: Option<PathBuf>) -> PathBuf {
    if let Some(out) = explicit_output {
        return out;
    }

    let stem = input_path
        .file_stem()
        .map(|s| s.to_string_lossy())
        .unwrap_or_else(|| "document".into());

    let parent = input_path.parent().unwrap_or_else(|| Path::new("."));
    parent.join(format!("{}.pdf", stem))
}

/// Compiles the input document into a PDF with a progress spinner and status report.
pub fn compile(
    input_path: &Path,
    output_path: &Path,
    format: DocumentFormat,
    theme: Theme,
) -> Result<()> {
    let start_time = Instant::now();

    let spinner = ProgressBar::new_spinner();
    spinner.set_style(
        ProgressStyle::default_spinner()
            .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
            .template("{spinner:.green} {msg}")
            .unwrap(),
    );

    let file_display = input_path.display().to_string();
    spinner.set_message(format!(
        "Compiling {} as {:?}...",
        file_display.bold(),
        format
    ));
    spinner.enable_steady_tick(std::time::Duration::from_millis(80));

    let compile_result = match format {
        DocumentFormat::Markdown => markdown::compile_markdown(input_path, output_path, theme),
        DocumentFormat::Latex => latex::compile_latex(input_path, output_path),
        DocumentFormat::PlainText => text::compile_plain_text(input_path, output_path, theme),
    };

    match compile_result {
        Ok(()) => {
            spinner.finish_and_clear();
            let elapsed = start_time.elapsed();
            let file_size_str = fs::metadata(output_path)
                .map(|m| format_file_size(m.len()))
                .unwrap_or_else(|_| "unknown size".into());

            println!(
                "{} Generated: {} ({}, {:.2}s)",
                "✔".bright_green().bold(),
                output_path.display().to_string().bright_cyan().bold(),
                file_size_str.dimmed(),
                elapsed.as_secs_f64()
            );
            Ok(())
        }
        Err(e) => {
            spinner.finish_and_clear();
            eprintln!("{} Compilation failed!", "✖".bright_red().bold());
            Err(e)
        }
    }
}

/// Formats byte counts into human-readable strings (e.g. "84.2 KB").
fn format_file_size(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;

    if bytes >= MB {
        format!("{:.1} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.1} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}