use std::cmp::{Eq, PartialEq};
use std::fmt::{Debug, Display, Formatter};
use std::hash::{Hash, Hasher};
use anyhow::anyhow;
use clap::ValueEnum;
use dyn_clone::DynClone;
use serde::{Deserialize, Serialize};
use crate::loader::{Loader, MarkdownLoader, NotebookLoader};
use crate::renderers::generic::GenericRenderer;
use crate::renderers::notebook::NotebookRenderer;
use crate::renderers::DocumentRenderer;
#[derive(Hash, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Debug, ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum InputFormat {
Markdown,
Notebook,
}
#[typetag::serde]
pub trait Format: DynClone + Debug + Send + Sync {
fn extension(&self) -> &str;
fn template_prefix(&self) -> &str;
fn name(&self) -> &str;
fn no_parse(&self) -> bool;
fn renderer(&self) -> Box<dyn DocumentRenderer>;
fn include_resources(&self) -> bool;
fn layout(&self) -> Option<String>;
}
impl PartialEq for dyn Format {
fn eq(&self, other: &Self) -> bool {
self.name() == other.name()
}
}
impl Hash for dyn Format {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name().hash(state)
}
}
impl Eq for dyn Format {}
impl Display for dyn Format {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
dyn_clone::clone_trait_object!(Format);
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NotebookFormat {}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct HtmlFormat {}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct InfoFormat {}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MarkdownFormat {}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LaTexFormat {}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DynamicFormat {
pub extension: String,
pub template_prefix: String,
pub name: String,
#[serde(default = "default_renderer")]
pub renderer: Box<dyn DocumentRenderer>,
#[serde(default)]
pub include_resources: bool,
pub layout: Option<String>,
}
fn default_renderer() -> Box<dyn DocumentRenderer> {
Box::<GenericRenderer>::default()
}
#[typetag::serde(name = "dynamic")]
impl Format for DynamicFormat {
fn extension(&self) -> &str {
&self.extension
}
fn template_prefix(&self) -> &str {
&self.template_prefix
}
fn name(&self) -> &str {
&self.name
}
fn no_parse(&self) -> bool {
false
}
fn renderer(&self) -> Box<dyn DocumentRenderer> {
self.renderer.clone()
}
fn include_resources(&self) -> bool {
self.include_resources
}
fn layout(&self) -> Option<String> {
self.layout.clone()
}
}
#[typetag::serde(name = "notebook")]
impl Format for NotebookFormat {
fn extension(&self) -> &str {
"ipynb"
}
fn template_prefix(&self) -> &str {
"markdown"
}
fn name(&self) -> &str {
"notebook"
}
fn no_parse(&self) -> bool {
false
}
fn renderer(&self) -> Box<dyn DocumentRenderer> {
Box::new(NotebookRenderer)
}
fn include_resources(&self) -> bool {
true
}
fn layout(&self) -> Option<String> {
None
}
}
#[typetag::serde(name = "html")]
impl Format for HtmlFormat {
fn extension(&self) -> &str {
"html"
}
fn template_prefix(&self) -> &str {
"html"
}
fn name(&self) -> &str {
"html"
}
fn no_parse(&self) -> bool {
false
}
fn renderer(&self) -> Box<dyn DocumentRenderer> {
Box::<GenericRenderer>::default()
}
fn include_resources(&self) -> bool {
true
}
fn layout(&self) -> Option<String> {
Some("section".to_string())
}
}
#[typetag::serde(name = "info")]
impl Format for InfoFormat {
fn extension(&self) -> &str {
"yml"
}
fn template_prefix(&self) -> &str {
"yml"
}
fn name(&self) -> &str {
"info"
}
fn no_parse(&self) -> bool {
true
}
fn renderer(&self) -> Box<dyn DocumentRenderer> {
Box::<GenericRenderer>::default()
}
fn include_resources(&self) -> bool {
false
}
fn layout(&self) -> Option<String> {
None
}
}
#[typetag::serde(name = "markdown")]
impl Format for MarkdownFormat {
fn extension(&self) -> &str {
"md"
}
fn template_prefix(&self) -> &str {
"markdown"
}
fn name(&self) -> &str {
"markdown"
}
fn no_parse(&self) -> bool {
false
}
fn renderer(&self) -> Box<dyn DocumentRenderer> {
Box::<GenericRenderer>::default()
}
fn include_resources(&self) -> bool {
false
}
fn layout(&self) -> Option<String> {
None
}
}
#[typetag::serde(name = "latex")]
impl Format for LaTexFormat {
fn extension(&self) -> &str {
"tex"
}
fn template_prefix(&self) -> &str {
"latex"
}
fn name(&self) -> &str {
"latex"
}
fn no_parse(&self) -> bool {
false
}
fn renderer(&self) -> Box<dyn DocumentRenderer> {
Box::<GenericRenderer>::default()
}
fn include_resources(&self) -> bool {
true
}
fn layout(&self) -> Option<String> {
Some("section".to_string())
}
}
impl InputFormat {
pub fn loader(&self) -> Box<dyn Loader> {
match self {
InputFormat::Markdown => Box::new(MarkdownLoader),
InputFormat::Notebook => Box::new(NotebookLoader),
}
}
pub fn extension(&self) -> &str {
match self {
InputFormat::Markdown => "md",
InputFormat::Notebook => "ipynb",
}
}
pub fn name(&self) -> &str {
match self {
InputFormat::Markdown => "markdown",
InputFormat::Notebook => "notebook",
}
}
pub fn from_extension(ext: &str) -> Result<Self, anyhow::Error> {
match ext {
"md" => Ok(InputFormat::Markdown),
"ipynb" => Ok(InputFormat::Notebook),
_ => Err(anyhow!("Invalid extension for input")),
}
}
pub fn from_name(name: &str) -> Result<Self, anyhow::Error> {
match name {
"markdown" => Ok(InputFormat::Markdown),
"notebook" => Ok(InputFormat::Notebook),
_ => Err(anyhow!("Invalid format name for input")),
}
}
}
impl Display for InputFormat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn input_format_extension_and_name_mapping() {
assert_eq!(
InputFormat::from_extension("md").unwrap(),
InputFormat::Markdown
);
assert_eq!(
InputFormat::from_extension("ipynb").unwrap(),
InputFormat::Notebook
);
assert!(InputFormat::from_extension("txt").is_err());
assert_eq!(
InputFormat::from_name("markdown").unwrap(),
InputFormat::Markdown
);
assert_eq!(
InputFormat::from_name("notebook").unwrap(),
InputFormat::Notebook
);
assert!(InputFormat::from_name("html").is_err());
for f in [InputFormat::Markdown, InputFormat::Notebook] {
assert_eq!(InputFormat::from_name(f.name()).unwrap(), f);
assert_eq!(InputFormat::from_extension(f.extension()).unwrap(), f);
}
}
#[test]
fn output_format_typetag_roundtrip() {
let cases = [
("html: {}", "html", "html"),
("markdown: {}", "markdown", "md"),
("notebook: {}", "notebook", "ipynb"),
("latex: {}", "latex", "tex"),
("info: {}", "info", "yml"),
];
for (yaml, name, ext) in cases {
let f: Box<dyn Format> =
serde_yaml::from_str(yaml).unwrap_or_else(|e| panic!("parse `{yaml}`: {e}"));
assert_eq!(f.name(), name);
assert_eq!(f.extension(), ext);
let out = serde_yaml::to_string(&f).unwrap();
let f2: Box<dyn Format> = serde_yaml::from_str(&out)
.unwrap_or_else(|e| panic!("re-parse of `{out}`: {e}"));
assert_eq!(f2.name(), name);
assert_eq!(f2.extension(), ext);
}
}
#[test]
fn dynamic_format_deserializes() {
let yaml = r#"
dynamic:
name: myfmt
extension: xyz
template_prefix: html
"#;
let f: Box<dyn Format> = serde_yaml::from_str(yaml).unwrap();
assert_eq!(f.name(), "myfmt");
assert_eq!(f.extension(), "xyz");
assert_eq!(f.template_prefix(), "html");
}
}