tiefdownlib 0.10.0

A library to manage and convert TiefDown projects.
Documentation
use std::{
    path::{Path, PathBuf},
    process::Command,
};

use color_eyre::eyre::{Result, eyre};
use log::debug;
use toml::Table;

use crate::{
    converters::common::{
        add_lua_filters, combine_pandoc_native, get_relative_path_from_compiled_dir,
        merge_preprocessors, preprocess_cli_args, retrieve_combined_output, retrieve_preprocessors,
        run_preprocessors_on_inputs, run_with_logging, write_combined_output,
    },
    manifest_model::{
        DEFAULT_CUSTOM_PROCESSOR_PREPROCESSORS, Injection, MetadataSettings, Processors, Template,
    },
    template_management::{get_output_path, get_template_path},
};

pub(crate) fn convert_epub(
    project_directory_path: &Path,
    compiled_directory_path: &Path,
    conversion_input_dir: &Path,
    template: &Template,
    metadata_fields: &Table,
    metadata_settings: &MetadataSettings,
    custom_processors: &Processors,
    injections: &Vec<Injection>,
) -> Result<PathBuf> {
    debug!("Starting EPUB conversion process.");

    let output_path = get_output_path(
        template.output.clone(),
        &template.name,
        template.template_type.clone(),
    )?;

    let template_path = get_template_path(template.template_file.clone(), &template.name);

    debug!(
        "Template path: {} | Output path: {}",
        compiled_directory_path.join(&template_path).display(),
        output_path.display()
    );

    debug!("Retrieving preprocessors...");
    let default_preprocessors = retrieve_preprocessors(
        &Some(DEFAULT_CUSTOM_PROCESSOR_PREPROCESSORS.0.clone()),
        &DEFAULT_CUSTOM_PROCESSOR_PREPROCESSORS.1,
    );
    let preprocessors =
        retrieve_preprocessors(&template.preprocessors, &custom_processors.preprocessors);
    let preprocessors = merge_preprocessors(vec![preprocessors, default_preprocessors]);
    debug!(
        "Selected preprocessors: {:?}",
        preprocessors
            .iter()
            .map(|p| p.name.clone())
            .collect::<Vec<String>>()
    );

    let combined_output: PathBuf = retrieve_combined_output(
        template,
        &Some(DEFAULT_CUSTOM_PROCESSOR_PREPROCESSORS.0.clone()),
    )?;
    debug!("Combined output file: {}", combined_output.display());

    debug!("Running preprocessors on inputs...");
    let results = run_preprocessors_on_inputs(
        template,
        project_directory_path,
        compiled_directory_path,
        conversion_input_dir,
        metadata_fields,
        metadata_settings,
        &preprocessors,
        injections,
    )?;

    let pandoc_native = combine_pandoc_native(results);

    write_combined_output(
        compiled_directory_path,
        &combined_output,
        &vec![pandoc_native],
    )?;

    debug!("Preparing pandoc command...");

    let mut processor_args = vec!["-t", "epub3", "-f", "native"]
        .iter()
        .map(|s| s.to_string())
        .collect::<Vec<String>>();
    if let Some(processor) = &template.processor {
        processor_args.append(&mut preprocess_cli_args(
            &custom_processors
                .processors
                .iter()
                .find(|p| p.name == *processor)
                .ok_or_else(|| eyre!("Processor {} not found in custom processors.", processor))?
                .processor_args,
            metadata_fields,
        ));
    }

    let mut pandoc = Command::new("pandoc");
    pandoc
        .current_dir(compiled_directory_path)
        .args(&processor_args)
        .arg("-o")
        .arg(&output_path);

    add_meta_args(metadata_fields, &mut pandoc)?;
    debug!("Added metadata fields to pandoc command.");

    add_css_files(
        project_directory_path,
        compiled_directory_path,
        &compiled_directory_path.join(&template_path),
        &mut pandoc,
    )?;
    debug!("Added CSS files from template directory if present.");

    add_fonts(
        project_directory_path,
        compiled_directory_path,
        &compiled_directory_path.join(&template_path),
        &mut pandoc,
    )?;
    debug!("Added embedded fonts if present.");

    add_lua_filters(
        template,
        project_directory_path,
        compiled_directory_path,
        &mut pandoc,
    )?;
    debug!("Added lua filters if configured.");

    pandoc.arg(&combined_output);

    run_with_logging(pandoc, "pandoc", false)?;

    let output_path = compiled_directory_path.join(output_path);
    debug!("EPUB result path: {}", output_path.display());

    Ok(output_path)
}

fn add_meta_args(metadata_fields: &Table, pandoc: &mut Command) -> Result<()> {
    for (key, value) in metadata_fields {
        if let Some(value) = value.as_str() {
            pandoc.arg("-M").arg(format!("{}:{}", key, value));
        } else {
            return Err(eyre!(
                "Metadata field {} is not a string, and is not supported by TiefDownConverter.",
                key
            ));
        }
    }

    Ok(())
}

fn add_css_files(
    project_directory_path: &Path,
    compiled_directory_path: &Path,
    template_path: &Path,
    pandoc: &mut Command,
) -> Result<()> {
    let files = template_path.read_dir()?;
    for file in files {
        let file = file?.path();
        if file.is_file() && file.extension().unwrap_or_default() == "css" {
            debug!("Adding CSS file to EPUB: {}", file.display());

            pandoc.arg("-c").arg(
                get_relative_path_from_compiled_dir(
                    &file,
                    project_directory_path,
                    compiled_directory_path,
                )
                .unwrap_or(file),
            );
        }
    }

    Ok(())
}

fn add_fonts(
    project_directory_path: &Path,
    compiled_directory_path: &Path,
    template_path: &Path,
    pandoc: &mut Command,
) -> Result<()> {
    let fonts_dir = template_path.join("fonts");

    if !fonts_dir.exists() {
        return Ok(());
    }

    let font_files = fonts_dir.read_dir()?;

    for font_file in font_files {
        let font_file = font_file?.path();
        if font_file.is_file()
            && ["ttf", "otf", "woff"]
                .contains(&&*font_file.extension().unwrap_or_default().to_string_lossy())
        {
            debug!("Embedding font in EPUB: {}", font_file.display());

            pandoc.arg("--epub-embed-font").arg(
                get_relative_path_from_compiled_dir(
                    &font_file,
                    project_directory_path,
                    compiled_directory_path,
                )
                .unwrap_or(font_file),
            );
        }
    }

    Ok(())
}