#[cfg(feature = "no-indexmap")]
use std::collections::HashMap;
#[cfg(feature = "content-builder")]
use std::io::Read;
use std::{
fs,
path::{Path, PathBuf},
};
use chrono::{SecondsFormat, Utc};
#[cfg(not(feature = "no-indexmap"))]
use indexmap::IndexMap;
use infer::Infer;
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
#[cfg(feature = "content-builder")]
use crate::builder::content::ContentBuilder;
use crate::{
builder::{XmlWriter, normalize_manifest_path, refine_mime_type},
error::{EpubBuilderError, EpubError},
types::{ManifestItem, MetadataItem, MetadataSheet, NavPoint, SpineItem},
utils::ELEMENT_IN_DC_NAMESPACE,
};
#[derive(Debug)]
pub struct RootfileBuilder {
pub(crate) rootfiles: Vec<String>,
}
impl RootfileBuilder {
pub(crate) fn new() -> Self {
Self { rootfiles: Vec::new() }
}
pub fn add(&mut self, rootfile: impl AsRef<str>) -> Result<&mut Self, EpubError> {
let rootfile = rootfile.as_ref();
if rootfile.starts_with("/") || rootfile.starts_with("../") {
return Err(EpubBuilderError::IllegalRootfilePath.into());
}
let rootfile = rootfile.strip_prefix("./").unwrap_or(rootfile);
self.rootfiles.push(rootfile.into());
Ok(self)
}
pub fn clear(&mut self) -> &mut Self {
self.rootfiles.clear();
self
}
pub(crate) fn is_empty(&self) -> bool {
self.rootfiles.is_empty()
}
pub(crate) fn first(&self) -> Option<&String> {
self.rootfiles.first()
}
pub(crate) fn make(&self, writer: &mut XmlWriter) -> Result<(), EpubError> {
writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
writer.write_event(Event::Start(BytesStart::new("container").with_attributes(
[
("version", "1.0"),
("xmlns", "urn:oasis:names:tc:opendocument:xmlns:container"),
],
)))?;
writer.write_event(Event::Start(BytesStart::new("rootfiles")))?;
for rootfile in &self.rootfiles {
writer.write_event(Event::Empty(BytesStart::new("rootfile").with_attributes([
("full-path", rootfile.as_str()),
("media-type", "application/oebps-package+xml"),
])))?;
}
writer.write_event(Event::End(BytesEnd::new("rootfiles")))?;
writer.write_event(Event::End(BytesEnd::new("container")))?;
Ok(())
}
}
#[derive(Debug)]
pub struct MetadataBuilder {
pub(crate) metadata: Vec<MetadataItem>,
}
impl MetadataBuilder {
pub(crate) fn new() -> Self {
Self { metadata: Vec::new() }
}
pub fn add(&mut self, item: MetadataItem) -> &mut Self {
self.metadata.push(item);
self
}
pub fn clear(&mut self) -> &mut Self {
self.metadata.clear();
self
}
pub fn from(&mut self, sheet: MetadataSheet) -> &mut Self {
self.metadata.extend(Vec::<MetadataItem>::from(sheet));
self
}
pub(crate) fn make(&mut self, writer: &mut XmlWriter) -> Result<(), EpubError> {
self.metadata.push(MetadataItem {
id: None,
property: "dcterms:modified".to_string(),
value: Utc::now().to_rfc3339_opts(SecondsFormat::AutoSi, true),
lang: None,
refined: vec![],
});
writer.write_event(Event::Start(BytesStart::new("metadata")))?;
for metadata in &self.metadata {
let tag_name = if ELEMENT_IN_DC_NAMESPACE.contains(&metadata.property.as_str()) {
format!("dc:{}", metadata.property)
} else {
"meta".to_string()
};
writer.write_event(Event::Start(
BytesStart::new(tag_name.as_str()).with_attributes(metadata.attributes()),
))?;
writer.write_event(Event::Text(BytesText::new(metadata.value.as_str())))?;
writer.write_event(Event::End(BytesEnd::new(tag_name.as_str())))?;
for refinement in &metadata.refined {
writer.write_event(Event::Start(
BytesStart::new("meta").with_attributes(refinement.attributes()),
))?;
writer.write_event(Event::Text(BytesText::new(refinement.value.as_str())))?;
writer.write_event(Event::End(BytesEnd::new("meta")))?;
}
}
writer.write_event(Event::End(BytesEnd::new("metadata")))?;
Ok(())
}
pub(crate) fn validate(&self) -> Result<(), EpubError> {
let mut has_title = false;
let mut has_language = false;
let mut has_identifier = false;
for item in &self.metadata {
match item.property.as_str() {
"title" => has_title = true,
"language" => has_language = true,
"identifier" => {
if item.id.as_ref().is_some_and(|id| id == "pub-id") {
has_identifier = true;
}
}
_ => {}
}
if has_title && has_language && has_identifier {
return Ok(());
}
}
Err(EpubBuilderError::MissingNecessaryMetadata.into())
}
}
#[derive(Debug)]
pub struct ManifestBuilder {
temp_dir: PathBuf,
rootfile: Option<String>,
#[cfg(feature = "no-indexmap")]
pub(crate) manifest: HashMap<String, ManifestItem>,
#[cfg(not(feature = "no-indexmap"))]
pub(crate) manifest: IndexMap<String, ManifestItem>,
}
impl ManifestBuilder {
pub(crate) fn new(temp_dir: impl AsRef<Path>) -> Self {
Self {
temp_dir: temp_dir.as_ref().to_path_buf(),
rootfile: None,
#[cfg(feature = "no-indexmap")]
manifest: HashMap::new(),
#[cfg(not(feature = "no-indexmap"))]
manifest: IndexMap::new(),
}
}
pub(crate) fn set_rootfile(&mut self, rootfile: impl Into<String>) {
self.rootfile = Some(rootfile.into());
}
pub fn add(
&mut self,
manifest_source: impl Into<String>,
manifest_item: ManifestItem,
) -> Result<&mut Self, EpubError> {
let manifest_source = manifest_source.into();
let source = PathBuf::from(&manifest_source);
if !source.is_file() {
return Err(EpubBuilderError::TargetIsNotFile { target_path: manifest_source }.into());
}
let extension = match source.extension() {
Some(ext) => ext.to_string_lossy().to_lowercase(),
None => String::new(),
};
let buf = fs::read(source)?;
let real_mime = match Infer::new().get(&buf) {
Some(infer_mime) => refine_mime_type(infer_mime.mime_type(), &extension),
None => {
return Err(
EpubBuilderError::UnknownFileFormat { file_path: manifest_source }.into(),
);
}
};
let target_path = normalize_manifest_path(
&self.temp_dir,
self.rootfile
.as_ref()
.ok_or(EpubBuilderError::MissingRootfile)?,
&manifest_item.path,
&manifest_item.id,
)?;
if let Some(parent_dir) = target_path.parent() {
if !parent_dir.exists() {
fs::create_dir_all(parent_dir)?
}
}
match fs::write(target_path, buf) {
Ok(_) => {
self.manifest
.insert(manifest_item.id.clone(), manifest_item.set_mime(real_mime));
Ok(self)
}
Err(err) => Err(err.into()),
}
}
pub fn clear(&mut self) -> &mut Self {
let paths = self
.manifest
.values()
.map(|manifest| &manifest.path)
.collect::<Vec<&PathBuf>>();
for path in paths {
let _ = fs::remove_file(path);
}
self.manifest.clear();
self
}
pub(crate) fn insert(
&mut self,
key: impl Into<String>,
value: ManifestItem,
) -> Option<ManifestItem> {
self.manifest.insert(key.into(), value)
}
pub(crate) fn make(&self, writer: &mut XmlWriter) -> Result<(), EpubError> {
writer.write_event(Event::Start(BytesStart::new("manifest")))?;
for manifest in self.manifest.values() {
writer.write_event(Event::Empty(
BytesStart::new("item").with_attributes(manifest.attributes()),
))?;
}
writer.write_event(Event::End(BytesEnd::new("manifest")))?;
Ok(())
}
pub(crate) fn validate(&self) -> Result<(), EpubError> {
self.validate_fallback_chains()?;
self.validate_nav()?;
Ok(())
}
pub(crate) fn keys(&self) -> impl Iterator<Item = &String> {
self.manifest.keys()
}
fn validate_fallback_chains(&self) -> Result<(), EpubError> {
for (id, item) in &self.manifest {
if item.fallback.is_none() {
continue;
}
let mut fallback_chain = Vec::new();
self.validate_fallback_chain(id, &mut fallback_chain)?;
}
Ok(())
}
fn validate_fallback_chain(
&self,
manifest_id: &str,
fallback_chain: &mut Vec<String>,
) -> Result<(), EpubError> {
if fallback_chain.contains(&manifest_id.to_string()) {
fallback_chain.push(manifest_id.to_string());
return Err(EpubBuilderError::ManifestCircularReference {
fallback_chain: fallback_chain.join("->"),
}
.into());
}
let item = self.manifest.get(manifest_id).unwrap();
if let Some(fallback_id) = &item.fallback {
if !self.manifest.contains_key(fallback_id) {
return Err(EpubBuilderError::ManifestNotFound {
manifest_id: fallback_id.to_owned(),
}
.into());
}
fallback_chain.push(manifest_id.to_string());
self.validate_fallback_chain(fallback_id, fallback_chain)
} else {
Ok(())
}
}
fn validate_nav(&self) -> Result<(), EpubError> {
if self
.manifest
.values()
.filter(|&item| {
if let Some(properties) = &item.properties {
properties.split(" ").any(|property| property == "nav")
} else {
false
}
})
.count()
== 1
{
Ok(())
} else {
Err(EpubBuilderError::TooManyNavFlags.into())
}
}
}
#[derive(Debug)]
pub struct SpineBuilder {
pub(crate) spine: Vec<SpineItem>,
}
impl SpineBuilder {
pub(crate) fn new() -> Self {
Self { spine: Vec::new() }
}
pub fn add(&mut self, item: SpineItem) -> &mut Self {
self.spine.push(item);
self
}
pub fn clear(&mut self) -> &mut Self {
self.spine.clear();
self
}
pub(crate) fn make(&self, writer: &mut XmlWriter) -> Result<(), EpubError> {
writer.write_event(Event::Start(BytesStart::new("spine")))?;
for spine in &self.spine {
writer.write_event(Event::Empty(
BytesStart::new("itemref").with_attributes(spine.attributes()),
))?;
}
writer.write_event(Event::End(BytesEnd::new("spine")))?;
Ok(())
}
pub(crate) fn validate(
&self,
manifest_keys: impl Iterator<Item = impl AsRef<str>>,
) -> Result<(), EpubError> {
let manifest_keys: Vec<String> = manifest_keys.map(|k| k.as_ref().to_string()).collect();
for spine in &self.spine {
if !manifest_keys.contains(&spine.idref) {
return Err(
EpubBuilderError::SpineManifestNotFound { idref: spine.idref.clone() }.into(),
);
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct CatalogBuilder {
pub(crate) title: String,
pub(crate) catalog: Vec<NavPoint>,
}
impl CatalogBuilder {
pub(crate) fn new() -> Self {
Self {
title: String::new(),
catalog: Vec::new(),
}
}
pub fn set_title(&mut self, title: impl Into<String>) -> &mut Self {
self.title = title.into();
self
}
pub fn add(&mut self, item: NavPoint) -> &mut Self {
self.catalog.push(item);
self
}
pub fn clear(&mut self) -> &mut Self {
self.title.clear();
self.catalog.clear();
self
}
pub(crate) fn is_empty(&self) -> bool {
self.catalog.is_empty()
}
pub(crate) fn make(&self, writer: &mut XmlWriter) -> Result<(), EpubError> {
writer.write_event(Event::Start(BytesStart::new("html").with_attributes([
("xmlns", "http://www.w3.org/1999/xhtml"),
("xmlns:epub", "http://www.idpf.org/2007/ops"),
])))?;
writer.write_event(Event::Start(BytesStart::new("head")))?;
writer.write_event(Event::Start(BytesStart::new("title")))?;
writer.write_event(Event::Text(BytesText::new(&self.title)))?;
writer.write_event(Event::End(BytesEnd::new("title")))?;
writer.write_event(Event::End(BytesEnd::new("head")))?;
writer.write_event(Event::Start(BytesStart::new("body")))?;
writer.write_event(Event::Start(
BytesStart::new("nav").with_attributes([("epub:type", "toc")]),
))?;
if !self.title.is_empty() {
writer.write_event(Event::Start(BytesStart::new("h1")))?;
writer.write_event(Event::Text(BytesText::new(&self.title)))?;
writer.write_event(Event::End(BytesEnd::new("h1")))?;
}
Self::make_nav(writer, &self.catalog)?;
writer.write_event(Event::End(BytesEnd::new("nav")))?;
writer.write_event(Event::End(BytesEnd::new("body")))?;
writer.write_event(Event::End(BytesEnd::new("html")))?;
Ok(())
}
fn make_nav(writer: &mut XmlWriter, navgations: &Vec<NavPoint>) -> Result<(), EpubError> {
writer.write_event(Event::Start(BytesStart::new("ol")))?;
for nav in navgations {
writer.write_event(Event::Start(BytesStart::new("li")))?;
if let Some(path) = &nav.content {
writer.write_event(Event::Start(
BytesStart::new("a").with_attributes([("href", path.to_string_lossy())]),
))?;
writer.write_event(Event::Text(BytesText::new(nav.label.as_str())))?;
writer.write_event(Event::End(BytesEnd::new("a")))?;
} else {
writer.write_event(Event::Start(BytesStart::new("span")))?;
writer.write_event(Event::Text(BytesText::new(nav.label.as_str())))?;
writer.write_event(Event::End(BytesEnd::new("span")))?;
}
if !nav.children.is_empty() {
Self::make_nav(writer, &nav.children)?;
}
writer.write_event(Event::End(BytesEnd::new("li")))?;
}
writer.write_event(Event::End(BytesEnd::new("ol")))?;
Ok(())
}
}
#[cfg(feature = "content-builder")]
#[derive(Debug)]
pub struct DocumentBuilder {
pub(crate) documents: Vec<(PathBuf, ContentBuilder)>,
}
#[cfg(feature = "content-builder")]
impl DocumentBuilder {
pub(crate) fn new() -> Self {
Self { documents: Vec::new() }
}
pub fn add(&mut self, target: impl AsRef<str>, content: ContentBuilder) -> &mut Self {
self.documents
.push((PathBuf::from(target.as_ref()), content));
self
}
pub fn clear(&mut self) -> &mut Self {
self.documents.clear();
self
}
pub fn make(
&mut self,
temp_dir: PathBuf,
rootfile: impl AsRef<str>,
) -> Result<Vec<ManifestItem>, EpubError> {
let mut buf = vec![0; 512];
let contents = std::mem::take(&mut self.documents);
let mut manifest = Vec::new();
for (target, mut content) in contents.into_iter() {
let manifest_id = content.id.clone();
let absolute_target =
normalize_manifest_path(&temp_dir, &rootfile, &target, &manifest_id)?;
let mut resources = content.make(&absolute_target)?;
let to_container_path = |p: &PathBuf| -> PathBuf {
match p.strip_prefix(&temp_dir) {
Ok(rel) => PathBuf::from("/").join(rel.to_string_lossy().replace("\\", "/")),
Err(_) => unreachable!("path MUST under temp directory"),
}
};
let path = resources.swap_remove(0);
let mut file = std::fs::File::open(&path)?;
let _ = file.read(&mut buf)?;
let extension = path
.extension()
.map(|e| e.to_string_lossy().to_lowercase())
.unwrap_or_default();
let mime = match Infer::new().get(&buf) {
Some(infer) => refine_mime_type(infer.mime_type(), &extension),
None => {
return Err(EpubBuilderError::UnknownFileFormat {
file_path: path.to_string_lossy().to_string(),
}
.into());
}
}
.to_string();
manifest.push(ManifestItem {
id: manifest_id.clone(),
path: to_container_path(&path),
mime,
properties: None,
fallback: None,
});
for res in resources {
let mut file = fs::File::open(&res)?;
let _ = file.read(&mut buf)?;
let extension = res
.extension()
.map(|e| e.to_string_lossy().to_lowercase())
.unwrap_or_default();
let mime = match Infer::new().get(&buf) {
Some(ft) => refine_mime_type(ft.mime_type(), &extension),
None => {
return Err(EpubBuilderError::UnknownFileFormat {
file_path: path.to_string_lossy().to_string(),
}
.into());
}
}
.to_string();
let file_name = res
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
let res_id = format!("{}-{}", manifest_id, file_name);
manifest.push(ManifestItem {
id: res_id,
path: to_container_path(&res),
mime,
properties: None,
fallback: None,
});
}
}
Ok(manifest)
}
}