pub mod assets;
pub mod backends;
pub mod brand;
pub mod frontmatter;
mod image_format;
pub mod images;
#[cfg(feature = "mermaid")]
pub mod mermaid;
pub mod pages;
pub mod preprocessor;
use std::path::{Path, PathBuf};
use anyhow::Result;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
pub use assets::{
AssetProvider, BoxFuture, EmbeddedAssets, LayeredAssets, async_provider, sync_provider,
};
pub use brand::{AutoLayout, BrandSpec};
pub use pages::splitter::{DefaultSplitter, PageSplitter};
pub use pages::{Page, PageOrigin, RawPage};
pub use preprocessor::{Chain, HtmlImageTags, Identity, MarkdownPreprocessor};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Target {
Docx,
Odt,
Pdf,
PdfPresentation,
Pptx,
HtmlReveal,
}
impl Target {
pub fn as_str(&self) -> &'static str {
match self {
Target::Docx => "docx",
Target::Odt => "odt",
Target::Pdf => "pdf",
Target::PdfPresentation => "pdf-presentation",
Target::Pptx => "pptx",
Target::HtmlReveal => "html-reveal",
}
}
pub fn extension(&self) -> &'static str {
match self {
Target::Docx => "docx",
Target::Odt => "odt",
Target::Pdf | Target::PdfPresentation => "pdf",
Target::Pptx => "pptx",
Target::HtmlReveal => "html",
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DocMeta {
pub title: Option<String>,
pub author: Option<String>,
pub date: Option<String>,
#[serde(default)]
pub extra: std::collections::BTreeMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct BrandHandle(pub std::sync::Arc<BrandSpec>);
#[derive(Debug, Clone)]
pub struct AssetRef {
pub key: String,
}
#[derive(Debug, Clone)]
pub struct ResolvedDoc {
pub pages: Vec<Page>,
pub meta: DocMeta,
pub brand: BrandHandle,
pub assets: Vec<AssetRef>,
pub fonts: Vec<AssetRef>,
pub toc: Option<u8>,
}
pub struct RenderRequest<'a> {
pub doc: &'a ResolvedDoc,
pub assets: &'a dyn AssetProvider,
pub out: &'a Path,
}
#[derive(Debug, Clone)]
pub struct Artifact {
pub primary: PathBuf,
pub extras: Vec<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct RenderedArtifact {
pub primary: Bytes,
pub filename: String,
pub extras: Vec<(String, Bytes)>,
}
impl RenderedArtifact {
pub async fn write_to(&self, out: &Path) -> Result<Artifact> {
if let Some(parent) = out.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(out, &self.primary).await?;
let dir = out.parent().unwrap_or_else(|| Path::new("."));
let mut extras = Vec::with_capacity(self.extras.len());
for (name, bytes) in &self.extras {
let p = dir.join(name);
tokio::fs::write(&p, bytes).await?;
extras.push(p);
}
Ok(Artifact {
primary: out.to_path_buf(),
extras,
})
}
}
pub trait Backend: Send + Sync {
fn target(&self) -> Target;
fn render_to_bytes<'a>(
&'a self,
doc: &'a ResolvedDoc,
assets: &'a dyn AssetProvider,
) -> BoxFuture<'a, Result<RenderedArtifact>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn write_to_creates_parent_dirs_and_writes_primary_and_extras() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("nested").join("sub").join("out.html");
let artifact = RenderedArtifact {
primary: Bytes::from_static(b"<html></html>"),
filename: "out.html".to_string(),
extras: vec![
("out.css".to_string(), Bytes::from_static(b"body{}")),
("out.js".to_string(), Bytes::from_static(b"console.log(1)")),
],
};
let written = artifact.write_to(&out).await.unwrap();
assert_eq!(written.primary, out);
assert_eq!(tokio::fs::read(&out).await.unwrap(), b"<html></html>");
let expected_extras = vec![
dir.path().join("nested").join("sub").join("out.css"),
dir.path().join("nested").join("sub").join("out.js"),
];
assert_eq!(written.extras, expected_extras);
assert_eq!(
tokio::fs::read(&expected_extras[0]).await.unwrap(),
b"body{}"
);
assert_eq!(
tokio::fs::read(&expected_extras[1]).await.unwrap(),
b"console.log(1)"
);
}
}