maimai 0.1.1

Markup-based meme generator
Documentation
use std::{
    ops::Range,
    process::{ExitCode, Termination},
};

use camino::{Utf8Path, Utf8PathBuf};
use clap::Parser as _;
use indexmap::IndexMap;
use maimai::{ErrorKind, FileProvider, FsFileProvider, IncompleteMemeDefinition};
use tracing_subscriber::EnvFilter;

fn main() -> CliResult {
    fn _main() -> maimai::Result<()> {
        let cli = Cli::parse();
        tracing_subscriber::fmt()
            .with_env_filter(
                EnvFilter::try_from_default_env()
                    .or_else(|_| {
                        EnvFilter::try_new(if cli.verbose {
                            "info,maimai=debug"
                        } else if cli.quiet {
                            "error,maimai=warn"
                        } else {
                            "warn,maimai=info"
                        })
                    })
                    .map_err(maimai::Error::other)?,
            )
            .init();

        for input_path in cli.input_files {
            let output_path = match &cli.output_dir {
                Some(dir) => dir.join(
                    input_path
                        .with_extension(cli.format.as_ref())
                        .file_name()
                        .ok_or_else(|| maimai::Error::other("input filename must not be none"))?,
                ),
                None => input_path.with_extension(cli.format.as_ref()),
            };

            maimai::Meme::from_files(&FsFileProvider, &input_path)?
                .render_to_file(&output_path, cli.debug)?;
        }

        Ok(())
    }

    CliResult(_main())
}

#[derive(Debug, clap::Parser)]
pub struct Cli {
    input_files: Vec<Utf8PathBuf>,
    #[arg(long, default_value_t = ImageFormat::Webp)]
    format: ImageFormat,
    #[arg(long)]
    output_dir: Option<Utf8PathBuf>,

    /// Add colored debug lines to the image
    #[arg(long)]
    debug: bool,

    /// Show debug logs
    #[arg(short, long)]
    verbose: bool,

    /// Show less logs
    #[arg(short, long)]
    quiet: bool,
}

#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display, strum::EnumString, strum::AsRefStr)]
#[strum(serialize_all = "lowercase")]
pub enum ImageFormat {
    Png,
    #[strum(to_string = "jpg", serialize = "jpeg")]
    Jpeg,
    Webp,
}

impl From<ImageFormat> for image::ImageFormat {
    fn from(format: ImageFormat) -> Self {
        match format {
            ImageFormat::Png => image::ImageFormat::Png,
            ImageFormat::Jpeg => image::ImageFormat::Jpeg,
            ImageFormat::Webp => image::ImageFormat::WebP,
        }
    }
}

struct CliResult(maimai::Result<()>);

impl Termination for CliResult {
    fn report(self) -> std::process::ExitCode {
        match &self.0 {
            Ok(()) => ExitCode::SUCCESS,
            Err(_) => {
                eprintln!("{self}");
                ExitCode::FAILURE
            }
        }
    }
}

impl std::fmt::Display for CliResult {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Err(error) = &self.0 {
            let path = error.path();
            let mut sources = ariadne::FnCache::new(
                (|path| FsFileProvider.read_string(path))
                    as fn(&&Utf8Path) -> std::io::Result<String>,
            );

            match error.kind() {
                ErrorKind::Parse(e) => {
                    if let Some(span) = e.span() {
                        let path = path.expect("missing file path");
                        let report = ariadne::Report::build(
                            ariadne::ReportKind::Error,
                            (path, span.clone()),
                        )
                        .with_message(error)
                        .with_label(ariadne::Label::new((path, span)).with_message(e.message()))
                        .finish();

                        let mut buf = Vec::new();
                        report.write(sources, &mut buf).map_err(|e| {
                            tracing::error!(?e);
                            std::fmt::Error
                        })?;

                        f.write_str(&String::from_utf8_lossy(&buf))?;
                    }
                }
                ErrorKind::Incomplete(errors) => {
                    let path = path.expect("missing file path");
                    let doc: toml_edit::ImDocument<String> =
                        FsFileProvider.read_string(path).unwrap().parse().unwrap();
                    let mut buf = Vec::new();
                    let mut spans: IndexMap<
                        (&Utf8Path, Range<usize>),
                        Vec<&IncompleteMemeDefinition>,
                    > = IndexMap::new();

                    for e in errors {
                        let mut item = doc.as_item();

                        for key in e.keys() {
                            if let Some(child) = item.get(key.as_ref()) {
                                if item.span().is_some() {
                                    item = child
                                }
                            }
                        }
                        spans
                            .entry((path, item.span().unwrap_or(0..0)))
                            .or_default()
                            .push(e);
                    }

                    for (span, es) in spans {
                        ariadne::Report::build(ariadne::ReportKind::Error, span.clone())
                            .with_message(error)
                            .with_labels(
                                es.into_iter()
                                    .map(|e| ariadne::Label::new(span.clone()).with_message(e)),
                            )
                            .finish()
                            .write(&mut sources, &mut buf)
                            .map_err(|e| {
                                tracing::error!(?e);
                                std::fmt::Error
                            })?;
                    }

                    f.write_str(&String::from_utf8_lossy(&buf))?;
                }
                _ => {
                    write!(f, "Error: {error}")?;
                    let mut e: &dyn std::error::Error = error;
                    while let Some(source) = e.source() {
                        e = source;
                        write!(f, "\n{e}")?;
                    }
                }
            }
        }

        Ok(())
    }
}