use crate::templates;
use crate::toc::{Toc, TocElement};
use crate::zip::Zip;
use crate::ReferenceType;
use crate::Result;
use crate::{common, EpubContent};
use core::fmt::Debug;
use std::io;
use std::io::Read;
use std::path::Path;
use std::str::FromStr;
use upon::Engine;
#[non_exhaustive]
#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Eq)]
pub enum EpubVersion {
V20,
V30,
V33,
}
pub trait MetadataRenderer: Send + Sync {
fn render_opf(&self, escape_html: bool) -> String;
}
impl Debug for dyn MetadataRenderer {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "MetadataRenderer{{{}}}", self.render_opf(true))
}
}
#[derive(Debug)]
pub struct MetadataOpfV3 {
pub property: String,
pub content: String,
pub dir: Option<String>,
pub id: Option<String>,
pub refines: Option<String>,
pub scheme: Option<String>,
pub xml_lang: Option<String>,
}
impl MetadataOpfV3 {
pub fn new(property: String, content: String) -> MetadataOpfV3 {
MetadataOpfV3{
property: property,
content: content,
dir: None,
id: None,
refines: None,
scheme: None,
xml_lang: None,
}
}
pub fn add_direction(&mut self, direction: String) -> &mut Self {
self.dir = Some(direction);
self
}
pub fn add_id(&mut self, id: String) -> &mut Self {
self.id = Some(id);
self
}
pub fn add_refines(&mut self, refines: String) -> &mut Self {
self.id = Some(refines);
self
}
pub fn add_scheme(&mut self, scheme: String) -> &mut Self {
self.scheme = Some(scheme);
self
}
pub fn add_xml_lang(&mut self, xml_lang: String) -> &mut Self {
self.xml_lang = Some(xml_lang);
self
}
}
impl MetadataRenderer for MetadataOpfV3 {
fn render_opf(&self, escape_html: bool) -> String {
let mut meta_tag = String::from("<meta ");
if let Some(dir) = &self.dir {
meta_tag.push_str(&format!(
"dir=\"{}\" ", common::encode_html(dir, escape_html),
));
}
if let Some(id) = &self.id {
meta_tag.push_str(&format!(
"id=\"{}\" ", common::encode_html(id, escape_html),
));
}
if let Some(refines) = &self.refines {
meta_tag.push_str(&format!(
"refines=\"{}\" ", common::encode_html(refines, escape_html)
));
}
if let Some(scheme) = &self.scheme {
meta_tag.push_str(&format!(
"scheme=\"{}\" ", common::encode_html(scheme, escape_html),
));
}
if let Some(xml_lang) = &self.xml_lang {
meta_tag.push_str(&format!(
"xml:lang=\"{}\" ", common::encode_html(xml_lang, escape_html)
));
}
meta_tag.push_str(&format!(
"property=\"{}\">{}</meta>",
common::encode_html(&self.property, escape_html),
&self.content,
));
meta_tag
}
}
#[derive(Debug)]
pub struct MetadataOpf {
pub name: String,
pub content: String
}
impl MetadataOpf {
pub fn new(&self, meta_name: String, meta_content: String) -> Self {
Self { name: meta_name, content: meta_content }
}
}
impl MetadataRenderer for MetadataOpf {
fn render_opf(&self, escape_html: bool) -> String {
format!(
"<meta name=\"{}\" content=\"{}\"/>",
common::encode_html(&self.name, escape_html),
common::encode_html(&self.content, escape_html),
)
}
}
#[derive(Debug, Copy, Clone, Default)]
pub enum PageDirection {
#[default]
Ltr,
Rtl,
}
impl ToString for PageDirection {
fn to_string(&self) -> String {
match &self {
PageDirection::Rtl => "rtl".into(),
PageDirection::Ltr => "ltr".into(),
}
}
}
impl FromStr for PageDirection {
type Err = crate::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let s = s.to_lowercase();
match s.as_ref() {
"rtl" => Ok(PageDirection::Rtl),
"ltr" => Ok(PageDirection::Ltr),
_ => Err(crate::Error::PageDirectionError(s)),
}
}
}
#[derive(Debug)]
pub struct Metadata {
pub title: String,
pub author: Vec<String>,
pub lang: Vec<String>,
pub direction: PageDirection,
pub generator: String,
pub toc_name: String,
pub description: Vec<String>,
pub subject: Vec<String>,
pub license: Option<String>,
pub date_published: Option<chrono::DateTime<chrono::Utc>>,
pub date_modified: Option<chrono::DateTime<chrono::Utc>>,
pub uuid: Option<uuid::Uuid>,
}
impl Default for Metadata {
fn default() -> Self {
Self {
title: String::new(),
author: vec![],
lang: vec![],
direction: PageDirection::default(),
generator: String::from("Rust EPUB library"),
toc_name: String::from("Table Of Contents"),
description: vec![],
subject: vec![],
license: None,
date_published: None,
date_modified: None,
uuid: None,
}
}
}
#[derive(Debug)]
struct Content {
pub file: String,
pub mime: String,
pub itemref: bool,
pub cover: bool,
pub reftype: Option<ReferenceType>,
pub title: String,
}
impl Content {
pub fn new<S1, S2>(file: S1, mime: S2) -> Content
where
S1: Into<String>,
S2: Into<String>,
{
Content {
file: file.into(),
mime: mime.into(),
itemref: false,
cover: false,
reftype: None,
title: String::new(),
}
}
}
#[derive(Debug)]
pub struct EpubBuilder<Z: Zip> {
version: EpubVersion,
direction: PageDirection,
zip: Z,
files: Vec<Content>,
metadata: Metadata,
toc: Toc,
stylesheet: bool,
inline_toc: bool,
escape_html: bool,
meta_opf: Vec<Box<dyn MetadataRenderer>>,
}
impl<Z: Zip> EpubBuilder<Z> {
pub fn new(zip: Z) -> Result<EpubBuilder<Z>> {
let mut epub = EpubBuilder {
version: EpubVersion::V20,
direction: PageDirection::Ltr,
zip,
files: vec![],
metadata: Metadata::default(),
toc: Toc::new(),
stylesheet: false,
inline_toc: false,
escape_html: true,
meta_opf: vec![],
};
epub.zip
.write_file("META-INF/container.xml", templates::CONTAINER)?;
epub.zip.write_file(
"META-INF/com.apple.ibooks.display-options.xml",
templates::IBOOKS,
)?;
Ok(epub)
}
pub fn epub_version(&mut self, version: EpubVersion) -> &mut Self {
self.version = version;
self
}
pub fn epub_direction(&mut self, direction: PageDirection) -> &mut Self {
self.direction = direction;
self
}
pub fn add_metadata_opf(&mut self, item: Box<dyn MetadataRenderer>) -> &mut Self {
self.meta_opf.push(item);
self
}
pub fn metadata<S1, S2>(&mut self, key: S1, value: S2) -> Result<&mut Self>
where
S1: AsRef<str>,
S2: Into<String>,
{
match key.as_ref() {
"author" => {
let value = value.into();
if value.is_empty() {
self.metadata.author = vec![];
} else {
self.metadata.author.push(value);
}
}
"title" => self.metadata.title = value.into(),
"lang" => {
let value = value.into();
if value.is_empty() {
self.metadata.lang = vec![];
} else {
self.metadata.lang.push(value.into())
}
}
"direction" => self.metadata.direction = PageDirection::from_str(&value.into())?,
"generator" => self.metadata.generator = value.into(),
"description" => {
let value = value.into();
if value.is_empty() {
self.metadata.description = vec![];
} else {
self.metadata.description.push(value);
}
}
"subject" => {
let value = value.into();
if value.is_empty() {
self.metadata.subject = vec![];
} else {
self.metadata.subject.push(value);
}
}
"license" => self.metadata.license = Some(value.into()),
"toc_name" => self.metadata.toc_name = value.into(),
s => Err(crate::Error::InvalidMetadataError(s.to_string()))?,
}
Ok(self)
}
pub fn set_authors(&mut self, value: Vec<String>) {
self.metadata.author = value;
}
pub fn add_author<S: Into<String>>(&mut self, value: S) {
self.metadata.author.push(value.into());
}
pub fn clear_authors<S: Into<String>>(&mut self) {
self.metadata.author.clear();
}
pub fn set_title<S: Into<String>>(&mut self, value: S) {
self.metadata.title = value.into();
}
pub fn escape_html(&mut self, val: bool) {
self.escape_html = val;
}
#[deprecated(since = "0.8.3", note = "Use set_languages or add_language instead")]
pub fn set_lang<S: Into<String>>(&mut self, value: S) {
self.clear_languages();
self.add_language(value);
}
pub fn set_languages(&mut self, value: Vec<String>) {
self.metadata.lang = value;
}
pub fn add_language<S: Into<String>>(&mut self, value: S) {
self.metadata.lang.push(value.into());
}
pub fn clear_languages(&mut self) {
self.metadata.lang.clear();
}
pub fn set_generator<S: Into<String>>(&mut self, value: S) {
self.metadata.generator = value.into();
}
pub fn set_toc_name<S: Into<String>>(&mut self, value: S) {
self.metadata.toc_name = value.into();
}
pub fn set_description(&mut self, value: Vec<String>) {
self.metadata.description = value;
}
pub fn add_description<S: Into<String>>(&mut self, value: S) {
self.metadata.description.push(value.into());
}
pub fn clear_description(&mut self) {
self.metadata.description.clear();
}
pub fn set_subjects(&mut self, value: Vec<String>) {
self.metadata.subject = value;
}
pub fn add_subject<S: Into<String>>(&mut self, value: S) {
self.metadata.subject.push(value.into());
}
pub fn clear_subjects(&mut self) {
self.metadata.subject.clear();
}
pub fn set_license<S: Into<String>>(&mut self, value: S) {
self.metadata.license = Some(value.into());
}
pub fn set_publication_date(&mut self, date_published: chrono::DateTime<chrono::Utc>) {
self.metadata.date_published = Some(date_published);
}
pub fn set_modified_date(&mut self, date_modified: chrono::DateTime<chrono::Utc>) {
self.metadata.date_modified = Some(date_modified);
}
pub fn set_uuid(&mut self, uuid: uuid::Uuid) {
self.metadata.uuid = Some(uuid);
}
pub fn stylesheet<R: Read>(&mut self, content: R) -> Result<&mut Self> {
self.add_resource("stylesheet.css", content, "text/css")?;
self.stylesheet = true;
Ok(self)
}
pub fn inline_toc(&mut self) -> &mut Self {
self.inline_toc = true;
self.toc.add(TocElement::new(
"toc.xhtml",
self.metadata.toc_name.as_str(),
));
let mut file = Content::new("toc.xhtml", "application/xhtml+xml");
file.reftype = Some(ReferenceType::Toc);
file.title = self.metadata.toc_name.clone();
file.itemref = true;
self.files.push(file);
self
}
pub fn add_resource<R, P, S>(&mut self, path: P, content: R, mime_type: S) -> Result<&mut Self>
where
R: Read,
P: AsRef<Path>,
S: Into<String>,
{
self.zip
.write_file(Path::new("OEBPS").join(path.as_ref()), content)?;
log::debug!("Add resource: {:?}", path.as_ref().display());
self.files.push(Content::new(
format!("{}", path.as_ref().display()),
mime_type,
));
Ok(self)
}
pub fn add_cover_image<R, P, S>(
&mut self,
path: P,
content: R,
mime_type: S,
) -> Result<&mut Self>
where
R: Read,
P: AsRef<Path>,
S: Into<String>,
{
self.zip
.write_file(Path::new("OEBPS").join(path.as_ref()), content)?;
let mut file = Content::new(format!("{}", path.as_ref().display()), mime_type);
file.cover = true;
self.files.push(file);
Ok(self)
}
pub fn add_content<R: Read>(&mut self, content: EpubContent<R>) -> Result<&mut Self> {
self.zip.write_file(
Path::new("OEBPS").join(content.toc.url.as_str()),
content.content,
)?;
let mut file = Content::new(content.toc.url.as_str(), "application/xhtml+xml");
file.itemref = true;
file.reftype = content.reftype;
if file.reftype.is_some() {
file.title = content.toc.title.clone();
}
self.files.push(file);
if !content.toc.title.is_empty() {
self.toc.add(content.toc);
}
Ok(self)
}
pub fn generate<W: io::Write>(mut self, to: W) -> Result<()> {
if !self.stylesheet {
self.stylesheet(b"".as_ref())?;
}
let bytes = self.render_opf()?;
self.zip.write_file("OEBPS/content.opf", &*bytes)?;
let bytes = self.render_toc()?;
self.zip.write_file("OEBPS/toc.ncx", &*bytes)?;
let bytes = self.render_nav(true)?;
self.zip.write_file("OEBPS/nav.xhtml", &*bytes)?;
if self.inline_toc {
let bytes = self.render_nav(false)?;
self.zip.write_file("OEBPS/toc.xhtml", &*bytes)?;
}
self.zip.generate(to)?;
Ok(())
}
fn render_opf(&mut self) -> Result<Vec<u8>> {
log::debug!("render_opf...");
let mut optional: Vec<String> = Vec::new();
for desc in &self.metadata.description {
optional.push(format!(
"<dc:description>{}</dc:description>",
common::encode_html(desc, self.escape_html),
));
}
for subject in &self.metadata.subject {
optional.push(format!(
"<dc:subject>{}</dc:subject>",
common::encode_html(subject, self.escape_html),
));
}
if let Some(ref rights) = self.metadata.license {
optional.push(format!(
"<dc:rights>{}</dc:rights>",
common::encode_html(rights, self.escape_html),
));
}
for meta in &self.meta_opf {
optional.push(meta.render_opf(self.escape_html))
}
let date_modified = self
.metadata
.date_modified
.unwrap_or_else(chrono::Utc::now)
.format("%Y-%m-%dT%H:%M:%SZ");
let date_published = self
.metadata
.date_published
.map(|date| date.format("%Y-%m-%dT%H:%M:%SZ"));
let uuid = uuid::fmt::Urn::from_uuid(self.metadata.uuid.unwrap_or_else(uuid::Uuid::new_v4))
.to_string();
let mut items: Vec<String> = Vec::new();
let mut itemrefs: Vec<String> = Vec::new();
let mut guide: Vec<String> = Vec::new();
for content in &self.files {
let id = if content.cover {
String::from("cover-image")
} else {
to_id(&content.file)
};
let properties = match (self.version, content.cover) {
(EpubVersion::V30, true) => "properties=\"cover-image\" ",
(EpubVersion::V33, true) => "properties=\"cover-image\" ",
_ => "",
};
if content.cover {
optional.push("<meta name=\"cover\" content=\"cover-image\"/>".to_string());
}
log::debug!("id={:?}, mime={:?}", id, content.mime);
items.push(format!(
"<item media-type=\"{mime}\" {properties}\
id=\"{id}\" href=\"{href}\"/>",
properties = properties, mime = html_escape::encode_double_quoted_attribute(&content.mime),
id = html_escape::encode_double_quoted_attribute(&id),
href =
html_escape::encode_double_quoted_attribute(&content.file.replace('\\', "/")),
));
if content.itemref {
itemrefs.push(format!(
"<itemref idref=\"{id}\"/>",
id = html_escape::encode_double_quoted_attribute(&id),
));
}
if let Some(reftype) = content.reftype {
use crate::ReferenceType::*;
let reftype = match reftype {
Cover => "cover",
TitlePage => "title-page",
Toc => "toc",
Index => "index",
Glossary => "glossary",
Acknowledgements => "acknowledgements",
Bibliography => "bibliography",
Colophon => "colophon",
Copyright => "copyright",
Dedication => "dedication",
Epigraph => "epigraph",
Foreword => "foreword",
Loi => "loi",
Lot => "lot",
Notes => "notes",
Preface => "preface",
Text => "text",
};
log::debug!("content = {:?}", &content);
guide.push(format!(
"<reference type=\"{reftype}\" title=\"{title}\" href=\"{href}\"/>",
reftype = html_escape::encode_double_quoted_attribute(&reftype),
title = html_escape::encode_double_quoted_attribute(&content.title),
href = html_escape::encode_double_quoted_attribute(&content.file),
));
}
}
let data = {
let mut authors: Vec<_> = vec![];
for (i, author) in self.metadata.author.iter().enumerate() {
let author = upon::value! {
id_attr: html_escape::encode_double_quoted_attribute(&i.to_string()),
name: common::encode_html(author, self.escape_html)
};
authors.push(author);
}
let mut languages: Vec<_> = vec![];
for (i, lang) in self.metadata.lang.iter().enumerate() {
let lang = upon::value! {
id_attr: html_escape::encode_double_quoted_attribute(&i.to_string()),
name: common::encode_html(lang, self.escape_html)
};
languages.push(lang);
}
upon::value! {
author: authors,
lang: languages,
direction: self.metadata.direction.to_string(),
title: common::encode_html(&self.metadata.title, self.escape_html),
generator_attr: html_escape::encode_double_quoted_attribute(&self.metadata.generator),
toc_name: common::encode_html(&self.metadata.toc_name, self.escape_html),
toc_name_attr: html_escape::encode_double_quoted_attribute(&self.metadata.toc_name),
optional: common::indent(optional.join("\n"), 2),
items: common::indent(items.join("\n"), 2), itemrefs: common::indent(itemrefs.join("\n"), 2), date_modified: html_escape::encode_text(&date_modified.to_string()),
uuid: html_escape::encode_text(&uuid),
guide: common::indent(guide.join("\n"), 2), date_published: if let Some(date) = date_published { date.to_string() } else { String::new() },
}
};
let mut res: Vec<u8> = vec![];
match self.version {
EpubVersion::V20 => templates::v2::CONTENT_OPF.render(&Engine::new(), &data).to_writer(&mut res),
EpubVersion::V30 => templates::v3::CONTENT_OPF.render(&Engine::new(), &data).to_writer(&mut res),
EpubVersion::V33 => templates::v3::CONTENT_OPF.render(&Engine::new(), &data).to_writer(&mut res),
}
.map_err(|e| crate::Error::TemplateError {
msg: "could not render template for content.opf".to_string(),
cause: e.into(),
})?;
Ok(res)
}
fn render_toc(&mut self) -> Result<Vec<u8>> {
let mut nav_points = String::new();
nav_points.push_str(&self.toc.render_epub(self.escape_html));
let data = upon::value! {
toc_name: common::encode_html(&self.metadata.toc_name, self.escape_html),
nav_points: nav_points
};
let mut res: Vec<u8> = vec![];
templates::TOC_NCX
.render(&Engine::new(), &data)
.to_writer(&mut res)
.map_err(|e| crate::Error::TemplateError {
msg: "error rendering toc.ncx template".to_string(),
cause: e.into(),
})?;
Ok(res)
}
fn render_nav(&mut self, numbered: bool) -> Result<Vec<u8>> {
let content = self.toc.render(numbered, self.escape_html);
let mut landmarks: Vec<String> = Vec::new();
if self.version > EpubVersion::V20 {
for file in &self.files {
if let Some(ref reftype) = file.reftype {
use ReferenceType::*;
let reftype = match *reftype {
Cover => "cover",
Text => "bodymatter",
Toc => "toc",
Bibliography => "bibliography",
Epigraph => "epigraph",
Foreword => "foreword",
Preface => "preface",
Notes => "endnotes",
Loi => "loi",
Lot => "lot",
Colophon => "colophon",
TitlePage => "titlepage",
Index => "index",
Glossary => "glossary",
Copyright => "copyright-page",
Acknowledgements => "acknowledgements",
Dedication => "dedication",
};
if !file.title.is_empty() {
landmarks.push(format!(
"<li><a epub:type=\"{reftype}\" href=\"{href}\">\
{title}</a></li>",
reftype = html_escape::encode_double_quoted_attribute(&reftype),
href = html_escape::encode_double_quoted_attribute(&file.file),
title = common::encode_html(&file.title, self.escape_html),
));
}
}
}
}
let data = upon::value! {
content: content, toc_name: common::encode_html(&self.metadata.toc_name, self.escape_html),
generator_attr: html_escape::encode_double_quoted_attribute(&self.metadata.generator),
landmarks: if !landmarks.is_empty() {
common::indent(
format!(
"<ol>\n{}\n</ol>",
common::indent(landmarks.join("\n"), 1), ),
2,
)
} else {
String::new()
},
};
let mut res: Vec<u8> = vec![];
match self.version {
EpubVersion::V20 => templates::v2::NAV_XHTML.render(&Engine::new(), &data).to_writer(&mut res),
EpubVersion::V30 => templates::v3::NAV_XHTML.render(&Engine::new(), &data).to_writer(&mut res),
EpubVersion::V33 => templates::v3::NAV_XHTML.render(&Engine::new(), &data).to_writer(&mut res),
}
.map_err(|e| crate::Error::TemplateError {
msg: "error rendering nav.xhtml template".to_string(),
cause: e.into(),
})?;
Ok(res)
}
}
fn is_id_char(c: char) -> bool {
c.is_ascii_uppercase()
|| c == '_'
|| c.is_ascii_lowercase()
|| ('\u{C0}'..='\u{D6}').contains(&c)
|| ('\u{D8}'..='\u{F6}').contains(&c)
|| ('\u{F8}'..='\u{2FF}').contains(&c)
|| ('\u{370}'..='\u{37D}').contains(&c)
|| ('\u{37F}'..='\u{1FFF}').contains(&c)
|| ('\u{200C}'..='\u{200D}').contains(&c)
|| ('\u{2070}'..='\u{218F}').contains(&c)
|| ('\u{2C00}'..='\u{2FEF}').contains(&c)
|| ('\u{3001}'..='\u{D7FF}').contains(&c)
|| ('\u{F900}'..='\u{FDCF}').contains(&c)
|| ('\u{FDF0}'..='\u{FFFD}').contains(&c)
|| ('\u{10000}'..='\u{EFFFF}').contains(&c)
|| c == '-'
|| c == '.'
|| c.is_ascii_digit()
|| c == '\u{B7}'
|| ('\u{0300}'..='\u{036F}').contains(&c)
|| ('\u{203F}'..='\u{2040}').contains(&c)
}
fn to_id(s: &str) -> String {
"id_".to_string() + &s.replace(|c: char| !is_id_char(c), "_")
}