mod azw3;
mod epub;
mod kfx;
mod mobi;
pub use azw3::Azw3Importer;
pub use epub::EpubImporter;
pub use kfx::KfxImporter;
pub use mobi::MobiImporter;
use std::path::{Path, PathBuf};
use crate::book::{Landmark, Metadata, TocEntry};
use crate::compiler::{Origin, Stylesheet, compile_html_bytes, extract_stylesheets};
use crate::ir::IRChapter;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ChapterId(pub u32);
#[derive(Debug, Clone)]
pub struct SpineEntry {
pub id: ChapterId,
pub size_estimate: usize,
}
pub trait Importer: Send + Sync {
fn open(path: &Path) -> std::io::Result<Self>
where
Self: Sized;
fn metadata(&self) -> &Metadata;
fn toc(&self) -> &[TocEntry];
fn landmarks(&self) -> &[Landmark];
fn spine(&self) -> &[SpineEntry];
fn load_chapter(&mut self, id: ChapterId) -> std::io::Result<IRChapter> {
let html_bytes = self.load_raw(id)?;
let html_str = String::from_utf8_lossy(&html_bytes);
let (linked, inline) = extract_stylesheets(&html_str);
let mut stylesheets = Vec::new();
for href in linked {
let css_path = if let Some(chapter_path) = self.source_id(id) {
resolve_relative_path(chapter_path, &href)
} else {
PathBuf::from(&href)
};
if let Ok(css_bytes) = self.load_asset(&css_path) {
let css_str = String::from_utf8_lossy(&css_bytes);
stylesheets.push((Stylesheet::parse(&css_str), Origin::Author));
}
}
for css in inline {
stylesheets.push((Stylesheet::parse(&css), Origin::Author));
}
let mut chapter = compile_html_bytes(&html_bytes, &stylesheets);
if let Some(base_path) = self.source_id(id) {
resolve_semantic_paths(&mut chapter, base_path);
}
Ok(chapter)
}
fn source_id(&self, id: ChapterId) -> Option<&str>;
fn load_raw(&mut self, id: ChapterId) -> std::io::Result<Vec<u8>>;
fn list_assets(&self) -> Vec<PathBuf>;
fn load_asset(&mut self, path: &Path) -> std::io::Result<Vec<u8>>;
fn requires_normalized_export(&self) -> bool {
false
}
}
fn resolve_relative_path(base: &str, relative: &str) -> PathBuf {
if relative.starts_with('/') || relative.contains("://") {
return PathBuf::from(relative);
}
let base_path = Path::new(base);
let base_dir = base_path.parent().unwrap_or(Path::new(""));
let joined = base_dir.join(relative);
let mut result = PathBuf::new();
for component in joined.components() {
match component {
std::path::Component::ParentDir => {
result.pop();
}
std::path::Component::Normal(name) => {
result.push(name);
}
std::path::Component::CurDir => {}
std::path::Component::RootDir => {
result.push("/");
}
std::path::Component::Prefix(prefix) => {
result.push(prefix.as_os_str());
}
}
}
result
}
fn resolve_semantic_paths(chapter: &mut IRChapter, base_path: &str) {
chapter.semantics.resolve_paths(|path| {
if path.contains("://") || path.starts_with("data:") {
return path.to_string();
}
let resolved = resolve_relative_path(base_path, path);
resolved.to_string_lossy().replace('\\', "/")
});
}