use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::io::Write;
use std::sync::{Arc, Mutex};
use super::common::{self};
use super::html::{get_html_info, to_html};
use crate::cache_struct;
use crate::common::{escape_xml, IError, IResult};
use crate::epub::common::LinkRel;
crate::cache_enum!{
#[derive(Clone)]
pub enum Direction {
RTL,
LTR,
CUS(String),
}
}
impl Display for Direction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Direction::RTL => f.write_str("rtl"),
Direction::LTR => f.write_str("ltr"),
Direction::CUS(v) => f.write_fmt(format_args!("{}", escape_xml(v))),
}
}
}
impl From<String> for Direction {
fn from(value: String) -> Self {
if value.eq_ignore_ascii_case("rtl") {
Direction::RTL
} else if value.eq_ignore_ascii_case("ltr") {
Direction::LTR
} else {
Direction::CUS(value)
}
}
}
pub(crate) mod info {
include!(concat!(env!("OUT_DIR"), "/version.rs"));
}
macro_rules! epub_base_field{
(
// meta data about struct
$(#[$meta:meta])*
$vis:vis struct $struct_name:ident {
$(
// meta data about field
$(#[$field_meta:meta])*
$field_vis:vis $field_name:ident : $field_type:ty
),*$(,)?
}
) => {
crate::cache_struct!{
$(#[$meta])*
pub struct $struct_name{
id:String,
_file_name:String,
media_type:String,
_data: Option<Vec<u8>>,
#[cfg(not(feature="cache"))]
reader:Option<std::sync::Arc<std::sync::Mutex< Box<dyn EpubReaderTrait+Send+Sync>>>>,
#[cfg(feature="cache")]
#[serde(skip)]
reader:Option<std::sync::Arc<std::sync::Mutex< Box<dyn EpubReaderTrait+Send+Sync>>>>,
$(
$(#[$field_meta])*
$field_vis $field_name : $field_type,
)*
}
}
impl $struct_name {
pub fn file_name(&self)->&str{
self._file_name.as_str()
}
pub fn set_file_name<T:Into<String>>(&mut self,value: T){
self._file_name = value.into();
}
pub fn id(&self)->&str{
self.id.as_str()
}
pub fn set_id<T:Into<String>>(&mut self,id: T){
self.id = id.into();
}
pub fn set_data(&mut self, data: Vec<u8>) {
self._data = Some(data);
}
pub fn with_file_name<T: Into<String>>(mut self,value: T)->Self{
self.set_file_name(value);
self
}
pub fn with_data(mut self, value:Vec<u8>)->Self{
self.set_data(value);
self
}
}
}
}
crate::cache_struct! {
#[derive(Debug)]
pub struct EpubLink {
pub rel: LinkRel,
pub file_type: String,
pub href: String,
}
}
epub_base_field! {
#[derive(Default)]
pub struct EpubHtml {
pub(crate) lang: String,
links: Option<Vec<EpubLink>>,
title: String,
css: Option<String>,
raw_data:Option<String>,
pub(crate) direction:Option<Direction>,
}
}
impl Debug for EpubHtml {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EpubHtml")
.field("id", &self.id)
.field("_file_name", &self._file_name)
.field("media_type", &self.media_type)
.field("_data", &self._data)
.field("lang", &self.lang)
.field("links", &self.links)
.field("title", &self.title)
.field("css", &self.css)
.finish()
}
}
impl EpubHtml {
pub fn string_data(&self) -> String {
if let Some(data) = &self._data {
String::from_utf8(data.clone()).unwrap_or_else(|_e| String::new())
} else {
String::new()
}
}
pub fn data(&self) -> Option<&[u8]> {
self._data.as_ref().map(|f| f.as_slice())
}
pub fn data_mut(&mut self) -> Option<&[u8]> {
let (id, origin) = if let Some(index) = self._file_name.find(|f| f == '#') {
(
Some(&self._file_name[(index + 1)..]),
self._file_name[0..index].to_string(),
)
} else {
(None, self.file_name().to_string())
};
let mut f = String::from(self._file_name.as_str());
let prefixs = vec!["", common::EPUB, common::EPUB3];
if self._data.is_none() && self.reader.is_some() && !f.is_empty() {
for prefix in prefixs.iter() {
f = format!("{prefix}{origin}");
let s = self.reader.as_mut().unwrap();
let d = s.lock().unwrap().read_string(f.as_str());
match d {
Ok(v) => {
if let Ok((title, data, lang, direction)) = get_html_info(v.as_str(), id) {
if !title.is_empty() {
self.set_title(&title);
}
self.set_data(data);
if let Some(lang) = lang {
self.set_language(lang);
}
self.direction = direction;
}
break;
}
Err(IError::FileNotFound) => {}
Err(e) => {
break;
}
}
}
}
self._data.as_deref()
}
pub fn release_data(&mut self) {
if let Some(data) = &mut self._data {
data.clear();
}
self._data = None;
}
pub fn format(&mut self) -> Option<String> {
self.data_mut();
Some(to_html(self, false, &None))
}
pub fn raw_data(&mut self) -> Option<&str> {
let (id, origin) = if let Some(index) = self._file_name.find(|f| f == '#') {
(
Some(&self._file_name[(index + 1)..]),
self._file_name[0..index].to_string(),
)
} else {
(None, self.file_name().to_string())
};
let mut f = String::from(self._file_name.as_str());
let prefixs = vec!["", common::EPUB, common::EPUB3];
if self.raw_data.is_none() && self.reader.is_some() && !f.is_empty() {
for prefix in prefixs.iter() {
f = format!("{prefix}{origin}");
let s = self.reader.as_mut().unwrap();
let d = s.lock().unwrap().read_string(f.as_str());
if let Ok(data) = d {
self.raw_data = Some(data);
}
}
}
self.raw_data.as_deref()
}
pub fn release_raw_data(&mut self) {
if let Some(data) = &mut self.raw_data {
data.clear();
}
self.raw_data = None;
}
pub fn set_title<T: Into<String>>(&mut self, title: T) {
self.title = title.into();
}
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
self.set_title(title);
self
}
pub fn title(&self) -> &str {
&self.title
}
pub fn set_css<T: Into<String>>(&mut self, css: T) {
self.css = Some(css.into());
}
pub fn with_css<T: Into<String>>(mut self, css: T) -> Self {
self.set_css(css);
self
}
pub fn css(&self) -> Option<&str> {
self.css.as_deref()
}
pub fn set_language<T: Into<String>>(&mut self, lang: T) {
self.lang = lang.into();
}
pub fn with_language<T: Into<String>>(mut self, lang: T) -> Self {
self.set_language(lang);
self
}
pub fn links(&self) -> Option<std::slice::Iter<EpubLink>> {
self.links.as_ref().map(|f| f.iter())
}
pub fn add_link(&mut self, link: EpubLink) {
if let Some(links) = &mut self.links {
links.push(link);
} else {
self.links = Some(vec![link]);
}
}
fn get_links(&mut self) -> Option<&mut Vec<EpubLink>> {
self.links.as_mut()
}
pub fn set_direction(&mut self, dir: Direction) {
self.direction = Some(dir);
}
pub fn with_direction(mut self, dir: Direction) -> Self {
self.set_direction(dir);
self
}
}
epub_base_field! {
#[derive(Default,Clone)]
pub struct EpubAssets {
pub(crate) version:String,
}
}
impl EpubAssets {
pub fn with_version<T: Into<String>>(&mut self, version: T) {
self.version = version.into();
}
pub fn data(&self) -> Option<&[u8]> {
self._data.as_deref()
}
pub fn data_mut(&mut self) -> Option<&[u8]> {
let mut f = String::from(self._file_name.as_str());
if self._data.is_none() && self.reader.is_some() && !f.is_empty() {
let prefixs = vec!["", common::EPUB, common::EPUB3];
if self._data.is_none() && self.reader.is_some() && !f.is_empty() {
for prefix in prefixs.iter() {
let s = self.reader.as_mut().unwrap();
f = format!("{prefix}{}", self._file_name);
let d = s.lock().unwrap().read_file(f.as_str());
if let Ok(v) = d {
self.set_data(v);
break;
}
}
}
}
self._data.as_deref()
}
pub fn write_to<W: Write>(&mut self, writer: &mut W) -> IResult<()> {
if let Some(data) = self.data_mut() {
writer.write_all(&data)?;
writer.flush()?;
}
Ok(())
}
pub fn save_to<T: AsRef<str>>(&mut self, file_path: T) -> IResult<()> {
let mut f: String = self._file_name.clone();
if self.reader.is_some() && !f.is_empty() {
let prefixs = vec!["", common::EPUB, common::EPUB3];
for prefix in prefixs.iter() {
let s = self.reader.as_mut().unwrap();
f = format!("{prefix}{}", self._file_name);
let d: Result<(), IError> = s
.lock()
.unwrap()
.read_to_path(f.as_str(), file_path.as_ref());
if d.is_ok() {
break;
}
}
}
Ok(())
}
pub fn release_data(&mut self) {
if let Some(data) = &mut self._data {
data.clear();
}
self._data = None;
}
}
impl Debug for EpubAssets {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EpubAssets")
.field("id", &self.id)
.field("_file_name", &self._file_name)
.field("media_type", &self.media_type)
.field("_data", &self._data)
.field("reader_mode", &self.reader.is_some())
.finish()
}
}
epub_base_field! {
#[derive(Default)]
pub struct EpubNav {
title: String,
child: Vec<EpubNav>,
}
}
impl Debug for EpubNav {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EpubNav")
.field("id", &self.id)
.field("_file_name", &self._file_name)
.field("media_type", &self.media_type)
.field("_data", &self._data)
.field("title", &self.title)
.field("child", &self.child)
.finish()
}
}
impl Clone for EpubNav {
fn clone(&self) -> Self {
Self {
id: self.id.clone(),
_file_name: self._file_name.clone(),
media_type: self.media_type.clone(),
_data: self._data.clone(),
reader: None,
title: self.title.clone(),
child: self.child.clone(),
}
}
}
impl EpubNav {
pub fn title(&self) -> &str {
&self.title
}
pub fn set_title<T: Into<String>>(&mut self, title: T) {
self.title = title.into();
}
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
self.set_title(title);
self
}
pub fn push(&mut self, child: EpubNav) {
self.child.push(child);
}
pub fn child(&self) -> std::slice::Iter<EpubNav> {
self.child.iter()
}
}
cache_struct! {
#[derive(Debug, Default)]
pub struct EpubMetaData {
attr: HashMap<String, String>,
text: Option<String>,
}
}
impl EpubMetaData {
pub fn with_attr<K: Into<String>>(mut self, key: K, value: K) -> Self {
self.push_attr(key, value);
self
}
pub fn push_attr<T: Into<String>>(&mut self, key: T, value: T) {
self.attr.insert(key.into(), value.into());
}
pub fn with_text<T: Into<String>>(mut self, text: T) -> Self {
self.set_text(text);
self
}
pub fn set_text<T: Into<String>>(&mut self, text: T) {
self.text = Some(text.into());
}
pub fn text(&self) -> Option<&str> {
self.text.as_deref()
}
pub fn attrs(&self) -> std::collections::hash_map::Iter<'_, String, String> {
self.attr.iter()
}
pub fn get_attr<T: AsRef<str>>(&self, key: T) -> Option<&String> {
self.attr.get(key.as_ref())
}
}
crate::cache_struct! {
#[derive(Default)]
pub struct EpubBook {
last_modify: Option<String>,
generator: Option<String>,
info: crate::common::BookInfo,
meta: Vec<EpubMetaData>,
nav: Vec<EpubNav>,
assets: Vec<EpubAssets>,
chapters: Vec<EpubHtml>,
cover: Option<EpubAssets>,
version: String,
#[cfg(not(feature="cache"))]
reader:Option<std::sync::Arc<std::sync::Mutex< Box<dyn EpubReaderTrait+Send+Sync>>>>,
#[cfg(feature="cache")]
#[serde(skip)]
reader:Option<std::sync::Arc<std::sync::Mutex< Box<dyn EpubReaderTrait+Send+Sync>>>>,
pub(crate) prefix: String,
pub(crate) direction: Option<Direction>,
language: Option<String>,
}
}
impl Display for EpubBook {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f,"last_modify={:?},info={:?},meta={:?},nav={:?},assets={:?},chapters={:?},cover={:?},is in read mode={}",
self.last_modify,
self.info,
self.meta,
self.nav,
self.assets,
self.chapters,
self.cover,
self.reader.is_some()
)
}
}
impl EpubBook {
iepub_derive::option_string_method!(info, creator);
iepub_derive::option_string_method!(info, description);
iepub_derive::option_string_method!(info, contributor);
iepub_derive::option_string_method!(info, date);
iepub_derive::option_string_method!(info, format);
iepub_derive::option_string_method!(info, publisher);
iepub_derive::option_string_method!(info, subject);
iepub_derive::option_string_method!(last_modify);
iepub_derive::option_string_method!(generator);
iepub_derive::option_string_method!(language);
}
impl EpubBook {
pub fn set_direction(&mut self, dir: Direction) {
self.direction = Some(dir);
}
pub fn with_direction(mut self, dir: Direction) -> Self {
self.set_direction(dir);
self
}
pub fn set_title<T: AsRef<str>>(&mut self, title: T) {
self.info.title.clear();
self.info.title.push_str(title.as_ref());
}
pub fn title(&self) -> &str {
self.info.title.as_str()
}
pub fn with_title<T: AsRef<str>>(mut self, title: T) -> Self {
self.set_title(title.as_ref());
self
}
pub fn identifier(&self) -> &str {
self.info.identifier.as_str()
}
pub fn set_identifier<T: AsRef<str>>(&mut self, identifier: T) {
self.info.identifier.clear();
self.info.identifier.push_str(identifier.as_ref());
}
pub fn with_identifier<T: AsRef<str>>(mut self, identifier: T) -> Self {
self.set_identifier(identifier.as_ref());
self
}
pub fn add_meta(&mut self, meta: EpubMetaData) {
self.meta.push(meta);
}
pub fn meta(&self) -> &[EpubMetaData] {
&self.meta
}
pub fn get_meta_mut(&mut self, index: usize) -> Option<&mut EpubMetaData> {
self.meta.get_mut(index)
}
pub fn get_meta(&self, index: usize) -> Option<&EpubMetaData> {
self.meta.get(index)
}
pub fn meta_len(&self) -> usize {
self.meta.len()
}
pub(crate) fn set_reader(
&mut self,
reader: Arc<Mutex<Box<dyn EpubReaderTrait + Send + Sync>>>,
) {
self.reader = Some(reader)
}
#[inline]
pub fn add_nav(&mut self, nav: EpubNav) {
self.nav.push(nav);
}
pub fn add_assets(&mut self, mut assets: EpubAssets) {
if let Some(r) = &self.reader {
assets.reader = Some(Arc::clone(r));
}
self.assets.push(assets);
}
pub fn get_assets<T: AsRef<str>>(&self, file_name: T) -> Option<&EpubAssets> {
self.assets
.iter()
.find(|s| s.file_name() == file_name.as_ref())
}
pub fn get_assets_mut<T: AsRef<str>>(&mut self, file_name: T) -> Option<&mut EpubAssets> {
self.assets
.iter_mut()
.find(|s| s.file_name() == file_name.as_ref())
}
pub fn assets(&self) -> std::slice::Iter<EpubAssets> {
self.assets.iter()
}
pub fn assets_mut(&mut self) -> std::slice::IterMut<EpubAssets> {
self.assets.iter_mut()
}
pub fn add_chapter(&mut self, mut chap: EpubHtml) {
if let Some(r) = &self.reader {
chap.reader = Some(Arc::clone(r));
}
self.chapters.push(chap);
}
pub fn chapters_mut(&mut self) -> std::slice::IterMut<EpubHtml> {
self.chapters.iter_mut()
}
pub fn chapters(&self) -> std::slice::Iter<EpubHtml> {
self.chapters.iter()
}
pub fn get_chapter<T: AsRef<str>>(&self, file_name: T) -> Option<&EpubHtml> {
self.chapters.iter().find(|s| {
return s.file_name() == file_name.as_ref();
})
}
pub fn get_chapter_mut<T: AsRef<str>>(&mut self, file_name: T) -> Option<&mut EpubHtml> {
self.chapters.iter_mut().find(|s| {
return s.file_name() == file_name.as_ref();
})
}
pub fn set_version<T: AsRef<str>>(&mut self, version: T) {
self.version.clear();
self.version.push_str(version.as_ref());
}
pub fn version(&self) -> &str {
self.version.as_ref()
}
pub fn nav(&self) -> std::slice::Iter<EpubNav> {
self.nav.iter()
}
pub fn set_cover(&mut self, cover: EpubAssets) {
self.cover = Some(cover);
}
pub fn cover(&self) -> Option<&EpubAssets> {
self.cover.as_ref()
}
pub fn cover_mut(&mut self) -> Option<&mut EpubAssets> {
self.cover.as_mut()
}
pub(crate) fn update_chapter(&mut self) {
let f = flatten_nav(&self.nav);
let mut map = HashMap::new();
for (index, ele) in self.chapters.iter_mut().enumerate() {
if let Some(v) = self.nav.iter().find(|f| f.file_name() == ele.file_name()) {
ele.set_title(v.title());
} else {
let id_nav: Vec<&&EpubNav> = f
.iter()
.filter(|f| {
f.file_name().contains("#") && f.file_name().starts_with(ele.file_name())
})
.collect();
if !id_nav.is_empty() {
map.insert(index, id_nav);
}
}
}
let mut offset = 0;
for (index, nav) in map {
for ele in nav {
let mut chap = EpubHtml::default()
.with_title(ele.title())
.with_file_name(ele.file_name());
if let Some(r) = &self.reader {
chap.reader = Some(Arc::clone(r));
}
self.chapters.insert(index + offset, chap);
offset = offset + 1;
}
}
}
pub(crate) fn update_assets(&mut self) {
let version = self.version().to_string();
for assets in self.assets_mut() {
assets.with_version(&version);
}
}
#[cfg(feature = "cache")]
pub fn cache<T: AsRef<std::path::Path>>(&self, file: T) -> IResult<()> {
std::fs::write(file, serde_json::to_string(self).unwrap())?;
Ok(())
}
#[cfg(feature = "cache")]
pub fn load_from_cache<T: AsRef<std::path::Path>>(file: T) -> IResult<EpubBook> {
let file = std::fs::File::open(file)?;
let reader = std::io::BufReader::new(file);
let u: EpubBook = serde_json::from_reader(reader)?;
Ok(u)
}
}
fn flatten_nav(nav: &[EpubNav]) -> Vec<&EpubNav> {
let mut n = Vec::new();
for ele in nav {
if ele.child.is_empty() {
n.push(ele);
} else {
n.append(&mut flatten_nav(&ele.child));
}
}
n
}
pub(crate) trait EpubReaderTrait: Send + Sync {
fn read(&mut self, book: &mut EpubBook) -> IResult<()>;
fn read_file(&mut self, file_name: &str) -> IResult<Vec<u8>>;
fn read_string(&mut self, file_name: &str) -> IResult<String>;
fn read_to_path(&mut self, file_name: &str, file_path: &str) -> IResult<()>;
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
fn book() -> EpubBook {
let mut book = EpubBook::default();
let mut css = EpubAssets::default();
css.set_file_name("style/1.css");
css.set_data(String::from("ok").as_bytes().to_vec());
book.add_assets(css);
let mut n = EpubNav::default();
n.set_title("作品说明");
n.set_file_name("chaps/0.xhtml");
let mut n1 = EpubNav::default();
n1.set_title("第一卷");
let mut n2 = EpubNav::default();
n2.set_title("第一卷 第一章");
n2.set_file_name("chaps/1.xhtml");
let mut n3 = EpubNav::default();
n3.set_title("第一卷 第二章");
n3.set_file_name("chaps/2.xhtml");
n1.push(n2);
book.add_nav(n);
book.add_nav(n1);
book.set_version("2.0");
let mut chap = EpubHtml::default();
chap.set_file_name("chaps/0.xhtml");
chap.set_title("标题1");
chap.set_data(String::from("<p>章节内容html片段</p>").as_bytes().to_vec());
book.add_chapter(chap);
chap = EpubHtml::default();
chap.set_file_name("chaps/1.xhtml");
chap.set_title("标题2");
chap.set_data(String::from("第一卷 第一章content").as_bytes().to_vec());
book.add_chapter(chap);
chap = EpubHtml::default();
chap.set_file_name("chaps/2.xhtml");
chap.set_title("标题2");
chap.set_data(String::from("第一卷 第二章content").as_bytes().to_vec());
book.add_chapter(chap);
book.set_title("书名");
book.set_creator("作者");
book.set_identifier("id");
book.set_description("desc");
book.set_date("29939");
book.set_subject("subject");
book.set_format("format");
book.set_publisher("publisher");
book.set_contributor("contributor");
let mut cover = EpubAssets::default();
cover.set_file_name("cover.jpg");
let data = vec![2];
cover.set_data(data);
book.set_cover(cover);
book
}
#[test]
fn write_assets() {
let mut book: EpubBook = book();
EpubWriter::write_to_file("12.epub", &mut book, true).unwrap();
}
#[test]
#[cfg(feature = "cache")]
fn test_cache() {
let mut book: EpubBook = book();
let f = if std::path::Path::new("target").exists() {
"target/cache.json"
} else {
"../target/cache.json"
};
book.cache(f).unwrap();
let book2 = EpubBook::load_from_cache(f).unwrap();
assert_eq!(book.chapters.len(), book2.chapters.len());
assert_eq!(book.chapters[0]._data, book2.chapters[0]._data);
assert_eq!(book.assets[0]._data, book2.assets[0]._data);
}
}