mod ncx;
mod xhtml;
use crate::ebook::resource::consts::mime;
use crate::ebook::toc::TocEntryKind;
use crate::epub::Epub;
use crate::epub::consts::opf;
use crate::epub::manifest::EpubManifestEntryData;
use crate::epub::metadata::EpubVersion;
use crate::epub::toc::EpubTocEntryData;
use crate::epub::write::writer::toc::ncx::NcxTocWriter;
use crate::epub::write::writer::toc::xhtml::XhtmlTocWriter;
use crate::epub::write::writer::{EpubWriteConfig, EpubWriter, EpubWriterContext};
use crate::util::uri;
use crate::writer::WriterResult;
use std::borrow::Cow;
use std::io::Write;
pub(super) const EPUB2_TOC_FALLBACKS: [EpubVersion; 2] = [EpubVersion::EPUB2, EpubVersion::EPUB3];
const EPUB3_TOC_FALLBACKS: [EpubVersion; 2] = [EpubVersion::EPUB3, EpubVersion::EPUB2];
pub(super) struct TocData<'a> {
pub(super) is_generated: bool,
pub(super) id: Cow<'a, str>,
pub(super) location: Cow<'a, str>,
}
pub(super) struct TocContext<'a> {
pub(super) epub2_ncx: Option<TocData<'a>>,
pub(super) epub3_nav: Option<TocData<'a>>,
}
impl<'a> TocContext<'a> {
pub(super) fn generate(config: &EpubWriteConfig, epub: &'a Epub) -> Self {
const NCX_ID: &str = "ncx";
const NCX_FILE: &str = "toc.ncx";
const XHTML_ID: &str = "nav";
const XHTML_FILE: &str = "toc.xhtml";
let version = epub.package.version.parsed;
let mut epub2_ncx = None;
let mut epub3_nav = None;
if version.is_epub2() || config.targets.epub2 {
epub2_ncx = Self::resolve_toc_data(config, epub, NCX_ID, NCX_FILE, |entry| {
entry.media_type == mime::NCX
});
}
if version.is_epub3() || config.targets.epub3 {
epub3_nav = Self::resolve_toc_data(config, epub, XHTML_ID, XHTML_FILE, |entry| {
entry.properties.has_property(opf::NAV_PROPERTY)
});
}
TocContext {
epub2_ncx,
epub3_nav,
}
}
fn resolve_toc_data(
config: &EpubWriteConfig,
epub: &'a Epub,
default_toc_id: &'static str,
default_toc_href: &'static str,
predicate: fn(&EpubManifestEntryData) -> bool,
) -> Option<TocData<'a>> {
let manifest = &epub.manifest;
let data = manifest
.entries
.iter()
.find(|(_, entry)| predicate(entry))
.map(|(id, entry)| TocData {
is_generated: false,
id: Cow::Borrowed(id.as_str()),
location: Cow::Borrowed(entry.href.as_str()),
});
if let Some(data) = data {
return Some(data);
}
else if !config.generate_toc {
return None;
}
let id = manifest.generate_unique_id(default_toc_id.to_owned());
let href = manifest.generate_unique_href(uri::join(
epub.package().directory().as_str(),
default_toc_href,
));
Some(TocData {
is_generated: true,
id: Cow::Owned(id),
location: Cow::Owned(href),
})
}
}
impl<W: Write> EpubWriter<'_, W> {
pub(super) fn write_toc(&mut self) -> WriterResult<()> {
if !self.ctx.config.generate_toc {
return Ok(());
}
if let Some(ncx) = &self.ctx.toc.epub2_ncx {
self.zip.start_file(&uri::decode(&ncx.location))?;
NcxTocWriter::new(&self.ctx, ncx, &mut self.zip).write_ncx()?;
}
if let Some(nav) = &self.ctx.toc.epub3_nav {
self.zip.start_file(&uri::decode(&nav.location))?;
XhtmlTocWriter::new(&self.ctx, nav, &mut self.zip).write_xhtml()?;
}
Ok(())
}
}
pub(super) fn get_toc_root<'ebook>(
ctx: &'ebook EpubWriterContext,
kind: TocEntryKind<'_>,
versions: impl IntoIterator<Item = EpubVersion>,
) -> Option<&'ebook EpubTocEntryData> {
let toc = &ctx.epub.toc.entries;
let kind = kind.as_str();
for version in versions {
if let Some(root) = toc.get(&(kind, version)) {
return Some(root);
}
}
None
}