mod writer;
use crate::ebook::archive::ResourceProvider;
use crate::ebook::element::Href;
use crate::ebook::errors::EbookResult;
use crate::ebook::metadata::datetime::DateTime;
use crate::ebook::resource::consts::mime;
use crate::ebook::resource::{Resource, ResourceContent};
use crate::ebook::spine::PageDirection;
use crate::ebook::toc::TocEntryKind;
use crate::epub::Epub;
use crate::epub::archive::EpubArchive;
use crate::epub::consts::{dc, marc, opf};
use crate::epub::manifest::{
DetachedEpubManifestEntry, EpubManifestContext, EpubManifestData, EpubManifestMut,
};
use crate::epub::metadata::{
DetachedEpubMetaEntry, EpubMetaEntryKind, EpubMetadataData, EpubMetadataMut, EpubVersion,
marker,
};
use crate::epub::package::{EpubPackageData, EpubPackageMut};
use crate::epub::spine::{DetachedEpubSpineEntry, EpubSpineContext, EpubSpineData, EpubSpineMut};
use crate::epub::toc::{DetachedEpubTocEntry, EpubTocContext, EpubTocData, EpubTocMut};
use crate::epub::write::writer::{EpubWriteConfig, EpubWriter};
use crate::input::{Batch, IntoOption, Many};
use crate::util;
use crate::util::borrow::{CowExt, MaybeOwned};
use crate::util::sync::SendAndSync;
use crate::util::uri::{self, UriResolver};
use std::fmt::Debug;
use std::io::{Cursor, Write};
use std::path::Path;
impl Epub {
pub fn new() -> Self {
const RBOOK: &str = concat!("rbook v", env!("CARGO_PKG_VERSION"));
let mut epub = Self {
archive: EpubArchive::empty(),
package: EpubPackageData::new("/OEBPS/package.opf".to_owned(), EpubVersion::EPUB3),
metadata: EpubMetadataData::empty(),
manifest: EpubManifestData::empty(),
spine: EpubSpineData::empty(),
toc: EpubTocData::empty(),
};
epub.metadata_mut()
.push(DetachedEpubMetaEntry::meta_name(opf::GENERATOR).value(RBOOK));
epub
}
pub fn builder() -> EpubEditor<'static> {
EpubEditor::new()
}
pub fn edit(&mut self) -> EpubEditor<'_> {
EpubEditor {
epub: MaybeOwned::Borrowed(self),
}
}
pub fn package_mut(&mut self) -> EpubPackageMut<'_> {
EpubPackageMut::new(&mut self.archive, &mut self.package)
}
pub fn metadata_mut(&mut self) -> EpubMetadataMut<'_> {
EpubMetadataMut::new(&mut self.package, &mut self.metadata)
}
pub fn manifest_mut(&mut self) -> EpubManifestMut<'_> {
EpubManifestMut::new(
UriResolver::parent_of(&self.package.location),
(&self.package).into(),
&mut self.archive,
&mut self.manifest,
&mut self.metadata,
&mut self.spine,
&mut self.toc,
)
}
pub fn spine_mut(&mut self) -> EpubSpineMut<'_> {
EpubSpineMut::new(
EpubSpineContext::new(
EpubManifestContext::new(
ResourceProvider::Archive(&self.archive),
(&self.package).into(),
Some(&self.manifest),
),
(&self.package).into(),
),
&mut self.spine,
)
}
pub fn toc_mut(&mut self) -> EpubTocMut<'_> {
EpubTocMut::new(
EpubTocContext::new(EpubManifestContext::new(
ResourceProvider::Archive(&self.archive),
(&self.package).into(),
Some(&self.manifest),
)),
UriResolver::parent_of(&self.package.location),
&mut self.toc,
)
}
pub fn cleanup(&mut self) {
self.manifest.remove_non_existent_references();
let manifest_entries = &self.manifest.entries;
self.spine
.entries
.retain(|entry| manifest_entries.contains_key(&entry.idref));
let hrefs = manifest_entries
.iter()
.map(|(_, entry)| entry.href.as_str())
.collect::<std::collections::HashSet<_>>();
self.toc.recursive_retain(|entry| {
match &entry.href {
Some(href) if !uri::has_scheme(href) => hrefs.contains(uri::path(href)),
_ => true,
}
});
}
#[must_use]
pub fn write(&self) -> EpubWriteOptions<&Self> {
EpubWriteOptions::<&Self>::new(self)
}
}
impl Default for Epub {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, PartialEq)]
pub struct EpubEditor<'ebook> {
epub: MaybeOwned<'ebook, Epub>,
}
impl EpubEditor<'static> {
fn new() -> Self {
Self {
epub: MaybeOwned::Owned(Epub::new()),
}
}
#[must_use]
pub fn build(self) -> Epub {
self.epub
.into_owned()
.expect("`EpubEditor<'static>` should hold an owned `Epub`")
}
}
impl EpubEditor<'_> {
const UNIQUE_IDENTIFIER: &'static str = "unique-identifier";
const DEFAULT_TOC_TITLE: &'static str = "Table of Contents";
const DEFAULT_LANDMARKS_TITLE: &'static str = "Landmarks";
fn generate_id(&self, base: Option<&str>) -> String {
const BASE_MAX_LEN: usize = 50;
let mut id = base
.map(util::str::slugify)
.filter(|s| !s.is_empty())
.unwrap_or_else(|| String::from("entry"));
id.truncate(BASE_MAX_LEN);
if id.chars().next().is_some_and(char::is_numeric) {
id.insert(0, '_');
}
self.epub.manifest.generate_unique_id(id)
}
fn process_manifest_entry(
&mut self,
resource: &mut DetachedEpubManifestEntry,
fallback_id: Option<&str>,
) {
if resource.as_view().id().is_empty() {
let base = fallback_id.or_else(|| {
let href = resource.as_view().href_raw().as_str();
(!href.is_empty()).then_some(href)
});
let id = self.generate_id(base);
resource.as_mut().set_id(id);
}
}
fn insert_checked_meta<I: Into<DetachedEpubMetaEntry>>(
self,
should_be_property: &str,
input: impl Many<I>,
) -> Self {
self.meta(Batch(
input
.iter_many()
.map(|entry| entry.into().force_property(should_be_property)),
))
}
pub fn container_resource(
mut self,
location: impl Into<String>,
content: impl Into<ResourceContent>,
) -> Self {
let location = location.into();
let normalized = uri::normalize(&location).take_owned().unwrap_or(location);
self.epub.archive.insert(normalized, content.into());
self
}
pub fn package_location(mut self, location: impl Into<String>) -> Self {
self.epub.package_mut().set_location(location);
self
}
pub fn version(mut self, version: impl Into<EpubVersion>) -> Self {
self.epub.package_mut().set_version(version);
self
}
pub fn identifier(
mut self,
input: impl Many<DetachedEpubMetaEntry<marker::Identifier>>,
) -> Self {
let mut iter = input.iter_many();
if self.epub.package.unique_identifier.is_empty()
&& let Some(mut identifier) = iter.next()
{
if identifier.as_view().id().is_none() {
identifier.as_mut().set_id(Self::UNIQUE_IDENTIFIER);
}
if let Some(id) = identifier.as_view().id() {
self.epub.package_mut().set_unique_identifier(id);
}
self.epub.metadata_mut().push(identifier);
}
self.meta(Batch(iter))
}
pub fn published_date(mut self, date: impl Into<DetachedEpubMetaEntry<marker::Date>>) -> Self {
self.epub.metadata_mut().retain(|entry| {
if entry.property().as_str() != dc::DATE {
return true;
}
match entry.attributes().by_name(opf::OPF_EVENT) {
Some(event) if event.value() == opf::PUBLICATION => false,
Some(_) => true,
None => false,
}
});
self.epub.metadata_mut().insert(
0,
date.into()
.force_property(dc::DATE)
.force_kind(EpubMetaEntryKind::DublinCore {}),
);
self
}
pub fn modified_date(mut self, date: impl Into<DetachedEpubMetaEntry<marker::Date>>) -> Self {
let date = date.into();
let _ = self.epub.metadata_mut().remove_by_property(dc::MODIFIED);
for mut dc_date in self.epub.metadata_mut().by_property_mut(dc::DATE) {
let opf_event = dc_date.as_view().attributes().get_value(opf::OPF_EVENT);
if let Some(opf::MODIFICATION) = opf_event {
dc_date.set_value(date.as_view().value());
}
}
self.meta(
date.force_property(dc::MODIFIED)
.force_kind(EpubMetaEntryKind::Meta {
version: EpubVersion::EPUB3,
}),
)
}
pub fn modified_now(self) -> Self {
match DateTime::try_now() {
Some(now) => self.modified_date(now),
_ => self,
}
}
pub fn title(self, input: impl Many<DetachedEpubMetaEntry<marker::Title>>) -> Self {
self.meta(Batch(input.iter_many()))
}
pub fn author(self, input: impl Many<DetachedEpubMetaEntry<marker::Contributor>>) -> Self {
self.meta(Batch(input.iter_many().map(|mut author| {
let mut entry = author.as_mut();
let replace_role_attribute = entry
.attributes_mut()
.get_value(opf::OPF_ROLE)
.is_some_and(|role| role != marc::AUTHOR);
let needs_author_role_refinement = entry
.as_view()
.refinements()
.by_property(opf::ROLE)
.all(|role| role.value() != marc::AUTHOR);
if needs_author_role_refinement {
entry.refinements_mut().insert(
0,
DetachedEpubMetaEntry::meta(opf::ROLE)
.attribute((opf::SCHEME, marc::RELATORS))
.value(marc::AUTHOR),
);
}
if replace_role_attribute {
entry.attributes_mut().insert((opf::OPF_ROLE, marc::AUTHOR));
}
author.force_property(dc::CREATOR)
})))
}
pub fn creator(self, input: impl Many<DetachedEpubMetaEntry<marker::Contributor>>) -> Self {
self.insert_checked_meta(dc::CREATOR, input)
}
pub fn contributor(self, input: impl Many<DetachedEpubMetaEntry<marker::Contributor>>) -> Self {
self.insert_checked_meta(dc::CONTRIBUTOR, input)
}
pub fn publisher(self, input: impl Many<DetachedEpubMetaEntry<marker::Contributor>>) -> Self {
self.insert_checked_meta(dc::PUBLISHER, input)
}
pub fn tag(self, input: impl Many<DetachedEpubMetaEntry<marker::Tag>>) -> Self {
self.meta(Batch(input.iter_many()))
}
pub fn description(self, input: impl Many<DetachedEpubMetaEntry<marker::Description>>) -> Self {
self.meta(Batch(input.iter_many()))
}
pub fn language(self, input: impl Many<DetachedEpubMetaEntry<marker::Language>>) -> Self {
self.meta(Batch(input.iter_many()))
}
pub fn rights(self, input: impl Many<DetachedEpubMetaEntry<marker::Rights>>) -> Self {
self.meta(Batch(input.iter_many()))
}
pub fn generator(mut self, generator: impl IntoOption<String>) -> Self {
let _ = self.epub.metadata_mut().remove_by_property(opf::GENERATOR);
match generator.into_option() {
Some(generator) => {
self.meta(DetachedEpubMetaEntry::meta_name(opf::GENERATOR).value(generator))
}
None => self,
}
}
pub fn meta(mut self, detached: impl Many<DetachedEpubMetaEntry>) -> Self {
self.epub.metadata_mut().push(detached);
self
}
pub fn clear_meta(mut self, property: impl AsRef<str>) -> Self {
let _ = self
.epub
.metadata_mut()
.remove_by_property(property.as_ref());
self
}
pub fn resource(mut self, resource: impl Many<DetachedEpubManifestEntry>) -> Self {
self.epub.manifest_mut().push(resource);
self
}
pub fn cover_image(mut self, resource: impl Into<DetachedEpubManifestEntry>) -> Self {
let resource = resource.into().property(opf::COVER_IMAGE);
self.epub.manifest_mut().for_each_mut(|entry| {
entry.properties_mut().remove(opf::COVER_IMAGE);
});
if let Some(mut cover_meta) = self.epub.metadata_mut().by_property_mut(opf::COVER).next() {
cover_meta.set_value(resource.as_view().id());
}
self.resource(resource)
}
pub fn page_direction(mut self, direction: PageDirection) -> Self {
self.epub.spine_mut().set_page_direction(direction);
self
}
pub fn toc_title(mut self, title: impl Into<String>) -> Self {
self.set_toc_root_label(TocEntryKind::Toc, title.into());
self
}
pub fn landmarks_title(mut self, title: impl Into<String>) -> Self {
self.set_toc_root_label(TocEntryKind::Landmarks, title.into());
self
}
fn set_toc_root_label(&mut self, kind: TocEntryKind, title: String) {
let mut found = false;
for mut root in self.epub.toc_mut() {
if root.as_view().kind() == kind {
root.set_label(&title);
found = true;
}
}
if !found {
let version = self.epub.package().version();
self.epub
.toc_mut()
.insert_root(kind, version, DetachedEpubTocEntry::new(title));
}
}
pub fn chapter(mut self, chapter: impl Many<EpubChapter>) -> Self {
for chapter in chapter.iter_many() {
if let Some(toc_entry) = self.dfs_process_chapter(chapter) {
self.insert_into_toc(TocEntryKind::Toc, toc_entry);
}
}
self
}
fn insert_into_toc(&mut self, kind: TocEntryKind, entry: DetachedEpubTocEntry) {
if let Some(mut toc) = self.epub.toc_mut().by_kind_mut(kind) {
toc.push(entry);
} else {
let version = self.epub.package().version();
let default_title = match kind {
TocEntryKind::Landmarks => Self::DEFAULT_LANDMARKS_TITLE,
_ => Self::DEFAULT_TOC_TITLE,
};
self.epub.toc_mut().insert_root(
kind,
version,
DetachedEpubTocEntry::new(default_title).children(entry),
);
}
}
fn dfs_process_chapter(&mut self, mut chapter: EpubChapter) -> Option<DetachedEpubTocEntry> {
self.insert_chapter_resource(&mut chapter);
self.insert_chapter_landmarks(&mut chapter);
for sub in chapter.sub_chapters {
if let Some(child_toc_entry) = self.dfs_process_chapter(sub)
&& let Some(parent) = &mut chapter.toc_entry
{
parent.as_mut().push(child_toc_entry);
}
}
chapter.toc_entry
}
fn insert_chapter_landmarks(&mut self, chapter: &mut EpubChapter) {
if let Some(entry) = &mut chapter.toc_entry
&& entry.as_view().kind_raw().is_some()
{
let landmarks_entry = entry.clone();
self.insert_into_toc(TocEntryKind::Landmarks, landmarks_entry);
}
}
fn insert_chapter_resource(&mut self, chapter: &mut EpubChapter) {
let Some(mut manifest_entry) = chapter.manifest_entry.take() else {
return;
};
let mut spine_entry = chapter
.spine_entry
.take()
.unwrap_or_else(|| DetachedEpubSpineEntry::new(String::new()));
self.process_manifest_entry(
&mut manifest_entry,
chapter
.toc_entry
.as_ref()
.map(|entry| entry.as_view().label()),
);
if manifest_entry.as_view().href_raw().as_str().is_empty() {
let href = self
.epub
.manifest
.generate_unique_href(util::str::suffix(".xhtml", manifest_entry.as_view().id()));
if let Some(toc_entry) = chapter.toc_entry.as_mut() {
toc_entry.as_mut().set_href(Some(href.clone()));
}
manifest_entry.as_mut().set_href(href);
}
manifest_entry.as_mut().set_media_type(mime::XHTML);
spine_entry
.as_mut()
.set_idref(manifest_entry.as_view().id());
self.epub.manifest_mut().push(manifest_entry);
self.epub.spine_mut().push(spine_entry);
}
#[must_use]
pub fn write(&self) -> EpubWriteOptions<&Epub> {
self.epub.write()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct EpubChapter {
sub_chapters: Vec<Self>,
spine_entry: Option<DetachedEpubSpineEntry>,
manifest_entry: Option<DetachedEpubManifestEntry>,
toc_entry: Option<DetachedEpubTocEntry>,
}
impl EpubChapter {
pub fn new(title: impl Into<String>) -> Self {
Self {
sub_chapters: Vec::new(),
spine_entry: None,
manifest_entry: None,
toc_entry: Some(DetachedEpubTocEntry::new(title)),
}
}
pub fn unlisted(href: impl Into<String>) -> Self {
Self {
sub_chapters: Vec::new(),
spine_entry: None,
manifest_entry: Some(
DetachedEpubManifestEntry::new(String::new()),
),
toc_entry: None,
}
.href(href)
}
pub fn with_spine_entry(mut self, entry: DetachedEpubSpineEntry) -> Self {
self.spine_entry = Some(entry);
self
}
pub fn with_manifest_entry(mut self, entry: DetachedEpubManifestEntry) -> Self {
self.manifest_entry = Some(entry);
self
}
pub fn with_toc_entry(mut self, entry: DetachedEpubTocEntry) -> Self {
self.toc_entry = Some(entry);
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
if let Some(toc) = &mut self.toc_entry {
toc.as_mut().set_label(title);
}
self
}
pub fn href(mut self, href: impl Into<String>) -> Self {
let href = href.into();
if let Some(entry) = &mut self.manifest_entry {
entry.as_mut().set_href(&href);
}
if let Some(entry) = &mut self.toc_entry {
entry.as_mut().set_href(href);
}
self
}
pub fn kind(mut self, kind: impl Into<TocEntryKind<'static>>) -> Self {
if let Some(toc) = &mut self.toc_entry {
toc.as_mut().set_kind(kind.into());
}
self
}
pub fn supplementary(mut self, is_supplementary: bool) -> Self {
let spine_entry = self.spine_entry.get_or_insert_with(|| {
DetachedEpubSpineEntry::new(String::new())
});
spine_entry.as_mut().set_linear(!is_supplementary);
self
}
pub fn xhtml(mut self, xhtml: impl Into<ResourceContent>) -> Self {
let entry = self.manifest_entry.get_or_insert_with(|| {
let mut detached = DetachedEpubManifestEntry::new("");
if let Some(toc_entry) = self.toc_entry.as_ref().and_then(|e| e.as_view().href_raw()) {
detached.as_mut().set_href(toc_entry.as_str());
}
detached
});
entry.as_mut().set_content(xhtml.into());
self
}
pub fn xhtml_body(self, body: impl Into<Vec<u8>>) -> Self {
let title = self
.toc_entry
.as_ref()
.map(|toc_entry| toc_entry.as_view().label())
.unwrap_or_default();
let mut xhtml = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\
\n<head>\
\n <title>{}</title>\
\n</head>\
\n<body>\n",
quick_xml::escape::escape(title),
)
.into_bytes();
xhtml.extend(body.into());
xhtml.extend(b"\n</body>\n</html>");
self.xhtml(xhtml)
}
pub fn children(mut self, sub_chapter: impl Many<Self>) -> Self {
self.sub_chapters.extend(sub_chapter.iter_many());
self
}
}
#[derive(Clone, Debug)]
pub struct EpubWriteOptions<T = ()> {
container: T,
config: EpubWriteConfig,
}
impl<T> EpubWriteOptions<T> {
fn save_epub(epub: &Epub, config: &EpubWriteConfig, path: &Path) -> EbookResult<()> {
const TEMP: &str = "rbook.tmp";
let temp = path.with_extension(TEMP);
let write_result = (|| {
let file = std::fs::File::create(&temp)?;
let mut buf = std::io::BufWriter::new(file);
Self::write_epub(epub, config, &mut buf)?;
buf.flush()?;
std::fs::rename(&temp, path)?;
Ok(())
})();
if let Err(error) = write_result {
let _ = std::fs::remove_file(&temp);
return Err(error);
}
Ok(())
}
fn write_epub<W: Write>(epub: &Epub, config: &EpubWriteConfig, write_to: W) -> EbookResult<W> {
EpubWriter::new(config, epub, write_to).write()
}
fn vec_epub(epub: &Epub, config: &EpubWriteConfig) -> EbookResult<Vec<u8>> {
let cursor = Cursor::new(Vec::new());
Self::write_epub(epub, config, cursor).map(Cursor::into_inner)
}
pub fn target(&mut self, target: impl Many<EpubVersion>) -> &mut Self {
self.config.targets.clear();
for version in target.iter_many() {
self.config.targets.add(version);
}
self
}
pub fn generate_toc(&mut self, generate: bool) -> &mut Self {
self.config.generate_toc = generate;
self
}
pub fn toc_stylesheet<'a>(&mut self, location: impl Many<Resource<'a>>) -> &mut Self {
self.config.generated_toc_stylesheets = Some(
location
.iter_many()
.filter_map(|resource| resource.key().value().map(str::to_owned))
.collect(),
);
self
}
pub fn compression(&mut self, level: u8) -> &mut Self {
self.config.compression = level.min(9);
self
}
pub fn keep_orphans(&mut self, keep: impl OrphanFilter + 'static) -> &mut Self {
self.config.keep_orphans = Some(std::sync::Arc::new(keep));
self
}
}
impl<'ebook> EpubWriteOptions<&'ebook Epub> {
fn new(epub: &'ebook Epub) -> Self {
Self {
container: epub,
config: EpubWriteConfig::default(),
}
}
pub fn save(&self, path: impl AsRef<Path>) -> EbookResult<()> {
Self::save_epub(self.container, &self.config, path.as_ref())
}
pub fn write<W: Write>(&self, writer: W) -> EbookResult<W> {
Self::write_epub(self.container, &self.config, writer)
}
pub fn to_vec(&self) -> EbookResult<Vec<u8>> {
Self::vec_epub(self.container, &self.config)
}
}
impl EpubWriteOptions {
pub fn save(&self, epub: &Epub, path: impl AsRef<Path>) -> EbookResult<()> {
Self::save_epub(epub, &self.config, path.as_ref())
}
pub fn write<W: Write>(&self, epub: &Epub, write_to: W) -> EbookResult<W> {
Self::write_epub(epub, &self.config, write_to)
}
pub fn to_vec(&self, epub: &Epub) -> EbookResult<Vec<u8>> {
Self::vec_epub(epub, &self.config)
}
}
impl Default for EpubWriteOptions {
fn default() -> Self {
Self {
container: (),
config: EpubWriteConfig::default(),
}
}
}
pub trait OrphanFilter: SendAndSync {
fn filter(&self, path: Href<'_>) -> bool;
}
impl OrphanFilter for bool {
fn filter(&self, _href: Href<'_>) -> bool {
*self
}
}
impl<F: Fn(Href<'_>) -> bool + SendAndSync + 'static> OrphanFilter for F {
fn filter(&self, href: Href<'_>) -> bool {
self(href)
}
}