use crate::ebook::metadata::datetime::DateTime;
use crate::ebook::resource::ResourceContent;
use crate::ebook::resource::consts::mime;
use crate::ebook::spine::PageDirection;
use crate::ebook::toc::TocEntryKind;
use crate::epub::consts::{dc, marc, opf};
use crate::epub::manifest::DetachedEpubManifestEntry;
use crate::epub::metadata::{DetachedEpubMetaEntry, EpubMetaEntryKind, EpubVersion, marker};
use crate::epub::spine::DetachedEpubSpineEntry;
use crate::epub::toc::DetachedEpubTocEntry;
use crate::epub::{Epub, EpubWriteOptions};
use crate::input::{Batch, IntoOption, Many};
use crate::util;
use crate::util::borrow::{CowExt, MaybeOwned};
use crate::util::uri;
#[derive(Debug, PartialEq)]
pub struct EpubEditor<'ebook> {
pub(super) epub: MaybeOwned<'ebook, Epub>,
}
impl EpubEditor<'static> {
pub(super) 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.creator(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
})))
}
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
}
}