use std::path::PathBuf;
use clap::{crate_version, ArgEnum, Parser};
use logid::{
capturing::{LogIdTracing, MappedLogId},
log_id::LogId,
};
use serde::{Deserialize, Serialize};
use strum_macros::EnumString;
use crate::{
elements::types::ElementType,
log_id::{ConfigErrLogId, CORE_LOG_ID_MAP},
};
const UNIMARKUP_NAME: &str = "Unimarkup";
#[derive(Debug, PartialEq, Eq, Clone, Parser, Default, Serialize, Deserialize)]
#[clap(name = UNIMARKUP_NAME, version = crate_version!())]
pub struct Config {
#[clap(
index = 1,
value_name = "UM-FILE",
required = true,
takes_value = true,
parse(from_os_str)
)]
#[serde(skip)]
pub um_file: PathBuf,
#[clap(
index = 2,
value_name = "OUTPUT-FILE",
takes_value = true,
parse(from_os_str)
)]
#[serde(alias = "OUTPUT-FILE")]
#[serde(default)]
pub out_file: Option<PathBuf>,
#[clap(
name = "output-formats",
display_order = 1,
long = "output-formats",
alias = "formats",
takes_value = true,
use_value_delimiter = true,
arg_enum
)]
#[serde(alias = "output-formats")]
#[serde(alias = "formats")]
#[serde(default)]
pub out_formats: Option<Vec<OutputFormat>>,
#[clap(
display_order = 2,
long = "insert-paths",
takes_value = true,
use_value_delimiter = true,
parse(from_os_str)
)]
#[serde(alias = "insert-paths")]
#[serde(default)]
pub insert_paths: Option<Vec<PathBuf>>,
#[clap(
display_order = 3,
long = "dot-unimarkup",
alias = "config",
takes_value = true,
env = "UNIMARKUP_CONFIG",
parse(from_os_str)
)]
#[serde(alias = "dot-unimarkup")]
#[serde(alias = "config")]
#[serde(default)]
pub dot_unimarkup: Option<PathBuf>,
#[clap(
display_order = 5,
short = 't',
long = "theme",
takes_value = true,
parse(from_os_str)
)]
#[serde(alias = "theme")]
#[serde(default)]
pub theme: Option<PathBuf>,
#[clap(
display_order = 6,
short = 'f',
long = "flags",
takes_value = true,
use_value_delimiter = true
)]
#[serde(alias = "flags")]
#[serde(default)]
pub flags: Option<Vec<String>>,
#[clap(
display_order = 7,
long = "enable-elements",
takes_value = true,
use_value_delimiter = true,
arg_enum
)]
#[serde(alias = "enable-elements")]
#[serde(default)]
pub enable_elements: Option<Vec<ElementType>>,
#[clap(
display_order = 8,
long = "disable-elements",
takes_value = true,
use_value_delimiter = true,
arg_enum
)]
#[serde(alias = "disable-elements")]
#[serde(default)]
pub disable_elements: Option<Vec<ElementType>>,
#[clap(
display_order = 30,
long = "citation-style",
alias = "csl",
takes_value = true,
requires = "references",
parse(from_os_str)
)]
#[serde(alias = "citation-style")]
#[serde(alias = "csl")]
#[serde(default)]
pub citation_style: Option<PathBuf>,
#[clap(
display_order = 31,
long = "references",
alias = "refs",
takes_value = true,
use_value_delimiter = true,
requires = "citation-style",
parse(from_os_str)
)]
#[serde(alias = "references")]
#[serde(alias = "refs")]
#[serde(default)]
pub references: Option<Vec<PathBuf>>,
#[clap(
display_order = 10,
long = "fonts",
alias = "ttf",
alias = "woff",
takes_value = true,
use_value_delimiter = true,
parse(from_os_str)
)]
#[serde(alias = "fonts")]
#[serde(alias = "ttf")]
#[serde(alias = "woff")]
#[serde(default)]
pub fonts: Option<Vec<PathBuf>>,
#[clap(
display_order = 1,
short = 'w',
long = "overwrite-out-files",
takes_value = false
)]
#[serde(alias = "overwrite-out-files")]
#[serde(default)]
pub overwrite_out_files: bool,
#[clap(display_order = 2, short = 'c', long = "clean", takes_value = false)]
#[serde(alias = "clean")]
#[serde(alias = "c")]
#[serde(default)]
pub clean: bool,
#[clap(display_order = 3, short = 'r', long = "rebuild", takes_value = false)]
#[serde(alias = "rebuild")]
#[serde(default)]
pub rebuild: bool,
#[clap(
display_order = 20,
long = "replace-preamble",
requires = "output-formats",
takes_value = false
)]
#[serde(skip)]
pub replace_preamble: bool,
#[clap(
display_order = 20,
long = "relative-insert-prefix",
alias = "insert-prefix",
takes_value = true,
parse(from_os_str)
)]
#[serde(alias = "relative-insert-prefix")]
#[serde(alias = "insert-prefix")]
#[serde(default)]
pub relative_insert_prefix: Option<PathBuf>,
#[clap(
display_order = 40,
long = "html-template",
takes_value = true,
parse(from_os_str)
)]
#[serde(alias = "html-template")]
#[serde(default)]
pub html_template: Option<PathBuf>,
#[clap(
display_order = 41,
long = "html-mathmode",
takes_value = true,
arg_enum
)]
#[serde(alias = "html-mathmode")]
#[serde(default)]
pub html_mathmode: Option<HtmlMathmode>,
#[clap(display_order = 40, long = "html-embed-svg", takes_value = false)]
#[serde(alias = "html-embed-svg")]
#[serde(default)]
pub html_embed_svg: bool,
}
#[derive(
Debug, PartialEq, Eq, Clone, EnumString, ArgEnum, strum_macros::Display, Serialize, Deserialize,
)]
pub enum OutputFormat {
#[strum(ascii_case_insensitive)]
Pdf,
#[strum(ascii_case_insensitive)]
Html,
#[strum(ascii_case_insensitive, serialize = "reveal-js")]
#[clap(alias = "revealjs")]
RevealJs,
#[strum(ascii_case_insensitive)]
Intermediate,
}
#[derive(
Debug, PartialEq, Eq, Clone, EnumString, ArgEnum, strum_macros::Display, Serialize, Deserialize,
)]
pub enum HtmlMathmode {
#[strum(ascii_case_insensitive)]
Svg,
#[strum(ascii_case_insensitive)]
Embed,
#[strum(ascii_case_insensitive)]
Cdn,
}
trait ReplaceIfNone<T> {
fn replace_none(&mut self, other: Option<T>);
}
impl<T> ReplaceIfNone<T> for Option<T> {
fn replace_none(&mut self, other: Option<T>) {
if self.is_none() {
*self = other;
}
}
}
impl Config {
pub fn merge(&mut self, other: Config) {
self.out_file.replace_none(other.out_file);
self.out_formats.replace_none(other.out_formats);
self.insert_paths.replace_none(other.insert_paths);
self.dot_unimarkup.replace_none(other.dot_unimarkup);
self.theme.replace_none(other.theme);
self.flags.replace_none(other.flags);
self.enable_elements.replace_none(other.enable_elements);
self.disable_elements.replace_none(other.disable_elements);
self.citation_style.replace_none(other.citation_style);
self.references.replace_none(other.references);
self.fonts.replace_none(other.fonts);
self.overwrite_out_files |= other.overwrite_out_files;
self.clean |= other.clean;
self.rebuild |= other.rebuild;
self.relative_insert_prefix
.replace_none(other.relative_insert_prefix);
self.html_template.replace_none(other.html_template);
self.html_mathmode.replace_none(other.html_mathmode);
self.html_embed_svg |= other.html_embed_svg;
}
pub fn validate_config(&mut self) -> Result<(), MappedLogId> {
if let Some(ref file) = self.out_file {
if file.exists() && !self.overwrite_out_files {
return Err((ConfigErrLogId::InvalidConfig as LogId).set_event_with(
&CORE_LOG_ID_MAP,
"Option `overwrite-out-files` must be `true` if output file exists.",
file!(),
line!(),
));
}
}
if let Some(ref paths) = self.insert_paths {
for path in paths {
if !path.exists() {
return Err((ConfigErrLogId::InvalidPath as LogId).set_event_with(
&CORE_LOG_ID_MAP,
&format!("Invalid path given for `insert-paths`: {:?}", path),
file!(),
line!(),
));
}
}
}
if let Some(ref path) = self.dot_unimarkup {
if !path.is_dir() {
return Err((ConfigErrLogId::InvalidPath as LogId).set_event_with(
&CORE_LOG_ID_MAP,
&format!("Invalid path given for `dot-unimarkup`: {:?}", path),
file!(),
line!(),
));
}
}
if let Some(ref file) = self.theme {
if !file.exists() {
return Err((ConfigErrLogId::InvalidFile as LogId).set_event_with(
&CORE_LOG_ID_MAP,
&format!("Invalid file given for `theme`: {:?}", file),
file!(),
line!(),
));
}
}
if let Some(ref file) = self.citation_style {
if !file.exists() {
return Err((ConfigErrLogId::InvalidFile as LogId).set_event_with(
&CORE_LOG_ID_MAP,
&format!("Invalid file given for `citation-style`: {:?}", file),
file!(),
line!(),
));
}
}
if let Some(ref files) = self.references {
for file in files {
if !file.exists() {
return Err((ConfigErrLogId::InvalidFile as LogId).set_event_with(
&CORE_LOG_ID_MAP,
&format!("Invalid file given for `references`: {:?}", file),
file!(),
line!(),
));
}
}
}
if let Some(ref files) = self.fonts {
for file in files {
if !file.exists() {
return Err((ConfigErrLogId::InvalidFile as LogId).set_event_with(
&CORE_LOG_ID_MAP,
&format!("Invalid file given for `fonts`: {:?}", file),
file!(),
line!(),
));
}
}
}
if let Some(ref file) = self.html_template {
if !file.exists() {
return Err((ConfigErrLogId::InvalidFile as LogId).set_event_with(
&CORE_LOG_ID_MAP,
&format!("Invalid file given for `html-template`: {:?}", file),
file!(),
line!(),
));
}
}
if !self.um_file.exists() {
return Err((ConfigErrLogId::InvalidFile as LogId).set_event_with(
&CORE_LOG_ID_MAP,
"Set `um-file` does not exist!",
file!(),
line!(),
));
}
Ok(())
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test__validate__valid_config() {
let mut cfg: Config = Config::parse_from(vec![
"unimarkup",
"--output-formats=html",
"--dot-unimarkup=tests/test_files/",
"tests/test_files/frontend/heading1.um",
]);
let result = cfg.validate_config();
assert!(result.is_ok(), "Cause: {:?}", result.unwrap_err());
}
#[should_panic]
#[test]
fn test__validate__invalid_config() {
let mut cfg: Config = Config::parse_from(vec![
"unimarkup",
"--output-formats=html",
"--dot-unimarkup=shouldfail",
"tests/test_files/frontend/heading1.um",
]);
cfg.validate_config().unwrap();
}
#[test]
fn test__validate__valid_multi_path_config() {
let mut cfg: Config = Config::parse_from(vec![
"unimarkup",
"--output-formats=html",
"--dot-unimarkup=tests/test_files/",
"--insert-paths=tests/test_files/,tests/test_files/",
"tests/test_files/frontend/heading1.um",
]);
let result = cfg.validate_config();
assert!(result.is_ok(), "Cause: {:?}", result.unwrap_err());
}
#[should_panic]
#[test]
fn test__validate__invalid_multi_path_config() {
let mut cfg: Config = Config::parse_from(vec![
"unimarkup",
"--output-formats=html",
"--dot-unimarkup=tests/test_files/",
"--insert-paths=shouldfail,tests/test_files/",
"tests/test_files/frontend/heading1.um",
]);
cfg.validate_config().unwrap();
}
}