1#[cfg(feature = "no-indexmap")]
38use std::collections::HashMap;
39use std::{
40 cmp::Reverse,
41 env,
42 fs::{self, File},
43 io::{BufReader, Cursor, Read, Seek, Write},
44 marker::PhantomData,
45 path::{Path, PathBuf},
46};
47
48use chrono::{SecondsFormat, Utc};
49#[cfg(not(feature = "no-indexmap"))]
50use indexmap::IndexMap;
51use infer::Infer;
52use log::warn;
53use quick_xml::{
54 Writer,
55 events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event},
56};
57use walkdir::WalkDir;
58use zip::{CompressionMethod, ZipWriter, write::FileOptions};
59
60#[cfg(feature = "content-builder")]
61use crate::builder::content::ContentBuilder;
62use crate::{
63 epub::EpubDoc,
64 error::{EpubBuilderError, EpubError},
65 types::{ManifestItem, MetadataItem, NavPoint, SpineItem},
66 utils::{
67 ELEMENT_IN_DC_NAMESPACE, check_realtive_link_leakage, local_time, remove_leading_slash,
68 },
69};
70
71#[cfg(feature = "content-builder")]
72pub mod content;
73
74type XmlWriter = Writer<Cursor<Vec<u8>>>;
75
76#[cfg_attr(test, derive(Debug))]
78pub struct EpubVersion3;
79
80#[cfg_attr(test, derive(Debug))]
125pub struct EpubBuilder<Version> {
126 epub_version: PhantomData<Version>,
128
129 temp_dir: PathBuf,
131
132 rootfiles: Vec<String>,
134
135 metadata: Vec<MetadataItem>,
137
138 #[cfg(feature = "no-indexmap")]
140 manifest: HashMap<String, ManifestItem>,
141 #[cfg(not(feature = "no-indexmap"))]
142 manifest: IndexMap<String, ManifestItem>,
143
144 spine: Vec<SpineItem>,
146
147 catalog_title: String,
148
149 catalog: Vec<NavPoint>,
151
152 #[cfg(feature = "content-builder")]
154 content: Vec<(PathBuf, ContentBuilder)>,
155}
156
157impl EpubBuilder<EpubVersion3> {
158 pub fn new() -> Result<Self, EpubError> {
164 let temp_dir = env::temp_dir().join(local_time());
165 fs::create_dir(&temp_dir)?;
166 fs::create_dir(temp_dir.join("META-INF"))?;
167
168 let mime_file = temp_dir.join("mimetype");
169 fs::write(mime_file, "application/epub+zip")?;
170
171 Ok(EpubBuilder {
172 epub_version: PhantomData,
173 temp_dir,
174
175 rootfiles: vec![],
176 metadata: vec![],
177 #[cfg(feature = "no-indexmap")]
178 manifest: HashMap::new(),
179 #[cfg(not(feature = "no-indexmap"))]
180 manifest: IndexMap::new(),
181 spine: vec![],
182
183 catalog_title: String::new(),
184 catalog: vec![],
185
186 #[cfg(feature = "content-builder")]
187 content: vec![],
188 })
189 }
190
191 pub fn add_rootfile(&mut self, rootfile: &str) -> Result<&mut Self, EpubError> {
203 let rootfile = if rootfile.starts_with("/") || rootfile.starts_with("../") {
204 return Err(EpubBuilderError::IllegalRootfilePath.into());
205 } else if let Some(rootfile) = rootfile.strip_prefix("./") {
206 rootfile
207 } else {
208 rootfile
209 };
210
211 self.rootfiles.push(rootfile.to_string());
212
213 Ok(self)
214 }
215
216 pub fn remove_last_rootfile(&mut self) -> &mut Self {
218 self.rootfiles.pop();
219 self
220 }
221
222 pub fn take_last_rootfile(&mut self) -> Option<String> {
228 self.rootfiles.pop()
229 }
230
231 pub fn clear_rootfiles(&mut self) -> &mut Self {
233 self.rootfiles.clear();
234 self
235 }
236
237 pub fn add_metadata(&mut self, item: MetadataItem) -> &mut Self {
245 self.metadata.push(item);
246 self
247 }
248
249 pub fn remove_last_metadata(&mut self) -> &mut Self {
251 self.metadata.pop();
252 self
253 }
254
255 pub fn take_last_metadata(&mut self) -> Option<MetadataItem> {
261 self.metadata.pop()
262 }
263
264 pub fn clear_metadatas(&mut self) -> &mut Self {
266 self.metadata.clear();
267 self
268 }
269
270 pub fn add_manifest(
287 &mut self,
288 manifest_source: &str,
289 manifest_item: ManifestItem,
290 ) -> Result<&mut Self, EpubError> {
291 if self.rootfiles.is_empty() {
292 return Err(EpubBuilderError::MissingRootfile.into());
293 }
294
295 let source = PathBuf::from(manifest_source);
297 if !source.is_file() {
298 return Err(EpubBuilderError::TargetIsNotFile {
299 target_path: manifest_source.to_string(),
300 }
301 .into());
302 }
303
304 let extension = match source.extension() {
306 Some(ext) => ext.to_string_lossy().to_lowercase(),
307 None => String::new(),
308 };
309
310 let buf = fs::read(source)?;
312
313 let real_mime = match Infer::new().get(&buf) {
315 Some(infer_mime) => refine_mime_type(infer_mime.mime_type(), &extension),
316 None => {
317 return Err(EpubBuilderError::UnknownFileFormat {
318 file_path: manifest_source.to_string(),
319 }
320 .into());
321 }
322 };
323
324 let target_path = self.normalize_manifest_path(&manifest_item.path, &manifest_item.id)?;
325 if let Some(parent_dir) = target_path.parent() {
326 if !parent_dir.exists() {
327 fs::create_dir_all(parent_dir)?
328 }
329 }
330
331 match fs::write(target_path, buf) {
332 Ok(_) => {
333 self.manifest
334 .insert(manifest_item.id.clone(), manifest_item.set_mime(real_mime));
335 Ok(self)
336 }
337 Err(err) => Err(err.into()),
338 }
339 }
340
341 pub fn remove_manifest(&mut self, id: &str) -> Result<&mut Self, EpubError> {
353 #[cfg(feature = "no-indexmap")]
354 let manifest = self.manifest.remove(id);
355 #[cfg(not(feature = "no-indexmap"))]
356 let manifest = self.manifest.shift_remove(id);
357
358 if let Some(manifest) = manifest {
359 let target_path = self.normalize_manifest_path(&manifest.path, &manifest.id)?;
360 fs::remove_file(target_path)?;
361 }
362
363 Ok(self)
364 }
365
366 pub fn take_manifest(&mut self, id: &str) -> Option<ManifestItem> {
375 #[cfg(feature = "no-indexmap")]
376 let manifest = self.manifest.remove(id);
377 #[cfg(not(feature = "no-indexmap"))]
378 let manifest = self.manifest.shift_remove(id);
379
380 if let Some(manifest) = manifest {
381 let target_path = self
382 .normalize_manifest_path(&manifest.path, &manifest.id)
383 .ok()?;
384 fs::remove_file(target_path).ok()?;
385
386 return Some(manifest);
387 }
388
389 None
390 }
391
392 pub fn clear_manifests(&mut self) -> Result<&mut Self, EpubError> {
398 let paths = self
399 .manifest
400 .values()
401 .map(|manifest| &manifest.path)
402 .collect::<Vec<&PathBuf>>();
403
404 for path in paths {
405 let _ = fs::remove_file(path);
406 }
407
408 self.manifest.clear();
409
410 Ok(self)
411 }
412
413 pub fn add_spine(&mut self, item: SpineItem) -> &mut Self {
420 self.spine.push(item);
421 self
422 }
423
424 pub fn remove_last_spine(&mut self) -> &mut Self {
426 self.spine.pop();
427 self
428 }
429
430 pub fn take_last_spine(&mut self) -> Option<SpineItem> {
436 self.spine.pop()
437 }
438
439 pub fn clear_spines(&mut self) -> &mut Self {
441 self.spine.clear();
442 self
443 }
444
445 pub fn set_catalog_title(&mut self, title: &str) -> &mut Self {
450 self.catalog_title = title.to_string();
451 self
452 }
453
454 pub fn add_catalog_item(&mut self, item: NavPoint) -> &mut Self {
461 self.catalog.push(item);
462 self
463 }
464
465 pub fn remove_last_catalog_item(&mut self) -> &mut Self {
467 self.catalog.pop();
468 self
469 }
470
471 pub fn take_last_catalog_item(&mut self) -> Option<NavPoint> {
477 self.catalog.pop()
478 }
479
480 pub fn set_catalog(&mut self, catalog: Vec<NavPoint>) -> &mut Self {
487 self.catalog = catalog;
488 self
489 }
490
491 pub fn clear_catalog(&mut self) -> &mut Self {
493 self.catalog.clear();
494 self
495 }
496
497 #[cfg(feature = "content-builder")]
506 pub fn add_content(&mut self, target_path: &str, content: ContentBuilder) -> &mut Self {
507 self.content.push((PathBuf::from(target_path), content));
508 self
509 }
510
511 #[cfg(feature = "content-builder")]
513 pub fn remove_last_content(&mut self) -> &mut Self {
514 self.content.pop();
515 self
516 }
517
518 #[cfg(feature = "content-builder")]
524 pub fn take_last_content(&mut self) -> Option<(PathBuf, ContentBuilder)> {
525 self.content.pop()
526 }
527
528 #[cfg(feature = "content-builder")]
530 pub fn clear_contents(&mut self) -> &mut Self {
531 self.content.clear();
532 self
533 }
534
535 pub fn clear_all(&mut self) -> Result<&mut Self, EpubError> {
544 self.catalog_title = String::new();
545 self.clear_metadatas()
546 .clear_manifests()?
547 .clear_spines()
548 .clear_catalog();
549
550 #[cfg(feature = "content-builder")]
551 self.clear_contents();
552
553 Ok(self)
554 }
555
556 pub fn make<P: AsRef<Path>>(mut self, output_path: P) -> Result<(), EpubError> {
565 self.make_container_xml()?;
569 self.make_navigation_document()?;
570 #[cfg(feature = "content-builder")]
571 self.make_contents()?;
572 self.make_opf_file()?;
573 self.remove_empty_dirs()?;
574
575 if let Some(parent) = output_path.as_ref().parent() {
576 if !parent.exists() {
577 fs::create_dir_all(parent)?;
578 }
579 }
580
581 let file = File::create(output_path)?;
583 let mut zip = ZipWriter::new(file);
584 let options = FileOptions::<()>::default().compression_method(CompressionMethod::Stored);
585
586 for entry in WalkDir::new(&self.temp_dir) {
587 let entry = entry?;
588 let path = entry.path();
589
590 let relative_path = path.strip_prefix(&self.temp_dir).unwrap();
593 let target_path = relative_path.to_string_lossy().replace("\\", "/");
594
595 if path.is_file() {
596 zip.start_file(target_path, options)?;
597
598 let mut buf = Vec::new();
599 File::open(path)?.read_to_end(&mut buf)?;
600
601 zip.write_all(&buf)?;
602 } else if path.is_dir() {
603 zip.add_directory(target_path, options)?;
604 }
605 }
606
607 zip.finish()?;
608 Ok(())
609 }
610
611 pub fn build<P: AsRef<Path>>(
622 self,
623 output_path: P,
624 ) -> Result<EpubDoc<BufReader<File>>, EpubError> {
625 self.make(&output_path)?;
626
627 EpubDoc::new(output_path)
628 }
629
630 pub fn from<R: Read + Seek>(doc: &mut EpubDoc<R>) -> Result<Self, EpubError> {
657 let mut builder = Self::new()?;
658
659 builder.add_rootfile(&doc.package_path.clone().to_string_lossy())?;
660 builder.metadata = doc.metadata.clone();
661 builder.spine = doc.spine.clone();
662 builder.catalog = doc.catalog.clone();
663 builder.catalog_title = doc.catalog_title.clone();
664
665 for (_, mut manifest) in doc.manifest.clone().into_iter() {
667 if let Some(properties) = &manifest.properties {
668 if properties.contains("nav") {
669 continue;
670 }
671 }
672
673 manifest.path = PathBuf::from("/").join(manifest.path);
677
678 let (buf, _) = doc.get_manifest_item(&manifest.id)?; let target_path = builder.normalize_manifest_path(&manifest.path, &manifest.id)?;
680 if let Some(parent_dir) = target_path.parent() {
681 if !parent_dir.exists() {
682 fs::create_dir_all(parent_dir)?
683 }
684 }
685
686 fs::write(target_path, buf)?;
687 builder.manifest.insert(manifest.id.clone(), manifest);
688 }
689
690 Ok(builder)
691 }
692
693 fn make_container_xml(&self) -> Result<(), EpubError> {
697 if self.rootfiles.is_empty() {
698 return Err(EpubBuilderError::MissingRootfile.into());
699 }
700
701 let mut writer = Writer::new(Cursor::new(Vec::new()));
702
703 writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
704
705 writer.write_event(Event::Start(BytesStart::new("container").with_attributes(
706 [
707 ("version", "1.0"),
708 ("xmlns", "urn:oasis:names:tc:opendocument:xmlns:container"),
709 ],
710 )))?;
711 writer.write_event(Event::Start(BytesStart::new("rootfiles")))?;
712
713 for rootfile in &self.rootfiles {
714 writer.write_event(Event::Empty(BytesStart::new("rootfile").with_attributes([
715 ("full-path", rootfile.as_str()),
716 ("media-type", "application/oebps-package+xml"),
717 ])))?;
718 }
719
720 writer.write_event(Event::End(BytesEnd::new("rootfiles")))?;
721 writer.write_event(Event::End(BytesEnd::new("container")))?;
722
723 let file_path = self.temp_dir.join("META-INF").join("container.xml");
724 let file_data = writer.into_inner().into_inner();
725 fs::write(file_path, file_data)?;
726
727 Ok(())
728 }
729
730 #[cfg(feature = "content-builder")]
732 fn make_contents(&mut self) -> Result<(), EpubError> {
733 let mut buf = vec![0; 512];
734 let contents = std::mem::take(&mut self.content);
735
736 for (target, mut content) in contents.into_iter() {
737 let manifest_id = content.id.clone();
738
739 let absolute_target = self.normalize_manifest_path(&target, &manifest_id)?;
741 let mut resources = content.make(&absolute_target)?;
742
743 let to_container_path = |p: &PathBuf| -> PathBuf {
745 match p.strip_prefix(&self.temp_dir) {
746 Ok(rel) => PathBuf::from("/").join(rel.to_string_lossy().replace("\\", "/")),
747 Err(_) => unreachable!("path MUST under temp directory"),
748 }
749 };
750
751 let path = resources.swap_remove(0);
753 let mut file = std::fs::File::open(&path)?;
754 let _ = file.read(&mut buf)?;
755 let extension = path
756 .extension()
757 .map(|e| e.to_string_lossy().to_lowercase())
758 .unwrap_or_default();
759 let mime = match Infer::new().get(&buf) {
760 Some(infer) => refine_mime_type(infer.mime_type(), &extension),
761 None => {
762 return Err(EpubBuilderError::UnknownFileFormat {
763 file_path: path.to_string_lossy().to_string(),
764 }
765 .into());
766 }
767 }
768 .to_string();
769
770 self.manifest.insert(
771 manifest_id.clone(),
772 ManifestItem {
773 id: manifest_id.clone(),
774 path: to_container_path(&path),
775 mime,
776 properties: None,
777 fallback: None,
778 },
779 );
780
781 for res in resources {
783 let mut file = fs::File::open(&res)?;
784 let _ = file.read(&mut buf)?;
785 let extension = res
786 .extension()
787 .map(|e| e.to_string_lossy().to_lowercase())
788 .unwrap_or_default();
789 let mime = match Infer::new().get(&buf) {
790 Some(ft) => refine_mime_type(ft.mime_type(), &extension),
791 None => {
792 return Err(EpubBuilderError::UnknownFileFormat {
793 file_path: path.to_string_lossy().to_string(),
794 }
795 .into());
796 }
797 }
798 .to_string();
799
800 let file_name = res
801 .file_name()
802 .map(|s| s.to_string_lossy().to_string())
803 .unwrap_or_default();
804 let res_id = format!("{}-{}", manifest_id, file_name);
805
806 self.manifest.insert(
807 res_id.clone(),
808 ManifestItem {
809 id: res_id,
810 path: to_container_path(&res),
811 mime,
812 properties: None,
813 fallback: None,
814 },
815 );
816 }
817 }
818
819 Ok(())
820 }
821
822 fn make_navigation_document(&mut self) -> Result<(), EpubError> {
826 if self.catalog.is_empty() {
827 return Err(EpubBuilderError::NavigationInfoUninitalized.into());
828 }
829
830 let mut writer = Writer::new(Cursor::new(Vec::new()));
831
832 writer.write_event(Event::Start(BytesStart::new("html").with_attributes([
833 ("xmlns", "http://www.w3.org/1999/xhtml"),
834 ("xmlns:epub", "http://www.idpf.org/2007/ops"),
835 ])))?;
836
837 writer.write_event(Event::Start(BytesStart::new("head")))?;
839 writer.write_event(Event::Start(BytesStart::new("title")))?;
840 writer.write_event(Event::Text(BytesText::new(&self.catalog_title)))?;
841 writer.write_event(Event::End(BytesEnd::new("title")))?;
842 writer.write_event(Event::End(BytesEnd::new("head")))?;
843
844 writer.write_event(Event::Start(BytesStart::new("body")))?;
846 writer.write_event(Event::Start(
847 BytesStart::new("nav").with_attributes([("epub:type", "toc")]),
848 ))?;
849
850 if !self.catalog_title.is_empty() {
851 writer.write_event(Event::Start(BytesStart::new("h1")))?;
852 writer.write_event(Event::Text(BytesText::new(&self.catalog_title)))?;
853 writer.write_event(Event::End(BytesEnd::new("h1")))?;
854 }
855
856 Self::make_nav(&mut writer, &self.catalog)?;
857
858 writer.write_event(Event::End(BytesEnd::new("nav")))?;
859 writer.write_event(Event::End(BytesEnd::new("body")))?;
860
861 writer.write_event(Event::End(BytesEnd::new("html")))?;
862
863 let file_path = self.temp_dir.join("nav.xhtml");
864 let file_data = writer.into_inner().into_inner();
865 fs::write(file_path, file_data)?;
866
867 self.manifest.insert(
868 "nav".to_string(),
869 ManifestItem {
870 id: "nav".to_string(),
871 path: PathBuf::from("/nav.xhtml"),
872 mime: "application/xhtml+xml".to_string(),
873 properties: Some("nav".to_string()),
874 fallback: None,
875 },
876 );
877
878 Ok(())
879 }
880
881 fn make_opf_file(&mut self) -> Result<(), EpubError> {
888 if !self.validate_metadata() {
889 return Err(EpubBuilderError::MissingNecessaryMetadata.into());
890 }
891 self.validate_manifest_fallback_chains()?;
892 self.validate_manifest_nav()?;
893
894 let mut writer = Writer::new(Cursor::new(Vec::new()));
895
896 writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
897
898 writer.write_event(Event::Start(BytesStart::new("package").with_attributes([
899 ("xmlns", "http://www.idpf.org/2007/opf"),
900 ("xmlns:dc", "http://purl.org/dc/elements/1.1/"),
901 ("unique-identifier", "pub-id"),
902 ("version", "3.0"),
903 ])))?;
904
905 self.make_opf_metadata(&mut writer)?;
906 self.make_opf_manifest(&mut writer)?;
907 self.make_opf_spine(&mut writer)?;
908
909 writer.write_event(Event::End(BytesEnd::new("package")))?;
910
911 let file_path = self.temp_dir.join(&self.rootfiles[0]);
912 let file_data = writer.into_inner().into_inner();
913 fs::write(file_path, file_data)?;
914
915 Ok(())
916 }
917
918 fn make_opf_metadata(&mut self, writer: &mut XmlWriter) -> Result<(), EpubError> {
919 self.metadata.push(MetadataItem {
920 id: None,
921 property: "dcterms:modified".to_string(),
922 value: Utc::now().to_rfc3339_opts(SecondsFormat::AutoSi, true),
923 lang: None,
924 refined: vec![],
925 });
926
927 writer.write_event(Event::Start(BytesStart::new("metadata")))?;
928
929 for metadata in &self.metadata {
930 let tag_name = if ELEMENT_IN_DC_NAMESPACE.contains(&metadata.property.as_str()) {
931 format!("dc:{}", metadata.property)
932 } else {
933 "meta".to_string()
934 };
935
936 writer.write_event(Event::Start(
937 BytesStart::new(tag_name.as_str()).with_attributes(metadata.attributes()),
938 ))?;
939 writer.write_event(Event::Text(BytesText::new(metadata.value.as_str())))?;
940 writer.write_event(Event::End(BytesEnd::new(tag_name.as_str())))?;
941
942 for refinement in &metadata.refined {
943 writer.write_event(Event::Start(
944 BytesStart::new("meta").with_attributes(refinement.attributes()),
945 ))?;
946 writer.write_event(Event::Text(BytesText::new(refinement.value.as_str())))?;
947 writer.write_event(Event::End(BytesEnd::new("meta")))?;
948 }
949 }
950
951 writer.write_event(Event::End(BytesEnd::new("metadata")))?;
952
953 Ok(())
954 }
955
956 fn make_opf_manifest(&self, writer: &mut XmlWriter) -> Result<(), EpubError> {
957 writer.write_event(Event::Start(BytesStart::new("manifest")))?;
958
959 for manifest in self.manifest.values() {
960 writer.write_event(Event::Empty(
961 BytesStart::new("item").with_attributes(manifest.attributes()),
962 ))?;
963 }
964
965 writer.write_event(Event::End(BytesEnd::new("manifest")))?;
966
967 Ok(())
968 }
969
970 fn make_opf_spine(&self, writer: &mut XmlWriter) -> Result<(), EpubError> {
971 writer.write_event(Event::Start(BytesStart::new("spine")))?;
972
973 for spine in &self.spine {
974 writer.write_event(Event::Empty(
975 BytesStart::new("itemref").with_attributes(spine.attributes()),
976 ))?;
977 }
978
979 writer.write_event(Event::End(BytesEnd::new("spine")))?;
980
981 Ok(())
982 }
983
984 fn make_nav(writer: &mut XmlWriter, navgations: &Vec<NavPoint>) -> Result<(), EpubError> {
985 writer.write_event(Event::Start(BytesStart::new("ol")))?;
986
987 for nav in navgations {
988 writer.write_event(Event::Start(BytesStart::new("li")))?;
989
990 if let Some(path) = &nav.content {
991 writer.write_event(Event::Start(
992 BytesStart::new("a").with_attributes([("href", path.to_string_lossy())]),
993 ))?;
994 writer.write_event(Event::Text(BytesText::new(nav.label.as_str())))?;
995 writer.write_event(Event::End(BytesEnd::new("a")))?;
996 } else {
997 writer.write_event(Event::Start(BytesStart::new("span")))?;
998 writer.write_event(Event::Text(BytesText::new(nav.label.as_str())))?;
999 writer.write_event(Event::End(BytesEnd::new("span")))?;
1000 }
1001
1002 if !nav.children.is_empty() {
1003 Self::make_nav(writer, &nav.children)?;
1004 }
1005
1006 writer.write_event(Event::End(BytesEnd::new("li")))?;
1007 }
1008
1009 writer.write_event(Event::End(BytesEnd::new("ol")))?;
1010
1011 Ok(())
1012 }
1013
1014 fn validate_metadata(&self) -> bool {
1018 let has_title = self.metadata.iter().any(|item| item.property == "title");
1019 let has_language = self.metadata.iter().any(|item| item.property == "language");
1020 let has_identifier = self.metadata.iter().any(|item| {
1021 item.property == "identifier" && item.id.as_ref().is_some_and(|id| id == "pub-id")
1022 });
1023
1024 has_title && has_identifier && has_language
1025 }
1026
1027 fn validate_manifest_fallback_chains(&self) -> Result<(), EpubError> {
1029 for (id, item) in &self.manifest {
1030 if item.fallback.is_none() {
1031 continue;
1032 }
1033
1034 let mut fallback_chain = Vec::new();
1035 self.validate_fallback_chain(id, &mut fallback_chain)?;
1036 }
1037
1038 Ok(())
1039 }
1040
1041 fn validate_fallback_chain(
1047 &self,
1048 manifest_id: &str,
1049 fallback_chain: &mut Vec<String>,
1050 ) -> Result<(), EpubError> {
1051 if fallback_chain.contains(&manifest_id.to_string()) {
1052 fallback_chain.push(manifest_id.to_string());
1053
1054 return Err(EpubBuilderError::ManifestCircularReference {
1055 fallback_chain: fallback_chain.join("->"),
1056 }
1057 .into());
1058 }
1059
1060 let item = self.manifest.get(manifest_id).unwrap();
1062
1063 if let Some(fallback_id) = &item.fallback {
1064 if !self.manifest.contains_key(fallback_id) {
1065 return Err(EpubBuilderError::ManifestNotFound {
1066 manifest_id: fallback_id.to_owned(),
1067 }
1068 .into());
1069 }
1070
1071 fallback_chain.push(manifest_id.to_string());
1072 self.validate_fallback_chain(fallback_id, fallback_chain)
1073 } else {
1074 Ok(())
1076 }
1077 }
1078
1079 fn validate_manifest_nav(&self) -> Result<(), EpubError> {
1083 if self
1084 .manifest
1085 .values()
1086 .filter(|&item| {
1087 if let Some(properties) = &item.properties {
1088 properties.split(" ").any(|property| property == "nav")
1089 } else {
1090 false
1091 }
1092 })
1093 .count()
1094 == 1
1095 {
1096 Ok(())
1097 } else {
1098 Err(EpubBuilderError::TooManyNavFlags.into())
1099 }
1100 }
1101
1102 fn normalize_manifest_path<P: AsRef<Path>>(
1122 &self,
1123 path: P,
1124 id: &str,
1125 ) -> Result<PathBuf, EpubError> {
1126 let opf_path = PathBuf::from(&self.rootfiles[0]);
1127 let basic_path = remove_leading_slash(opf_path.parent().unwrap());
1128
1129 let mut target_path = if path.as_ref().starts_with("../") {
1131 check_realtive_link_leakage(
1132 self.temp_dir.clone(),
1133 basic_path.to_path_buf(),
1134 &path.as_ref().to_string_lossy(),
1135 )
1136 .map(PathBuf::from)
1137 .ok_or_else(|| EpubError::RelativeLinkLeakage {
1138 path: path.as_ref().to_string_lossy().to_string(),
1139 })?
1140 } else if let Ok(path) = path.as_ref().strip_prefix("/") {
1141 self.temp_dir.join(path)
1142 } else if path.as_ref().starts_with("./") {
1143 Err(EpubBuilderError::IllegalManifestPath { manifest_id: id.to_string() })?
1145 } else {
1146 self.temp_dir.join(basic_path).join(path)
1147 };
1148
1149 #[cfg(windows)]
1150 {
1151 target_path = PathBuf::from(target_path.to_string_lossy().replace('\\', "/"));
1152 }
1153
1154 Ok(target_path)
1155 }
1156
1157 fn remove_empty_dirs(&self) -> Result<(), EpubError> {
1168 let mut dirs = WalkDir::new(self.temp_dir.as_path())
1169 .min_depth(1)
1170 .into_iter()
1171 .filter_map(|entry| entry.ok())
1172 .filter(|entry| entry.file_type().is_dir())
1173 .map(|entry| entry.into_path())
1174 .collect::<Vec<PathBuf>>();
1175
1176 dirs.sort_by_key(|p| Reverse(p.components().count()));
1177
1178 for dir in dirs {
1179 if fs::read_dir(&dir)?.next().is_none() {
1180 fs::remove_dir(dir)?;
1181 }
1182 }
1183
1184 Ok(())
1185 }
1186}
1187
1188impl<Version> Drop for EpubBuilder<Version> {
1189 fn drop(&mut self) {
1191 if let Err(err) = fs::remove_dir_all(&self.temp_dir) {
1192 warn!("{}", err);
1193 };
1194 }
1195}
1196
1197fn refine_mime_type<'a>(infer_mime: &'a str, extension: &'a str) -> &'a str {
1201 match (infer_mime, extension) {
1202 ("text/xml", "xhtml")
1203 | ("application/xml", "xhtml")
1204 | ("text/xml", "xht")
1205 | ("application/xml", "xht") => "application/xhtml+xml",
1206
1207 ("text/xml", "opf") | ("application/xml", "opf") => "application/oebps-package+xml",
1208
1209 ("text/xml", "ncx") | ("application/xml", "ncx") => "application/x-dtbncx+xml",
1210
1211 ("application/zip", "epub") => "application/epub+zip",
1212
1213 ("text/plain", "css") => "text/css",
1214 ("text/plain", "js") => "application/javascript",
1215 ("text/plain", "json") => "application/json",
1216 ("text/plain", "svg") => "image/svg+xml",
1217
1218 _ => infer_mime,
1219 }
1220}
1221
1222#[cfg(test)]
1223mod tests {
1224 use std::{env, fs, path::PathBuf};
1225
1226 use crate::{
1227 builder::{EpubBuilder, EpubVersion3, refine_mime_type},
1228 epub::EpubDoc,
1229 error::{EpubBuilderError, EpubError},
1230 types::{ManifestItem, MetadataItem, NavPoint, SpineItem},
1231 utils::local_time,
1232 };
1233
1234 #[test]
1235 fn test_epub_builder_new() {
1236 let builder = EpubBuilder::<EpubVersion3>::new();
1237 assert!(builder.is_ok());
1238
1239 let builder = builder.unwrap();
1240 assert!(builder.temp_dir.exists());
1241 assert!(builder.rootfiles.is_empty());
1242 assert!(builder.metadata.is_empty());
1243 assert!(builder.manifest.is_empty());
1244 assert!(builder.spine.is_empty());
1245 assert!(builder.catalog_title.is_empty());
1246 assert!(builder.catalog.is_empty());
1247 }
1248
1249 #[test]
1250 fn test_add_rootfile() {
1251 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1252 assert!(builder.add_rootfile("content.opf").is_ok());
1253
1254 assert_eq!(builder.rootfiles.len(), 1);
1255 assert_eq!(builder.rootfiles[0], "content.opf");
1256
1257 assert!(builder.add_rootfile("./another.opf").is_ok());
1258 assert_eq!(builder.rootfiles.len(), 2);
1259 assert_eq!(builder.rootfiles, vec!["content.opf", "another.opf"]);
1260 }
1261
1262 #[test]
1263 fn test_add_rootfile_fail() {
1264 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1265
1266 let result = builder.add_rootfile("/rootfile.opf");
1267 assert!(result.is_err());
1268 assert_eq!(
1269 result.unwrap_err(),
1270 EpubBuilderError::IllegalRootfilePath.into()
1271 );
1272
1273 let result = builder.add_rootfile("../rootfile.opf");
1274 assert!(result.is_err());
1275 assert_eq!(
1276 result.unwrap_err(),
1277 EpubBuilderError::IllegalRootfilePath.into()
1278 );
1279 }
1280
1281 #[test]
1282 fn test_remove_last_rootfile() {
1283 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1284
1285 assert!(builder.add_rootfile("first.opf").is_ok());
1286 assert!(builder.add_rootfile("second.opf").is_ok());
1287 assert!(builder.add_rootfile("third.opf").is_ok());
1288 assert_eq!(builder.rootfiles.len(), 3);
1289
1290 let result = builder.remove_last_rootfile();
1291 assert_eq!(result.rootfiles.len(), 2);
1292 assert_eq!(builder.rootfiles, vec!["first.opf", "second.opf"]);
1293
1294 builder.remove_last_rootfile();
1295 assert_eq!(builder.rootfiles.len(), 1);
1296 assert_eq!(builder.rootfiles[0], "first.opf");
1297
1298 builder.remove_last_rootfile();
1299 assert!(builder.rootfiles.is_empty());
1300
1301 builder.remove_last_rootfile();
1302 assert!(builder.rootfiles.is_empty());
1303 }
1304
1305 #[test]
1306 fn test_take_last_rootfile() {
1307 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1308
1309 let result = builder.take_last_rootfile();
1310 assert!(result.is_none());
1311
1312 builder.add_rootfile("first.opf").unwrap();
1313 builder.add_rootfile("second.opf").unwrap();
1314 builder.add_rootfile("third.opf").unwrap();
1315 assert_eq!(builder.rootfiles.len(), 3);
1316
1317 let result = builder.take_last_rootfile();
1318 assert!(result.is_some());
1319 assert_eq!(result.unwrap(), "third.opf");
1320 assert_eq!(builder.rootfiles.len(), 2);
1321
1322 let result = builder.take_last_rootfile();
1323 assert_eq!(result.unwrap(), "second.opf");
1324 assert_eq!(builder.rootfiles.len(), 1);
1325
1326 let result = builder.take_last_rootfile();
1327 assert_eq!(result.unwrap(), "first.opf");
1328 assert!(builder.rootfiles.is_empty());
1329
1330 let result = builder.take_last_rootfile();
1331 assert!(result.is_none());
1332 }
1333
1334 #[test]
1335 fn test_clear_rootfiles() {
1336 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1337
1338 builder.clear_rootfiles();
1339 assert!(builder.rootfiles.is_empty());
1340
1341 builder.add_rootfile("first.opf").unwrap();
1342 builder.add_rootfile("second.opf").unwrap();
1343 builder.add_rootfile("third.opf").unwrap();
1344 assert_eq!(builder.rootfiles.len(), 3);
1345
1346 builder.clear_rootfiles();
1347 assert!(builder.rootfiles.is_empty());
1348 assert_eq!(builder.rootfiles.len(), 0);
1349
1350 builder.add_rootfile("new.opf").unwrap();
1351 assert_eq!(builder.rootfiles.len(), 1);
1352 assert_eq!(builder.rootfiles[0], "new.opf");
1353 }
1354
1355 #[test]
1356 fn test_add_metadata() {
1357 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1358 let metadata_item = MetadataItem::new("title", "Test Book");
1359
1360 builder.add_metadata(metadata_item);
1361
1362 assert_eq!(builder.metadata.len(), 1);
1363 assert_eq!(builder.metadata[0].property, "title");
1364 assert_eq!(builder.metadata[0].value, "Test Book");
1365 }
1366
1367 #[test]
1368 fn test_remove_last_metadata() {
1369 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1370 builder.add_metadata(MetadataItem::new("title", "Test Book"));
1371 builder.add_metadata(MetadataItem::new("author", "Test Author"));
1372
1373 assert_eq!(builder.metadata.len(), 2);
1374
1375 builder.remove_last_metadata();
1376
1377 assert_eq!(builder.metadata.len(), 1);
1378 assert_eq!(builder.metadata[0].property, "title");
1379
1380 builder.remove_last_metadata();
1381 builder.remove_last_metadata();
1382 assert_eq!(builder.metadata.len(), 0);
1383 }
1384
1385 #[test]
1386 fn test_take_last_metadata() {
1387 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1388 let metadata1 = MetadataItem::new("title", "Test Book");
1389 let metadata2 = MetadataItem::new("author", "Test Author");
1390
1391 builder.add_metadata(metadata1);
1392 builder.add_metadata(metadata2);
1393 assert_eq!(builder.metadata.len(), 2);
1394
1395 let taken = builder.take_last_metadata();
1396 assert!(taken.is_some());
1397 assert_eq!(taken.unwrap().property, "author");
1398 assert_eq!(builder.metadata.len(), 1);
1399
1400 let _ = builder.take_last_metadata();
1401 let result = builder.take_last_metadata();
1402 assert!(result.is_none());
1403 assert_eq!(builder.metadata.len(), 0);
1404 }
1405
1406 #[test]
1407 fn test_clear_metadatas() {
1408 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1409 builder.add_metadata(MetadataItem::new("title", "Test Book"));
1410 builder.add_metadata(MetadataItem::new("author", "Test Author"));
1411 builder.add_metadata(MetadataItem::new("language", "en"));
1412
1413 assert_eq!(builder.metadata.len(), 3);
1414
1415 builder.clear_metadatas();
1416
1417 assert_eq!(builder.metadata.len(), 0);
1418
1419 builder.clear_metadatas();
1420 assert_eq!(builder.metadata.len(), 0);
1421 }
1422
1423 #[test]
1424 fn test_add_manifest_success() {
1425 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1426 assert!(builder.add_rootfile("content.opf").is_ok());
1427
1428 let temp_dir = env::temp_dir().join(local_time());
1430 fs::create_dir_all(&temp_dir).unwrap();
1431 let test_file = temp_dir.join("test.xhtml");
1432 fs::write(&test_file, "<html><body>Hello World</body></html>").unwrap();
1433
1434 let manifest_item = ManifestItem::new("test", "/epub/test.xhtml").unwrap();
1435 let result = builder.add_manifest(test_file.to_str().unwrap(), manifest_item);
1436
1437 assert!(result.is_ok());
1438 assert_eq!(builder.manifest.len(), 1);
1439 assert!(builder.manifest.contains_key("test"));
1440
1441 fs::remove_dir_all(temp_dir).unwrap();
1442 }
1443
1444 #[test]
1445 fn test_add_manifest_no_rootfile() {
1446 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1447
1448 let manifest_item = ManifestItem {
1449 id: "main".to_string(),
1450 path: PathBuf::from("/Overview.xhtml"),
1451 mime: String::new(),
1452 properties: None,
1453 fallback: None,
1454 };
1455
1456 let result = builder.add_manifest("./test_case/Overview.xhtml", manifest_item.clone());
1457 assert!(result.is_err());
1458 assert_eq!(
1459 result.unwrap_err(),
1460 EpubBuilderError::MissingRootfile.into()
1461 );
1462
1463 let result = builder.add_rootfile("package.opf");
1464 assert!(result.is_ok());
1465
1466 let result = builder.add_manifest("./test_case/Overview.xhtml", manifest_item);
1467 assert!(result.is_ok());
1468 }
1469
1470 #[test]
1471 fn test_add_manifest_nonexistent_file() {
1472 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1473 assert!(builder.add_rootfile("content.opf").is_ok());
1474
1475 let manifest_item = ManifestItem::new("test", "nonexistent.xhtml").unwrap();
1476 let result = builder.add_manifest("nonexistent.xhtml", manifest_item);
1477
1478 assert!(result.is_err());
1479 assert_eq!(
1480 result.unwrap_err(),
1481 EpubBuilderError::TargetIsNotFile {
1482 target_path: "nonexistent.xhtml".to_string()
1483 }
1484 .into()
1485 );
1486 }
1487
1488 #[test]
1489 fn test_add_manifest_unknow_file_format() {
1490 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1491 let result = builder.add_rootfile("package.opf");
1492 assert!(result.is_ok());
1493
1494 let result = builder.add_manifest(
1495 "./test_case/unknown_file_format.xhtml",
1496 ManifestItem {
1497 id: "file".to_string(),
1498 path: PathBuf::from("unknown_file_format.xhtml"),
1499 mime: String::new(),
1500 properties: None,
1501 fallback: None,
1502 },
1503 );
1504
1505 assert!(result.is_err());
1506 assert_eq!(
1507 result.unwrap_err(),
1508 EpubBuilderError::UnknownFileFormat {
1509 file_path: "./test_case/unknown_file_format.xhtml".to_string(),
1510 }
1511 .into()
1512 )
1513 }
1514
1515 #[test]
1516 fn test_remove_manifest() {
1517 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1518 builder.add_rootfile("package.opf").unwrap();
1519
1520 builder
1521 .add_manifest(
1522 "./test_case/Overview.xhtml",
1523 ManifestItem::new("item1", "content1.xhtml").unwrap(),
1524 )
1525 .unwrap();
1526 builder
1527 .add_manifest(
1528 "./test_case/Overview.xhtml",
1529 ManifestItem::new("item2", "content2.xhtml").unwrap(),
1530 )
1531 .unwrap();
1532 builder
1533 .add_manifest(
1534 "./test_case/Overview.xhtml",
1535 ManifestItem::new("item3", "content3.xhtml").unwrap(),
1536 )
1537 .unwrap();
1538
1539 assert_eq!(builder.manifest.len(), 3);
1540
1541 let result = builder.remove_manifest("item2");
1542 assert!(result.is_ok());
1543 assert_eq!(builder.manifest.len(), 2);
1544 assert!(!builder.manifest.contains_key("item2"));
1545 assert!(builder.manifest.contains_key("item1"));
1546 assert!(builder.manifest.contains_key("item3"));
1547
1548 builder.remove_manifest("item1").unwrap();
1549 assert_eq!(builder.manifest.len(), 1);
1550 assert!(builder.manifest.contains_key("item3"));
1551
1552 let result = builder.remove_manifest("nonexistent");
1553 assert!(result.is_ok());
1554 assert_eq!(builder.manifest.len(), 1);
1555 }
1556
1557 #[test]
1558 fn test_take_manifest() {
1559 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1560 builder.add_rootfile("package.opf").unwrap();
1561
1562 builder
1563 .add_manifest(
1564 "./test_case/Overview.xhtml",
1565 ManifestItem::new("item1", "content1.xhtml").unwrap(),
1566 )
1567 .unwrap();
1568 builder
1569 .add_manifest(
1570 "./test_case/Overview.xhtml",
1571 ManifestItem::new("item2", "content2.xhtml").unwrap(),
1572 )
1573 .unwrap();
1574
1575 assert_eq!(builder.manifest.len(), 2);
1576
1577 let taken = builder.take_manifest("item1");
1578 assert!(taken.is_some());
1579 assert_eq!(taken.unwrap().id, "item1");
1580 assert_eq!(builder.manifest.len(), 1);
1581 assert!(!builder.manifest.contains_key("item1"));
1582
1583 let taken = builder.take_manifest("item2");
1584 assert!(taken.is_some());
1585 assert_eq!(taken.unwrap().id, "item2");
1586 assert!(builder.manifest.is_empty());
1587
1588 let taken = builder.take_manifest("item1");
1589 assert!(taken.is_none());
1590
1591 builder
1592 .add_manifest(
1593 "./test_case/Overview.xhtml",
1594 ManifestItem::new("item3", "content3.xhtml").unwrap(),
1595 )
1596 .unwrap();
1597 let taken = builder.take_manifest("nonexistent");
1598 assert!(taken.is_none());
1599 assert_eq!(builder.manifest.len(), 1);
1600 }
1601
1602 #[test]
1603 fn test_clear_manifests() {
1604 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1605 builder.add_rootfile("package.opf").unwrap();
1606
1607 let result = builder.clear_manifests();
1608 assert!(result.is_ok());
1609 assert!(builder.manifest.is_empty());
1610
1611 builder
1612 .add_manifest(
1613 "./test_case/Overview.xhtml",
1614 ManifestItem::new("item1", "content1.xhtml").unwrap(),
1615 )
1616 .unwrap();
1617 builder
1618 .add_manifest(
1619 "./test_case/Overview.xhtml",
1620 ManifestItem::new("item2", "content2.xhtml").unwrap(),
1621 )
1622 .unwrap();
1623 builder
1624 .add_manifest(
1625 "./test_case/Overview.xhtml",
1626 ManifestItem::new("item3", "content3.xhtml").unwrap(),
1627 )
1628 .unwrap();
1629
1630 assert_eq!(builder.manifest.len(), 3);
1631
1632 let result = builder.clear_manifests();
1633 assert!(result.is_ok());
1634 assert!(builder.manifest.is_empty());
1635
1636 builder
1637 .add_manifest(
1638 "./test_case/Overview.xhtml",
1639 ManifestItem::new("new_item", "new_content.xhtml").unwrap(),
1640 )
1641 .unwrap();
1642 assert_eq!(builder.manifest.len(), 1);
1643 assert_eq!(builder.manifest.get("new_item").unwrap().id, "new_item");
1644 }
1645
1646 #[test]
1647 fn test_add_spine() {
1648 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1649 let spine_item = SpineItem::new("test_item");
1650
1651 builder.add_spine(spine_item.clone());
1652
1653 assert_eq!(builder.spine.len(), 1);
1654 assert_eq!(builder.spine[0].idref, "test_item");
1655 }
1656
1657 #[test]
1658 fn test_remove_last_spine() {
1659 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1660
1661 builder.add_spine(SpineItem::new("chapter1"));
1662 builder.add_spine(SpineItem::new("chapter2"));
1663 builder.add_spine(SpineItem::new("chapter3"));
1664 assert_eq!(builder.spine.len(), 3);
1665
1666 builder.remove_last_spine();
1667 assert_eq!(builder.spine.len(), 2);
1668 assert_eq!(builder.spine[0].idref, "chapter1");
1669 assert_eq!(builder.spine[1].idref, "chapter2");
1670
1671 builder.remove_last_spine();
1672 assert_eq!(builder.spine.len(), 1);
1673 assert_eq!(builder.spine[0].idref, "chapter1");
1674
1675 builder.remove_last_spine();
1676 assert!(builder.spine.is_empty());
1677
1678 builder.remove_last_spine();
1679 assert!(builder.spine.is_empty());
1680 }
1681
1682 #[test]
1683 fn test_take_last_spine() {
1684 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1685
1686 let result = builder.take_last_spine();
1687 assert!(result.is_none());
1688
1689 builder.add_spine(SpineItem::new("chapter1"));
1690 builder.add_spine(SpineItem::new("chapter2"));
1691 builder.add_spine(SpineItem::new("chapter3"));
1692 assert_eq!(builder.spine.len(), 3);
1693
1694 let result = builder.take_last_spine();
1695 assert!(result.is_some());
1696 assert_eq!(result.unwrap().idref, "chapter3");
1697 assert_eq!(builder.spine.len(), 2);
1698
1699 let result = builder.take_last_spine();
1700 assert_eq!(result.unwrap().idref, "chapter2");
1701 assert_eq!(builder.spine.len(), 1);
1702
1703 let result = builder.take_last_spine();
1704 assert_eq!(result.unwrap().idref, "chapter1");
1705 assert!(builder.spine.is_empty());
1706
1707 let result = builder.take_last_spine();
1708 assert!(result.is_none());
1709 }
1710
1711 #[test]
1712 fn test_clear_spines() {
1713 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1714
1715 builder.clear_spines();
1716 assert!(builder.spine.is_empty());
1717
1718 builder.add_spine(SpineItem::new("chapter1"));
1719 builder.add_spine(SpineItem::new("chapter2"));
1720 builder.add_spine(SpineItem::new("chapter3"));
1721 assert_eq!(builder.spine.len(), 3);
1722
1723 builder.clear_spines();
1724 assert!(builder.spine.is_empty());
1725 assert_eq!(builder.spine.len(), 0);
1726
1727 builder.add_spine(SpineItem::new("new_chapter"));
1728 assert_eq!(builder.spine.len(), 1);
1729 assert_eq!(builder.spine[0].idref, "new_chapter");
1730 }
1731
1732 #[test]
1733 fn test_set_catalog_title() {
1734 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1735 let title = "Test Catalog Title";
1736
1737 builder.set_catalog_title(title);
1738
1739 assert_eq!(builder.catalog_title, title);
1740 }
1741
1742 #[test]
1743 fn test_add_catalog_item() {
1744 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1745 let nav_point = NavPoint::new("Chapter 1");
1746
1747 builder.add_catalog_item(nav_point.clone());
1748
1749 assert_eq!(builder.catalog.len(), 1);
1750 assert_eq!(builder.catalog[0].label, "Chapter 1");
1751 }
1752
1753 #[test]
1754 fn test_remove_last_catalog_item() {
1755 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1756
1757 builder.add_catalog_item(NavPoint::new("Chapter 1"));
1758 builder.add_catalog_item(NavPoint::new("Chapter 2"));
1759 builder.add_catalog_item(NavPoint::new("Chapter 3"));
1760 assert_eq!(builder.catalog.len(), 3);
1761
1762 builder.remove_last_catalog_item();
1763 assert_eq!(builder.catalog.len(), 2);
1764 assert_eq!(builder.catalog[0].label, "Chapter 1");
1765 assert_eq!(builder.catalog[1].label, "Chapter 2");
1766
1767 builder.remove_last_catalog_item();
1768 assert_eq!(builder.catalog.len(), 1);
1769 assert_eq!(builder.catalog[0].label, "Chapter 1");
1770
1771 builder.remove_last_catalog_item();
1772 assert!(builder.catalog.is_empty());
1773
1774 builder.remove_last_catalog_item();
1775 assert!(builder.catalog.is_empty());
1776 }
1777
1778 #[test]
1779 fn test_take_last_catalog_item() {
1780 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1781
1782 let result = builder.take_last_catalog_item();
1783 assert!(result.is_none());
1784
1785 builder.add_catalog_item(NavPoint::new("Chapter 1"));
1786 builder.add_catalog_item(NavPoint::new("Chapter 2"));
1787 builder.add_catalog_item(NavPoint::new("Chapter 3"));
1788 assert_eq!(builder.catalog.len(), 3);
1789
1790 let result = builder.take_last_catalog_item();
1791 assert!(result.is_some());
1792 assert_eq!(result.unwrap().label, "Chapter 3");
1793 assert_eq!(builder.catalog.len(), 2);
1794
1795 let result = builder.take_last_catalog_item();
1796 assert_eq!(result.unwrap().label, "Chapter 2");
1797 assert_eq!(builder.catalog.len(), 1);
1798
1799 let result = builder.take_last_catalog_item();
1800 assert_eq!(result.unwrap().label, "Chapter 1");
1801 assert!(builder.catalog.is_empty());
1802
1803 let result = builder.take_last_catalog_item();
1804 assert!(result.is_none());
1805 }
1806
1807 #[test]
1808 fn test_set_catalog() {
1809 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1810 let nav_points = vec![NavPoint::new("Chapter 1"), NavPoint::new("Chapter 2")];
1811
1812 builder.set_catalog(nav_points.clone());
1813
1814 assert_eq!(builder.catalog.len(), 2);
1815 assert_eq!(builder.catalog[0].label, "Chapter 1");
1816 assert_eq!(builder.catalog[1].label, "Chapter 2");
1817 }
1818
1819 #[test]
1820 fn test_clear_catalog() {
1821 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1822
1823 builder.clear_catalog();
1824 assert!(builder.catalog.is_empty());
1825
1826 builder.add_catalog_item(NavPoint::new("Chapter 1"));
1827 builder.add_catalog_item(NavPoint::new("Chapter 2"));
1828 builder.add_catalog_item(NavPoint::new("Chapter 3"));
1829 assert_eq!(builder.catalog.len(), 3);
1830
1831 builder.clear_catalog();
1832 assert!(builder.catalog.is_empty());
1833 assert_eq!(builder.catalog.len(), 0);
1834
1835 builder.add_catalog_item(NavPoint::new("New Chapter"));
1836 assert_eq!(builder.catalog.len(), 1);
1837 assert_eq!(builder.catalog[0].label, "New Chapter");
1838 }
1839
1840 #[test]
1841 fn test_clear_all() {
1842 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1843
1844 builder.add_rootfile("content.opf").unwrap();
1845 builder.add_metadata(MetadataItem::new("title", "Test Book"));
1846 builder.add_metadata(MetadataItem::new("language", "en"));
1847 builder.add_spine(SpineItem::new("chapter1"));
1848 builder.add_spine(SpineItem::new("chapter2"));
1849 builder.add_catalog_item(NavPoint::new("Chapter 1"));
1850 builder.add_catalog_item(NavPoint::new("Chapter 2"));
1851 builder.set_catalog_title("Table of Contents");
1852
1853 assert_eq!(builder.metadata.len(), 2);
1854 assert_eq!(builder.spine.len(), 2);
1855 assert_eq!(builder.catalog.len(), 2);
1856 assert_eq!(builder.catalog_title, "Table of Contents");
1857
1858 let result = builder.clear_all();
1859 assert!(result.is_ok());
1860
1861 assert!(builder.metadata.is_empty());
1862 assert!(builder.spine.is_empty());
1863 assert!(builder.catalog.is_empty());
1864 assert!(builder.catalog_title.is_empty());
1865 assert!(builder.manifest.is_empty());
1866
1867 builder.add_metadata(MetadataItem::new("title", "New Book"));
1868 builder.add_spine(SpineItem::new("new_chapter"));
1869 builder.add_catalog_item(NavPoint::new("New Chapter"));
1870
1871 assert_eq!(builder.metadata.len(), 1);
1872 assert_eq!(builder.spine.len(), 1);
1873 assert_eq!(builder.catalog.len(), 1);
1874 }
1875
1876 #[test]
1877 fn test_make_container_file() {
1878 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1879
1880 let result = builder.make_container_xml();
1881 assert!(result.is_err());
1882 assert_eq!(
1883 result.unwrap_err(),
1884 EpubBuilderError::MissingRootfile.into()
1885 );
1886
1887 assert!(builder.add_rootfile("content.opf").is_ok());
1888 let result = builder.make_container_xml();
1889 assert!(result.is_ok());
1890 }
1891
1892 #[test]
1893 fn test_make_navigation_document() {
1894 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1895
1896 let result = builder.make_navigation_document();
1897 assert!(result.is_err());
1898 assert_eq!(
1899 result.unwrap_err(),
1900 EpubBuilderError::NavigationInfoUninitalized.into()
1901 );
1902
1903 builder.set_catalog(vec![NavPoint::new("test")]);
1904 assert!(builder.make_navigation_document().is_ok());
1905 }
1906
1907 #[test]
1908 fn test_validate_metadata_success() {
1909 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1910
1911 builder.add_metadata(MetadataItem::new("title", "Test Book"));
1912 builder.add_metadata(MetadataItem::new("language", "en"));
1913 builder.add_metadata(
1914 MetadataItem::new("identifier", "urn:isbn:1234567890")
1915 .with_id("pub-id")
1916 .build(),
1917 );
1918
1919 assert!(builder.validate_metadata());
1920 }
1921
1922 #[test]
1923 fn test_validate_metadata_missing_required() {
1924 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1925
1926 builder.add_metadata(MetadataItem::new("title", "Test Book"));
1927 builder.add_metadata(MetadataItem::new("language", "en"));
1928
1929 assert!(!builder.validate_metadata());
1930 }
1931
1932 #[test]
1933 fn test_validate_fallback_chain_valid() {
1934 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1935
1936 let item3 = ManifestItem::new("item3", "path3");
1937 assert!(item3.is_ok());
1938
1939 let item3 = item3.unwrap();
1940 let item2 = ManifestItem::new("item2", "path2")
1941 .unwrap()
1942 .with_fallback("item3")
1943 .build();
1944 let item1 = ManifestItem::new("item1", "path1")
1945 .unwrap()
1946 .with_fallback("item2")
1947 .build();
1948
1949 builder.manifest.insert("item3".to_string(), item3);
1950 builder.manifest.insert("item2".to_string(), item2);
1951 builder.manifest.insert("item1".to_string(), item1);
1952
1953 let result = builder.validate_manifest_fallback_chains();
1954 assert!(result.is_ok());
1955 }
1956
1957 #[test]
1958 fn test_validate_fallback_chain_circular_reference() {
1959 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1960
1961 let item2 = ManifestItem::new("item2", "path2")
1962 .unwrap()
1963 .with_fallback("item1")
1964 .build();
1965 let item1 = ManifestItem::new("item1", "path1")
1966 .unwrap()
1967 .with_fallback("item2")
1968 .build();
1969
1970 builder.manifest.insert("item1".to_string(), item1);
1971 builder.manifest.insert("item2".to_string(), item2);
1972
1973 let result = builder.validate_manifest_fallback_chains();
1974 assert!(result.is_err());
1975 assert!(
1976 result.unwrap_err().to_string().starts_with(
1977 "Epub builder error: Circular reference detected in fallback chain for"
1978 ),
1979 );
1980 }
1981
1982 #[test]
1983 fn test_validate_fallback_chain_not_found() {
1984 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1985
1986 let item1 = ManifestItem::new("item1", "path1")
1987 .unwrap()
1988 .with_fallback("nonexistent")
1989 .build();
1990
1991 builder.manifest.insert("item1".to_string(), item1);
1992
1993 let result = builder.validate_manifest_fallback_chains();
1994 assert!(result.is_err());
1995 assert_eq!(
1996 result.unwrap_err().to_string(),
1997 "Epub builder error: Fallback resource 'nonexistent' does not exist in manifest."
1998 );
1999 }
2000
2001 #[test]
2002 fn test_validate_manifest_nav_single() {
2003 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2004
2005 let nav_item = ManifestItem::new("nav", "nav.xhtml")
2006 .unwrap()
2007 .append_property("nav")
2008 .build();
2009 builder.manifest.insert("nav".to_string(), nav_item);
2010
2011 let result = builder.validate_manifest_nav();
2012 assert!(result.is_ok());
2013 }
2014
2015 #[test]
2016 fn test_validate_manifest_nav_multiple() {
2017 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2018
2019 let nav_item1 = ManifestItem::new("nav1", "nav1.xhtml")
2020 .unwrap()
2021 .append_property("nav")
2022 .build();
2023 let nav_item2 = ManifestItem::new("nav2", "nav2.xhtml")
2024 .unwrap()
2025 .append_property("nav")
2026 .build();
2027
2028 builder.manifest.insert("nav1".to_string(), nav_item1);
2029 builder.manifest.insert("nav2".to_string(), nav_item2);
2030
2031 let result = builder.validate_manifest_nav();
2032 assert!(result.is_err());
2033 assert_eq!(
2034 result.unwrap_err().to_string(),
2035 "Epub builder error: There are too many items with 'nav' property in the manifest."
2036 );
2037 }
2038
2039 #[test]
2040 fn test_make_opf_file_success() {
2041 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2042
2043 assert!(builder.add_rootfile("content.opf").is_ok());
2044 builder.add_metadata(MetadataItem::new("title", "Test Book"));
2045 builder.add_metadata(MetadataItem::new("language", "en"));
2046 builder.add_metadata(
2047 MetadataItem::new("identifier", "urn:isbn:1234567890")
2048 .with_id("pub-id")
2049 .build(),
2050 );
2051
2052 let temp_dir = env::temp_dir().join(local_time());
2053 fs::create_dir_all(&temp_dir).unwrap();
2054
2055 let test_file = temp_dir.join("test.xhtml");
2056 fs::write(&test_file, "<html></html>").unwrap();
2057
2058 let manifest_result = builder.add_manifest(
2059 test_file.to_str().unwrap(),
2060 ManifestItem::new("test", "test.xhtml").unwrap(),
2061 );
2062 assert!(manifest_result.is_ok());
2063
2064 builder.add_catalog_item(NavPoint::new("Chapter"));
2065 builder.add_spine(SpineItem::new("test"));
2066
2067 let result = builder.make_navigation_document();
2068 assert!(result.is_ok());
2069
2070 let result = builder.make_opf_file();
2071 assert!(result.is_ok());
2072
2073 let opf_path = builder.temp_dir.join("content.opf");
2074 assert!(opf_path.exists());
2075
2076 fs::remove_dir_all(temp_dir).unwrap();
2077 }
2078
2079 #[test]
2080 fn test_make_opf_file_missing_metadata() {
2081 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2082 assert!(builder.add_rootfile("content.opf").is_ok());
2083
2084 let result = builder.make_opf_file();
2085 assert!(result.is_err());
2086 assert_eq!(
2087 result.unwrap_err().to_string(),
2088 "Epub builder error: Requires at least one 'title', 'language', and 'identifier' with id 'pub-id'."
2089 );
2090 }
2091
2092 #[test]
2093 fn test_make() {
2094 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2095
2096 assert!(builder.add_rootfile("content.opf").is_ok());
2097 builder.add_metadata(MetadataItem::new("title", "Test Book"));
2098 builder.add_metadata(MetadataItem::new("language", "en"));
2099 builder.add_metadata(
2100 MetadataItem::new("identifier", "test_identifier")
2101 .with_id("pub-id")
2102 .build(),
2103 );
2104
2105 assert!(
2106 builder
2107 .add_manifest(
2108 "./test_case/Overview.xhtml",
2109 ManifestItem {
2110 id: "test".to_string(),
2111 path: PathBuf::from("test.xhtml"),
2112 mime: String::new(),
2113 properties: None,
2114 fallback: None,
2115 },
2116 )
2117 .is_ok()
2118 );
2119
2120 builder.add_catalog_item(NavPoint::new("Chapter"));
2121 builder.add_spine(SpineItem::new("test"));
2122
2123 let file = env::temp_dir()
2124 .join("temp_dir")
2125 .join(format!("{}.epub", local_time()));
2126 assert!(builder.make(&file).is_ok());
2127 assert!(EpubDoc::new(&file).is_ok());
2128 }
2129
2130 #[test]
2131 fn test_build() {
2132 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2133
2134 assert!(builder.add_rootfile("content.opf").is_ok());
2135 builder.add_metadata(MetadataItem::new("title", "Test Book"));
2136 builder.add_metadata(MetadataItem::new("language", "en"));
2137 builder.add_metadata(
2138 MetadataItem::new("identifier", "test_identifier")
2139 .with_id("pub-id")
2140 .build(),
2141 );
2142
2143 assert!(
2144 builder
2145 .add_manifest(
2146 "./test_case/Overview.xhtml",
2147 ManifestItem {
2148 id: "test".to_string(),
2149 path: PathBuf::from("test.xhtml"),
2150 mime: String::new(),
2151 properties: None,
2152 fallback: None,
2153 },
2154 )
2155 .is_ok()
2156 );
2157
2158 builder.add_catalog_item(NavPoint::new("Chapter"));
2159 builder.add_spine(SpineItem::new("test"));
2160
2161 let file = env::temp_dir().join(format!("{}.epub", local_time()));
2162 assert!(builder.build(&file).is_ok());
2163 }
2164
2165 #[test]
2166 fn test_from() {
2167 let builder = EpubBuilder::<EpubVersion3>::new();
2168 assert!(builder.is_ok());
2169
2170 let metadata = vec![
2171 MetadataItem {
2172 id: None,
2173 property: "title".to_string(),
2174 value: "Test Book".to_string(),
2175 lang: None,
2176 refined: vec![],
2177 },
2178 MetadataItem {
2179 id: None,
2180 property: "language".to_string(),
2181 value: "en".to_string(),
2182 lang: None,
2183 refined: vec![],
2184 },
2185 MetadataItem {
2186 id: Some("pub-id".to_string()),
2187 property: "identifier".to_string(),
2188 value: "test-book".to_string(),
2189 lang: None,
2190 refined: vec![],
2191 },
2192 ];
2193 let spine = vec![SpineItem {
2194 id: None,
2195 idref: "main".to_string(),
2196 linear: true,
2197 properties: None,
2198 }];
2199 let catalog = vec![
2200 NavPoint {
2201 label: "Nav".to_string(),
2202 content: None,
2203 children: vec![],
2204 play_order: None,
2205 },
2206 NavPoint {
2207 label: "Overview".to_string(),
2208 content: None,
2209 children: vec![],
2210 play_order: None,
2211 },
2212 ];
2213
2214 let mut builder = builder.unwrap();
2215 assert!(builder.add_rootfile("content.opf").is_ok());
2216 builder.metadata = metadata.clone();
2217 builder.spine = spine.clone();
2218 builder.catalog = catalog.clone();
2219 builder.set_catalog_title("catalog title");
2220 let result = builder.add_manifest(
2221 "./test_case/Overview.xhtml",
2222 ManifestItem {
2223 id: "main".to_string(),
2224 path: PathBuf::from("Overview.xhtml"),
2225 mime: String::new(),
2226 properties: None,
2227 fallback: None,
2228 },
2229 );
2230 assert!(result.is_ok());
2231
2232 let epub_file = env::temp_dir().join(format!("{}.epub", local_time()));
2233 let result = builder.make(&epub_file);
2234 assert!(result.is_ok());
2235
2236 let doc = EpubDoc::new(&epub_file);
2237 assert!(doc.is_ok());
2238
2239 let mut doc = doc.unwrap();
2240 let builder = EpubBuilder::from(&mut doc);
2241 assert!(builder.is_ok());
2242 let builder = builder.unwrap();
2243
2244 assert_eq!(builder.metadata.len(), metadata.len() + 1);
2245 assert_eq!(builder.manifest.len(), 1); assert_eq!(builder.spine.len(), spine.len());
2247 assert_eq!(builder.catalog, catalog);
2248 assert_eq!(builder.catalog_title, "catalog title");
2249
2250 fs::remove_file(epub_file).unwrap();
2251 }
2252
2253 #[test]
2254 fn test_normalize_manifest_path() {
2255 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2256
2257 assert!(builder.add_rootfile("content.opf").is_ok());
2258
2259 let result = builder.normalize_manifest_path("../../test.xhtml", "id");
2260 assert!(result.is_err());
2261 assert_eq!(
2262 result.unwrap_err(),
2263 EpubError::RelativeLinkLeakage { path: "../../test.xhtml".to_string() }
2264 );
2265
2266 let result = builder.normalize_manifest_path("/test.xhtml", "id");
2267 assert!(result.is_ok());
2268 assert_eq!(result.unwrap(), builder.temp_dir.join("test.xhtml"));
2269
2270 let result = builder.normalize_manifest_path("./test.xhtml", "manifest_id");
2271 assert!(result.is_err());
2272 assert_eq!(
2273 result.unwrap_err(),
2274 EpubBuilderError::IllegalManifestPath { manifest_id: "manifest_id".to_string() }.into(),
2275 );
2276 }
2277
2278 #[test]
2279 fn test_refine_mime_type() {
2280 assert_eq!(
2281 refine_mime_type("text/xml", "xhtml"),
2282 "application/xhtml+xml"
2283 );
2284 assert_eq!(refine_mime_type("text/xml", "xht"), "application/xhtml+xml");
2285 assert_eq!(
2286 refine_mime_type("application/xml", "opf"),
2287 "application/oebps-package+xml"
2288 );
2289 assert_eq!(
2290 refine_mime_type("text/xml", "ncx"),
2291 "application/x-dtbncx+xml"
2292 );
2293 assert_eq!(refine_mime_type("text/plain", "css"), "text/css");
2294 assert_eq!(refine_mime_type("text/plain", "unknown"), "text/plain");
2295 }
2296
2297 #[cfg(feature = "content-builder")]
2298 mod make_contents_tests {
2299 use std::path::PathBuf;
2300
2301 use crate::builder::{EpubBuilder, EpubVersion3, content::ContentBuilder};
2302
2303 #[test]
2304 fn test_make_contents_basic() {
2305 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2306 builder.add_rootfile("content.opf").unwrap();
2307
2308 let mut content_builder = ContentBuilder::new("chapter1", "en").unwrap();
2309 content_builder
2310 .set_title("Test Chapter")
2311 .add_text_block("This is a test paragraph.", vec![])
2312 .unwrap();
2313
2314 builder.add_content("OEBPS/chapter1.xhtml", content_builder);
2315
2316 let result = builder.make_contents();
2317 assert!(result.is_ok());
2318 assert!(builder.temp_dir.join("OEBPS/chapter1.xhtml").exists());
2319 }
2320
2321 #[test]
2322 fn test_make_contents_multiple_blocks() {
2323 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2324 builder.add_rootfile("content.opf").unwrap();
2325
2326 let mut content_builder = ContentBuilder::new("chapter2", "zh-CN").unwrap();
2327 content_builder
2328 .set_title("多个区块章节")
2329 .add_text_block("第一段文本。", vec![])
2330 .unwrap()
2331 .add_quote_block("这是一个引用。", vec![])
2332 .unwrap()
2333 .add_title_block("子标题", 2, vec![])
2334 .unwrap()
2335 .add_text_block("最后的文本段落。", vec![])
2336 .unwrap();
2337
2338 builder.add_content("OEBPS/chapter2.xhtml", content_builder);
2339
2340 let result = builder.make_contents();
2341 assert!(result.is_ok());
2342 assert!(builder.temp_dir.join("OEBPS/chapter2.xhtml").exists());
2343 }
2344
2345 #[test]
2346 fn test_make_contents_with_media() {
2347 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2348 builder.add_rootfile("content.opf").unwrap();
2349
2350 let img_path = PathBuf::from("./test_case/image.jpg");
2351
2352 let mut content_builder = ContentBuilder::new("chapter3", "en").unwrap();
2353 content_builder
2354 .set_title("Chapter with Media")
2355 .add_text_block("Text before image.", vec![])
2356 .unwrap()
2357 .add_image_block(
2358 img_path,
2359 Some("Test Image".to_string()),
2360 Some("Figure 1: A test image".to_string()),
2361 vec![],
2362 )
2363 .unwrap()
2364 .add_text_block("Text after image.", vec![])
2365 .unwrap();
2366
2367 builder.add_content("OEBPS/chapter3.xhtml", content_builder);
2368
2369 let result = builder.make_contents();
2370 assert!(result.is_ok());
2371 assert!(builder.temp_dir.join("OEBPS/chapter3.xhtml").exists());
2372 assert!(builder.temp_dir.join("OEBPS/img").exists());
2373 assert!(builder.temp_dir.join("OEBPS/img/image.jpg").exists());
2374 }
2375
2376 #[test]
2377 fn test_make_contents_multiple_documents() {
2378 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2379 builder.add_rootfile("content.opf").unwrap();
2380
2381 let mut content = ContentBuilder::new("ch1", "en").unwrap();
2382 content
2383 .set_title("Chapter 1")
2384 .add_text_block("Content of chapter 1", vec![])
2385 .unwrap();
2386 builder.add_content("OEBPS/chapter1.xhtml", content);
2387
2388 let mut content = ContentBuilder::new("ch2", "en").unwrap();
2389 content
2390 .set_title("Chapter 2")
2391 .add_text_block("Content of chapter 2", vec![])
2392 .unwrap();
2393 builder.add_content("OEBPS/chapter2.xhtml", content);
2394
2395 let mut content = ContentBuilder::new("ch3", "en").unwrap();
2396 content
2397 .set_title("Chapter 3")
2398 .add_text_block("Content of chapter 3", vec![])
2399 .unwrap();
2400 builder.add_content("OEBPS/chapter3.xhtml", content);
2401
2402 let result = builder.make_contents();
2403 assert!(result.is_ok());
2404 assert!(builder.temp_dir.join("OEBPS/chapter1.xhtml").exists());
2405 assert!(builder.temp_dir.join("OEBPS/chapter2.xhtml").exists());
2406 assert!(builder.temp_dir.join("OEBPS/chapter3.xhtml").exists());
2407 }
2408
2409 #[test]
2410 fn test_make_contents_different_languages() {
2411 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2412 builder.add_rootfile("content.opf").unwrap();
2413
2414 let mut content = ContentBuilder::new("en_ch", "en").unwrap();
2415 content
2416 .set_title("English Chapter")
2417 .add_text_block("English text.", vec![])
2418 .unwrap();
2419 builder.add_content("OEBPS/en_chapter.xhtml", content);
2420
2421 let mut content = ContentBuilder::new("zh_ch", "zh-CN").unwrap();
2422 content
2423 .set_title("中文章节")
2424 .add_text_block("中文文本。", vec![])
2425 .unwrap();
2426 builder.add_content("OEBPS/zh_chapter.xhtml", content);
2427
2428 let mut content = ContentBuilder::new("ja_ch", "ja").unwrap();
2429 content
2430 .set_title("日本語の章")
2431 .add_text_block("日本語のテキスト。", vec![])
2432 .unwrap();
2433 builder.add_content("OEBPS/ja_chapter.xhtml", content);
2434
2435 let result = builder.make_contents();
2436 assert!(result.is_ok());
2437 assert!(builder.temp_dir.join("OEBPS/en_chapter.xhtml").exists());
2438 assert!(builder.temp_dir.join("OEBPS/zh_chapter.xhtml").exists());
2439 assert!(builder.temp_dir.join("OEBPS/ja_chapter.xhtml").exists());
2440 }
2441
2442 #[test]
2443 fn test_make_contents_unique_identifiers() {
2444 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2445 builder.add_rootfile("content.opf").unwrap();
2446
2447 let mut content = ContentBuilder::new("unique_id_1", "en").unwrap();
2448 content.add_text_block("First content", vec![]).unwrap();
2449 builder.add_content("OEBPS/ch1.xhtml", content);
2450
2451 let mut content = ContentBuilder::new("unique_id_2", "en").unwrap();
2452 content.add_text_block("Second content", vec![]).unwrap();
2453 builder.add_content("OEBPS/ch2.xhtml", content);
2454
2455 let mut content = ContentBuilder::new("unique_id_1", "en").unwrap();
2456 content
2457 .add_text_block("Duplicate ID content", vec![])
2458 .unwrap();
2459 builder.add_content("OEBPS/ch3.xhtml", content);
2460
2461 let result = builder.make_contents();
2462 assert!(result.is_ok());
2463 assert!(builder.temp_dir.join("OEBPS/ch1.xhtml").exists()); assert!(builder.temp_dir.join("OEBPS/ch2.xhtml").exists());
2465 assert!(builder.temp_dir.join("OEBPS/ch3.xhtml").exists());
2466
2467 let manifest = builder.manifest.get("unique_id_1");
2468 assert!(manifest.is_some());
2469
2470 let manifest = manifest.unwrap();
2471 assert_eq!(manifest.path, PathBuf::from("/OEBPS/ch3.xhtml"));
2472 }
2473
2474 #[test]
2475 fn test_make_contents_complex_structure() {
2476 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2477 builder.add_rootfile("content.opf").unwrap();
2478
2479 let mut content = ContentBuilder::new("complex_ch", "en").unwrap();
2480 content
2481 .set_title("Complex Chapter")
2482 .add_title_block("Section 1", 2, vec![])
2483 .unwrap()
2484 .add_text_block("Introduction text.", vec![])
2485 .unwrap()
2486 .add_quote_block("A wise quote here.", vec![])
2487 .unwrap()
2488 .add_title_block("Section 2", 2, vec![])
2489 .unwrap()
2490 .add_text_block("More content with multiple paragraphs.", vec![])
2491 .unwrap()
2492 .add_text_block("Another paragraph.", vec![])
2493 .unwrap()
2494 .add_title_block("Section 3", 2, vec![])
2495 .unwrap()
2496 .add_quote_block("Another quotation.", vec![])
2497 .unwrap();
2498
2499 builder.add_content("OEBPS/complex_chapter.xhtml", content);
2500
2501 let result = builder.make_contents();
2502 assert!(result.is_ok());
2503 assert!(
2504 builder
2505 .temp_dir
2506 .join("OEBPS/complex_chapter.xhtml")
2507 .exists()
2508 );
2509 }
2510
2511 #[test]
2512 fn test_make_contents_empty_document() {
2513 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2514 builder.add_rootfile("content.opf").unwrap();
2515
2516 let content = ContentBuilder::new("empty_ch", "en").unwrap();
2517 builder.add_content("OEBPS/empty.xhtml", content);
2518
2519 let result = builder.make_contents();
2520 assert!(result.is_ok());
2521 assert!(builder.temp_dir.join("OEBPS/empty.xhtml").exists());
2522 }
2523
2524 #[test]
2525 fn test_make_contents_path_normalization() {
2526 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2527 builder.add_rootfile("OEBPS/content.opf").unwrap();
2528
2529 let mut content = ContentBuilder::new("path_test", "en").unwrap();
2530 content.add_text_block("Path test content", vec![]).unwrap();
2531
2532 builder.add_content("/OEBPS/text/chapter.xhtml", content);
2533
2534 let result = builder.make_contents();
2535 assert!(result.is_ok());
2536 assert!(builder.temp_dir.join("OEBPS/text/chapter.xhtml").exists());
2537 }
2538
2539 #[test]
2540 fn test_make_contents_remove_and_readd() {
2541 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2542 builder.add_rootfile("content.opf").unwrap();
2543
2544 let mut content = ContentBuilder::new("ch1", "en").unwrap();
2545 content.add_text_block("First content", vec![]).unwrap();
2546 builder.add_content("OEBPS/ch1.xhtml", content);
2547
2548 let mut content = ContentBuilder::new("ch2", "en").unwrap();
2549 content.add_text_block("Second content", vec![]).unwrap();
2550 builder.add_content("OEBPS/ch2.xhtml", content);
2551
2552 builder.remove_last_content();
2553
2554 let taken = builder.take_last_content();
2555 assert!(taken.is_some());
2556 assert_eq!(taken.as_ref().unwrap().0, PathBuf::from("OEBPS/ch1.xhtml"));
2557
2558 let mut content = ContentBuilder::new("ch3", "en").unwrap();
2559 content.add_text_block("Third content", vec![]).unwrap();
2560 builder.add_content("OEBPS/ch3.xhtml", content);
2561
2562 let result = builder.make_contents();
2563 assert!(result.is_ok());
2564 assert!(builder.temp_dir.join("OEBPS/ch3.xhtml").exists());
2565 }
2566
2567 #[test]
2568 fn test_make_contents_clear_all() {
2569 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2570 builder.add_rootfile("content.opf").unwrap();
2571
2572 let mut content = ContentBuilder::new("ch1", "en").unwrap();
2573 content.add_text_block("Content 1", vec![]).unwrap();
2574 builder.add_content("OEBPS/ch1.xhtml", content);
2575
2576 let mut content = ContentBuilder::new("ch2", "en").unwrap();
2577 content.add_text_block("Content 2", vec![]).unwrap();
2578 builder.add_content("OEBPS/ch2.xhtml", content);
2579
2580 builder.clear_contents();
2581
2582 let result = builder.make_contents();
2583 assert!(result.is_ok());
2584 assert!(!builder.temp_dir.join("OEBPS/ch1.xhtml").exists());
2585 assert!(!builder.temp_dir.join("OEBPS/ch2.xhtml").exists());
2586 }
2587 }
2588}