use crate::conversion_decider;
use crate::manifest_model::Manifest;
use crate::manifest_model::MarkdownProject;
use crate::manifest_model::MetadataSettings;
use crate::manifest_model::Processors;
use crate::manifest_model::TemplateMapping;
use crate::project_management::get_missing_dependencies;
use crate::project_management::load_and_convert_manifest;
use crate::project_management::run_smart_clean;
use chrono::prelude::DateTime;
use chrono::prelude::Utc;
use color_eyre::eyre::Result;
use color_eyre::eyre::eyre;
use fs_extra::dir;
use fs_extra::file;
use log::debug;
use log::error;
use log::info;
use log::warn;
use std::path::Path;
use std::path::PathBuf;
use toml::Table;
pub fn convert(
project: Option<PathBuf>,
templates: Option<Vec<String>>,
profile: Option<String>,
) -> Result<()> {
let pandoc_errors = get_missing_dependencies(vec!["pandoc"])?;
if !pandoc_errors.is_empty() {
error!("{}", pandoc_errors.join("\n"));
return Err(eyre!("Pandoc is not installed or not in the PATH."));
}
let other_dependencies = get_missing_dependencies(vec!["xelatex", "typst"])?;
if !other_dependencies.is_empty() {
warn!("{}", other_dependencies.join("\n"));
warn!(
"Some dependencies are missing. Some features may not work, and conversion may fail."
);
}
let project = project.unwrap_or(PathBuf::from("."));
if !project.exists() {
return Err(eyre!("Project path does not exist."));
}
let manifest_path = project.join("manifest.toml");
let manifest = load_and_convert_manifest(&manifest_path)?;
if let Some(true) = manifest.smart_clean {
let threshold = manifest.smart_clean_threshold.unwrap_or(5);
run_smart_clean(&project, threshold.saturating_sub(1))?;
}
info!("Converting project: {}", project.to_string_lossy());
let compiled_directory_path = create_build_directory(&project)?;
debug!(
"Converting in directory: {}",
compiled_directory_path.display()
);
for markdown_project in manifest
.markdown_projects
.clone()
.unwrap_or(vec![MarkdownProject {
name: "Default".to_string(),
path: PathBuf::from("Markdown"),
output: PathBuf::from("."),
metadata_fields: None,
default_profile: None,
resources: None,
}])
{
info!("Converting markdown project: {}", markdown_project.name);
let profile = profile.clone().or(markdown_project.default_profile.clone());
if let Some(ref profile) = profile {
debug!("Using profile: {}", profile);
}
let templates = get_template_names(&templates, profile, &manifest)?;
debug!("Templates selected ({}): {:?}", templates.len(), templates);
let templates = get_template_mappings_from_names(&templates, &manifest)?;
debug!("Resolved {} template mappings.", templates.len());
let markdown_project_compiled_directory_path =
compiled_directory_path.join(markdown_project.output.clone());
dir::create_all(&markdown_project_compiled_directory_path, false)?;
dir::copy(
project.join("template/"),
&markdown_project_compiled_directory_path,
&dir::CopyOptions::new().overwrite(true).content_only(true),
)?;
debug!("Copied template directory.");
let input_dir = project.join(markdown_project.path.clone());
copy_resources(
&markdown_project,
&markdown_project_compiled_directory_path,
&input_dir,
)?;
let shared_metadata = manifest.shared_metadata.clone().unwrap_or(Table::new());
let project_metadata = markdown_project.metadata_fields.unwrap_or(Table::new());
let merged_metadata = merge_metadata(&shared_metadata, &project_metadata);
debug!(
"Merged {} metadata fields ({} shared, {} project specific).",
merged_metadata.len(),
shared_metadata.len(),
project_metadata.len()
);
for template in &templates {
let conversion_input_dir =
&markdown_project_compiled_directory_path.join(template.name.clone() + "_convdir/");
debug!(
"Prepared conversion input directory: {}",
conversion_input_dir.display()
);
copy_markdown_directory(
&input_dir,
&conversion_input_dir,
&markdown_project.resources,
)?;
convert_template(
&markdown_project_compiled_directory_path,
template,
&project,
&conversion_input_dir,
&markdown_project.output,
&merged_metadata,
&manifest.metadata_settings,
&manifest.custom_processors,
)?;
}
}
Ok(())
}
fn copy_resources(
markdown_project: &MarkdownProject,
markdown_project_compiled_directory_path: &PathBuf,
markdown_dir: &PathBuf,
) -> Result<()> {
for resource in markdown_project.resources.clone().unwrap_or(vec![]) {
let resource = markdown_dir.join(resource.clone());
if !resource.exists() {
return Err(eyre!(
"Resource file {} does not exist.",
resource.display()
));
}
if resource.is_dir() {
debug!("Copying directory: {}", resource.display());
dir::copy(
resource,
markdown_project_compiled_directory_path,
&dir::CopyOptions::new().overwrite(true).content_only(false),
)?;
} else {
debug!("Copying file: {}", resource.display());
file::copy(
&resource,
&markdown_project_compiled_directory_path
.join(resource.file_name().unwrap_or(std::ffi::OsStr::new("."))),
&file::CopyOptions::new().overwrite(true),
)?;
}
}
Ok(())
}
fn merge_metadata(shared_metadata: &Table, project_metadata: &Table) -> Table {
let mut merged_metadata = shared_metadata.clone();
for (key, value) in project_metadata {
merged_metadata.insert(key.clone(), value.clone());
}
merged_metadata
}
fn get_template_names(
templates: &Option<Vec<String>>,
profile: Option<String>,
manifest: &Manifest,
) -> Result<Vec<String>> {
if let Some(templates) = templates {
debug!(
"get_template_names: using provided templates ({}).",
templates.len()
);
return Ok(templates.clone());
}
if let Some(profile) = profile {
if let Some(available_profiles) = &manifest.profiles {
if let Some(profile_pos) = available_profiles.iter().position(|p| p.name == profile) {
let resolved: Vec<String> = available_profiles[profile_pos]
.templates
.iter()
.map(|t| t.clone())
.collect();
debug!(
"get_template_names: using profile '{}' with {} templates.",
profile,
resolved.len()
);
return Ok(resolved);
} else {
return Err(eyre!("Profile '{}' could not be found.", profile));
}
} else {
return Err(eyre!("No profiles are defined in the manifest.toml file."));
}
}
let all: Vec<String> = manifest.templates.iter().map(|t| t.name.clone()).collect();
debug!(
"get_template_names: no explicit selection; using all templates ({}).",
all.len()
);
Ok(all)
}
fn get_template_mappings_from_names(
templates: &Vec<String>,
manifest: &Manifest,
) -> Result<Vec<TemplateMapping>> {
let templates = templates
.iter()
.map(|t| manifest.templates.iter().find(|mapping| mapping.name == *t))
.filter_map(|t| t.cloned())
.collect::<Vec<_>>();
debug!(
"get_template_mappings_from_names: resolved {} mappings from {} names.",
templates.len(),
templates.len()
);
Ok(templates)
}
fn create_build_directory(project_path: &Path) -> Result<std::path::PathBuf> {
let current_time = std::time::SystemTime::now();
let current_time: DateTime<Utc> = current_time.into();
let current_time = current_time.format("%Y-%m-%d_%H-%M-%S").to_string();
let build_directory_path = project_path.join(current_time);
dir::create_all(&build_directory_path, false)?;
debug!(
"Created build directory at '{}'.",
build_directory_path.display()
);
Ok(build_directory_path)
}
fn copy_markdown_directory(
markdown_dir: &Path,
output_dir: &Path,
resources: &Option<Vec<PathBuf>>,
) -> Result<()> {
debug!(
"Copying markdown directory '{}' to '{}'",
markdown_dir.display(),
output_dir.display()
);
if !output_dir.exists() {
dir::create_all(output_dir, false)?;
}
dir::copy(
markdown_dir,
output_dir,
&dir::CopyOptions::new().overwrite(true).content_only(true),
)?;
for resource in resources.clone().unwrap_or(vec![]) {
let resource = output_dir.join(resource.clone());
if resource.is_dir() {
dir::remove(resource)?;
} else {
file::remove(resource)?;
}
}
Ok(())
}
fn convert_template(
compiled_directory_path: &Path,
template: &TemplateMapping,
project_path: &Path,
conversion_input_dir: &Path,
output_dir: &Path,
metadata_fields: &Table,
metadata_settings: &Option<MetadataSettings>,
custom_processors: &Processors,
) -> Result<()> {
debug!("Starting template conversion for '{}'.", template.name);
debug!(" Template type: '{}'.", template.template_type);
let converter = conversion_decider::get_converter(&template.template_type)?;
let metadata_settings = metadata_settings
.clone()
.unwrap_or(MetadataSettings::default());
debug!("Running converter...");
let result_file_path = converter(
project_path,
compiled_directory_path,
conversion_input_dir,
template,
metadata_fields,
&metadata_settings,
custom_processors,
)?;
debug!("Converter finished.");
debug!(" Result file path: {}", result_file_path.display());
debug!("Copying result file to output directory...");
dir::create_all(project_path.join(output_dir), false)?;
file::copy(
&result_file_path,
project_path
.join(output_dir)
.join(result_file_path.file_name().unwrap_or_default()),
&file::CopyOptions::new().overwrite(true),
)?;
debug!("Copying finished.");
info!("Converted template: {}", template.name);
Ok(())
}