1use std::path::PathBuf;
28
29#[cfg(feature = "builder")]
30use crate::{
31 error::{EpubBuilderError, EpubError},
32 utils::ELEMENT_IN_DC_NAMESPACE,
33};
34
35#[derive(Debug, PartialEq, Eq)]
39pub enum EpubVersion {
40 Version2_0,
41 Version3_0,
42}
43
44#[derive(Debug, Clone)]
69pub struct MetadataItem {
70 pub id: Option<String>,
75
76 pub property: String,
82
83 pub value: String,
85
86 pub lang: Option<String>,
88
89 pub refined: Vec<MetadataRefinement>,
97}
98
99#[cfg(feature = "builder")]
100impl MetadataItem {
101 pub fn new(property: &str, value: &str) -> Self {
109 Self {
110 id: None,
111 property: property.to_string(),
112 value: value.to_string(),
113 lang: None,
114 refined: vec![],
115 }
116 }
117
118 pub fn with_id(&mut self, id: &str) -> &mut Self {
125 self.id = Some(id.to_string());
126 self
127 }
128
129 pub fn with_lang(&mut self, lang: &str) -> &mut Self {
136 self.lang = Some(lang.to_string());
137 self
138 }
139
140 pub fn append_refinement(&mut self, refine: MetadataRefinement) -> &mut Self {
150 if self.id.is_some() {
151 self.refined.push(refine);
152 } else {
153 }
155
156 self
157 }
158
159 pub fn build(&self) -> Self {
163 Self { ..self.clone() }
164 }
165
166 pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
168 let mut attributes = Vec::new();
169
170 if !ELEMENT_IN_DC_NAMESPACE.contains(&self.property.as_str()) {
171 attributes.push(("property", self.property.as_str()));
172 }
173
174 if let Some(id) = &self.id {
175 attributes.push(("id", id.as_str()));
176 };
177
178 if let Some(lang) = &self.lang {
179 attributes.push(("lang", lang.as_str()));
180 };
181
182 attributes
183 }
184}
185
186#[derive(Debug, Clone)]
210pub struct MetadataRefinement {
211 pub refines: String,
212
213 pub property: String,
218
219 pub value: String,
221
222 pub lang: Option<String>,
224
225 pub scheme: Option<String>,
230}
231
232#[cfg(feature = "builder")]
233impl MetadataRefinement {
234 pub fn new(refines: &str, property: &str, value: &str) -> Self {
243 Self {
244 refines: refines.to_string(),
245 property: property.to_string(),
246 value: value.to_string(),
247 lang: None,
248 scheme: None,
249 }
250 }
251
252 pub fn with_lang(&mut self, lang: &str) -> &mut Self {
259 self.lang = Some(lang.to_string());
260 self
261 }
262
263 pub fn with_scheme(&mut self, scheme: &str) -> &mut Self {
270 self.scheme = Some(scheme.to_string());
271 self
272 }
273
274 pub fn build(&self) -> Self {
278 Self { ..self.clone() }
279 }
280
281 pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
283 let mut attributes = Vec::new();
284
285 attributes.push(("refines", self.refines.as_str()));
286 attributes.push(("property", self.property.as_str()));
287
288 if let Some(lang) = &self.lang {
289 attributes.push(("lang", lang.as_str()));
290 };
291
292 if let Some(scheme) = &self.scheme {
293 attributes.push(("scheme", scheme.as_str()));
294 };
295
296 attributes
297 }
298}
299
300#[derive(Debug)]
309pub struct MetadataLinkItem {
310 pub href: String,
312
313 pub rel: String,
315
316 pub hreflang: Option<String>,
318
319 pub id: Option<String>,
323
324 pub mime: Option<String>,
326
327 pub properties: Option<String>,
332
333 pub refines: Option<String>,
338}
339
340#[derive(Debug, Clone)]
371pub struct ManifestItem {
372 pub id: String,
374
375 pub path: PathBuf,
381
382 pub mime: String,
384
385 pub properties: Option<String>,
391
392 pub fallback: Option<String>,
403}
404
405#[cfg(feature = "builder")]
406impl ManifestItem {
407 pub fn new(id: &str, path: &str) -> Result<Self, EpubError> {
418 if path.starts_with("../") {
419 return Err(
420 EpubBuilderError::IllegalManifestPath { manifest_id: id.to_string() }.into(),
421 );
422 }
423
424 Ok(Self {
425 id: id.to_string(),
426 path: PathBuf::from(path),
427 mime: String::new(),
428 properties: None,
429 fallback: None,
430 })
431 }
432
433 pub(crate) fn set_mime(self, mime: &str) -> Self {
435 Self {
436 id: self.id,
437 path: self.path,
438 mime: mime.to_string(),
439 properties: self.properties,
440 fallback: self.fallback,
441 }
442 }
443
444 pub fn append_property(&mut self, property: &str) -> &mut Self {
451 let new_properties = if let Some(properties) = &self.properties {
452 format!("{} {}", properties, property)
453 } else {
454 property.to_string()
455 };
456
457 self.properties = Some(new_properties);
458 self
459 }
460
461 pub fn with_fallback(&mut self, fallback: &str) -> &mut Self {
468 self.fallback = Some(fallback.to_string());
469 self
470 }
471
472 pub fn build(&self) -> Self {
476 Self { ..self.clone() }
477 }
478
479 pub fn attributes(&self) -> Vec<(&str, &str)> {
481 let mut attributes = Vec::new();
482
483 attributes.push(("id", self.id.as_str()));
484 attributes.push(("href", self.path.to_str().unwrap()));
485 attributes.push(("media-type", self.mime.as_str()));
486
487 if let Some(properties) = &self.properties {
488 attributes.push(("properties", properties.as_str()));
489 }
490
491 if let Some(fallback) = &self.fallback {
492 attributes.push(("fallback", fallback.as_str()));
493 }
494
495 attributes
496 }
497}
498
499#[derive(Debug, Clone)]
526pub struct SpineItem {
527 pub idref: String,
533
534 pub id: Option<String>,
536
537 pub properties: Option<String>,
543
544 pub linear: bool,
554}
555
556#[cfg(feature = "builder")]
557impl SpineItem {
558 pub fn new(idref: &str) -> Self {
567 Self {
568 idref: idref.to_string(),
569 id: None,
570 properties: None,
571 linear: true,
572 }
573 }
574
575 pub fn with_id(&mut self, id: &str) -> &mut Self {
582 self.id = Some(id.to_string());
583 self
584 }
585
586 pub fn append_property(&mut self, property: &str) -> &mut Self {
593 let new_properties = if let Some(properties) = &self.properties {
594 format!("{} {}", properties, property)
595 } else {
596 property.to_string()
597 };
598
599 self.properties = Some(new_properties);
600 self
601 }
602
603 pub fn set_linear(&mut self, linear: bool) -> &mut Self {
610 self.linear = linear;
611 self
612 }
613
614 pub fn build(&self) -> Self {
618 Self { ..self.clone() }
619 }
620
621 pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
623 let mut attributes = Vec::new();
624
625 attributes.push(("idref", self.idref.as_str()));
626 attributes.push(("linear", if self.linear { "yes" } else { "no" }));
627
628 if let Some(id) = &self.id {
629 attributes.push(("id", id.as_str()));
630 }
631
632 if let Some(properties) = &self.properties {
633 attributes.push(("properties", properties.as_str()));
634 }
635
636 attributes
637 }
638}
639
640#[derive(Debug, Clone)]
646pub struct EncryptionData {
647 pub method: String,
654
655 pub data: String,
660}
661
662#[derive(Debug, Eq, Clone)]
687pub struct NavPoint {
688 pub label: String,
692
693 pub content: Option<PathBuf>,
698
699 pub children: Vec<NavPoint>,
701
702 pub play_order: Option<usize>,
707}
708
709#[cfg(feature = "builder")]
710impl NavPoint {
711 pub fn new(label: &str) -> Self {
718 Self {
719 label: label.to_string(),
720 content: None,
721 children: vec![],
722 play_order: None,
723 }
724 }
725
726 pub fn with_content(&mut self, content: &str) -> &mut Self {
733 self.content = Some(PathBuf::from(content));
734 self
735 }
736
737 pub fn append_child(&mut self, child: NavPoint) -> &mut Self {
744 self.children.push(child);
745 self
746 }
747
748 pub fn set_children(&mut self, children: Vec<NavPoint>) -> &mut Self {
755 self.children = children;
756 self
757 }
758
759 pub fn build(&self) -> Self {
763 Self { ..self.clone() }
764 }
765}
766
767impl Ord for NavPoint {
768 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
769 self.play_order.cmp(&other.play_order)
770 }
771}
772
773impl PartialOrd for NavPoint {
774 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
775 Some(self.cmp(other))
776 }
777}
778
779impl PartialEq for NavPoint {
780 fn eq(&self, other: &Self) -> bool {
781 self.play_order == other.play_order
782 }
783}
784
785#[cfg(feature = "content_builder")]
790#[derive(Debug, Clone, Eq, PartialEq)]
791pub struct Footnote {
792 pub locate: usize,
793 pub content: String,
794}
795
796#[cfg(feature = "content_builder")]
797impl Ord for Footnote {
798 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
799 self.locate.cmp(&other.locate)
800 }
801}
802
803#[cfg(feature = "content_builder")]
804impl PartialOrd for Footnote {
805 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
806 Some(self.cmp(other))
807 }
808}
809
810#[cfg(feature = "content_builder")]
812pub enum BlockType {
813 Text,
814 Quote,
815 Title,
816 Image,
817 Audio,
818 Video,
819 MathML,
820}
821
822#[cfg(test)]
823mod tests {
824 mod navpoint_tests {
825 use std::path::PathBuf;
826
827 use crate::types::NavPoint;
828
829 #[test]
831 fn test_navpoint_partial_eq() {
832 let nav1 = NavPoint {
833 label: "Chapter 1".to_string(),
834 content: Some(PathBuf::from("chapter1.html")),
835 children: vec![],
836 play_order: Some(1),
837 };
838
839 let nav2 = NavPoint {
840 label: "Chapter 1".to_string(),
841 content: Some(PathBuf::from("chapter2.html")),
842 children: vec![],
843 play_order: Some(1),
844 };
845
846 let nav3 = NavPoint {
847 label: "Chapter 2".to_string(),
848 content: Some(PathBuf::from("chapter1.html")),
849 children: vec![],
850 play_order: Some(2),
851 };
852
853 assert_eq!(nav1, nav2); assert_ne!(nav1, nav3); }
856
857 #[test]
859 fn test_navpoint_ord() {
860 let nav1 = NavPoint {
861 label: "Chapter 1".to_string(),
862 content: Some(PathBuf::from("chapter1.html")),
863 children: vec![],
864 play_order: Some(1),
865 };
866
867 let nav2 = NavPoint {
868 label: "Chapter 2".to_string(),
869 content: Some(PathBuf::from("chapter2.html")),
870 children: vec![],
871 play_order: Some(2),
872 };
873
874 let nav3 = NavPoint {
875 label: "Chapter 3".to_string(),
876 content: Some(PathBuf::from("chapter3.html")),
877 children: vec![],
878 play_order: Some(3),
879 };
880
881 assert!(nav1 < nav2);
883 assert!(nav2 > nav1);
884 assert!(nav1 == nav1);
885
886 assert_eq!(nav1.partial_cmp(&nav2), Some(std::cmp::Ordering::Less));
888 assert_eq!(nav2.partial_cmp(&nav1), Some(std::cmp::Ordering::Greater));
889 assert_eq!(nav1.partial_cmp(&nav1), Some(std::cmp::Ordering::Equal));
890
891 let mut nav_points = vec![nav2.clone(), nav3.clone(), nav1.clone()];
893 nav_points.sort();
894 assert_eq!(nav_points, vec![nav1, nav2, nav3]);
895 }
896
897 #[test]
899 fn test_navpoint_ord_with_none_play_order() {
900 let nav_with_order = NavPoint {
901 label: "Chapter 1".to_string(),
902 content: Some(PathBuf::from("chapter1.html")),
903 children: vec![],
904 play_order: Some(1),
905 };
906
907 let nav_without_order = NavPoint {
908 label: "Preface".to_string(),
909 content: Some(PathBuf::from("preface.html")),
910 children: vec![],
911 play_order: None,
912 };
913
914 assert!(nav_without_order < nav_with_order);
915 assert!(nav_with_order > nav_without_order);
916
917 let nav_without_order2 = NavPoint {
918 label: "Introduction".to_string(),
919 content: Some(PathBuf::from("intro.html")),
920 children: vec![],
921 play_order: None,
922 };
923
924 assert!(nav_without_order == nav_without_order2);
925 }
926
927 #[test]
929 fn test_navpoint_with_children() {
930 let child1 = NavPoint {
931 label: "Section 1.1".to_string(),
932 content: Some(PathBuf::from("section1_1.html")),
933 children: vec![],
934 play_order: Some(1),
935 };
936
937 let child2 = NavPoint {
938 label: "Section 1.2".to_string(),
939 content: Some(PathBuf::from("section1_2.html")),
940 children: vec![],
941 play_order: Some(2),
942 };
943
944 let parent1 = NavPoint {
945 label: "Chapter 1".to_string(),
946 content: Some(PathBuf::from("chapter1.html")),
947 children: vec![child1.clone(), child2.clone()],
948 play_order: Some(1),
949 };
950
951 let parent2 = NavPoint {
952 label: "Chapter 1".to_string(),
953 content: Some(PathBuf::from("chapter1.html")),
954 children: vec![child1.clone(), child2.clone()],
955 play_order: Some(1),
956 };
957
958 assert!(parent1 == parent2);
959
960 let parent3 = NavPoint {
961 label: "Chapter 2".to_string(),
962 content: Some(PathBuf::from("chapter2.html")),
963 children: vec![child1.clone(), child2.clone()],
964 play_order: Some(2),
965 };
966
967 assert!(parent1 != parent3);
968 assert!(parent1 < parent3);
969 }
970
971 #[test]
973 fn test_navpoint_with_none_content() {
974 let nav1 = NavPoint {
975 label: "Chapter 1".to_string(),
976 content: None,
977 children: vec![],
978 play_order: Some(1),
979 };
980
981 let nav2 = NavPoint {
982 label: "Chapter 1".to_string(),
983 content: None,
984 children: vec![],
985 play_order: Some(1),
986 };
987
988 assert!(nav1 == nav2);
989 }
990 }
991
992 #[cfg(feature = "builder")]
993 mod builder_tests {
994 mod metadata_item {
995 use crate::types::{MetadataItem, MetadataRefinement};
996
997 #[test]
998 fn test_metadata_item_new() {
999 let metadata_item = MetadataItem::new("title", "EPUB Test Book");
1000
1001 assert_eq!(metadata_item.property, "title");
1002 assert_eq!(metadata_item.value, "EPUB Test Book");
1003 assert_eq!(metadata_item.id, None);
1004 assert_eq!(metadata_item.lang, None);
1005 assert_eq!(metadata_item.refined.len(), 0);
1006 }
1007
1008 #[test]
1009 fn test_metadata_item_with_id() {
1010 let mut metadata_item = MetadataItem::new("creator", "John Doe");
1011 metadata_item.with_id("creator-1");
1012
1013 assert_eq!(metadata_item.property, "creator");
1014 assert_eq!(metadata_item.value, "John Doe");
1015 assert_eq!(metadata_item.id, Some("creator-1".to_string()));
1016 assert_eq!(metadata_item.lang, None);
1017 assert_eq!(metadata_item.refined.len(), 0);
1018 }
1019
1020 #[test]
1021 fn test_metadata_item_with_lang() {
1022 let mut metadata_item = MetadataItem::new("title", "测试书籍");
1023 metadata_item.with_lang("zh-CN");
1024
1025 assert_eq!(metadata_item.property, "title");
1026 assert_eq!(metadata_item.value, "测试书籍");
1027 assert_eq!(metadata_item.id, None);
1028 assert_eq!(metadata_item.lang, Some("zh-CN".to_string()));
1029 assert_eq!(metadata_item.refined.len(), 0);
1030 }
1031
1032 #[test]
1033 fn test_metadata_item_append_refinement() {
1034 let mut metadata_item = MetadataItem::new("creator", "John Doe");
1035 metadata_item.with_id("creator-1"); let refinement = MetadataRefinement::new("creator-1", "role", "author");
1038 metadata_item.append_refinement(refinement);
1039
1040 assert_eq!(metadata_item.refined.len(), 1);
1041 assert_eq!(metadata_item.refined[0].refines, "creator-1");
1042 assert_eq!(metadata_item.refined[0].property, "role");
1043 assert_eq!(metadata_item.refined[0].value, "author");
1044 }
1045
1046 #[test]
1047 fn test_metadata_item_append_refinement_without_id() {
1048 let mut metadata_item = MetadataItem::new("title", "Test Book");
1049 let refinement = MetadataRefinement::new("title", "title-type", "main");
1052 metadata_item.append_refinement(refinement);
1053
1054 assert_eq!(metadata_item.refined.len(), 0);
1056 }
1057
1058 #[test]
1059 fn test_metadata_item_build() {
1060 let mut metadata_item = MetadataItem::new("identifier", "urn:isbn:1234567890");
1061 metadata_item.with_id("pub-id").with_lang("en");
1062
1063 let built = metadata_item.build();
1064
1065 assert_eq!(built.property, "identifier");
1066 assert_eq!(built.value, "urn:isbn:1234567890");
1067 assert_eq!(built.id, Some("pub-id".to_string()));
1068 assert_eq!(built.lang, Some("en".to_string()));
1069 assert_eq!(built.refined.len(), 0);
1070 }
1071
1072 #[test]
1073 fn test_metadata_item_builder_chaining() {
1074 let mut metadata_item = MetadataItem::new("title", "EPUB 3.3 Guide");
1075 metadata_item.with_id("title").with_lang("en");
1076
1077 let refinement = MetadataRefinement::new("title", "title-type", "main");
1078 metadata_item.append_refinement(refinement);
1079
1080 let built = metadata_item.build();
1081
1082 assert_eq!(built.property, "title");
1083 assert_eq!(built.value, "EPUB 3.3 Guide");
1084 assert_eq!(built.id, Some("title".to_string()));
1085 assert_eq!(built.lang, Some("en".to_string()));
1086 assert_eq!(built.refined.len(), 1);
1087 }
1088
1089 #[test]
1090 fn test_metadata_item_attributes_dc_namespace() {
1091 let mut metadata_item = MetadataItem::new("title", "Test Book");
1092 metadata_item.with_id("title-id");
1093
1094 let attributes = metadata_item.attributes();
1095
1096 assert!(!attributes.iter().any(|(k, _)| k == &"property"));
1098 assert!(
1099 attributes
1100 .iter()
1101 .any(|(k, v)| k == &"id" && v == &"title-id")
1102 );
1103 }
1104
1105 #[test]
1106 fn test_metadata_item_attributes_non_dc_namespace() {
1107 let mut metadata_item = MetadataItem::new("meta", "value");
1108 metadata_item.with_id("meta-id");
1109
1110 let attributes = metadata_item.attributes();
1111
1112 assert!(attributes.iter().any(|(k, _)| k == &"property"));
1114 assert!(
1115 attributes
1116 .iter()
1117 .any(|(k, v)| k == &"id" && v == &"meta-id")
1118 );
1119 }
1120
1121 #[test]
1122 fn test_metadata_item_attributes_with_lang() {
1123 let mut metadata_item = MetadataItem::new("title", "Test Book");
1124 metadata_item.with_id("title-id").with_lang("en");
1125
1126 let attributes = metadata_item.attributes();
1127
1128 assert!(
1129 attributes
1130 .iter()
1131 .any(|(k, v)| k == &"id" && v == &"title-id")
1132 );
1133 assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1134 }
1135 }
1136
1137 mod metadata_refinement {
1138 use crate::types::MetadataRefinement;
1139
1140 #[test]
1141 fn test_metadata_refinement_new() {
1142 let refinement = MetadataRefinement::new("title", "title-type", "main");
1143
1144 assert_eq!(refinement.refines, "title");
1145 assert_eq!(refinement.property, "title-type");
1146 assert_eq!(refinement.value, "main");
1147 assert_eq!(refinement.lang, None);
1148 assert_eq!(refinement.scheme, None);
1149 }
1150
1151 #[test]
1152 fn test_metadata_refinement_with_lang() {
1153 let mut refinement = MetadataRefinement::new("creator", "role", "author");
1154 refinement.with_lang("en");
1155
1156 assert_eq!(refinement.refines, "creator");
1157 assert_eq!(refinement.property, "role");
1158 assert_eq!(refinement.value, "author");
1159 assert_eq!(refinement.lang, Some("en".to_string()));
1160 assert_eq!(refinement.scheme, None);
1161 }
1162
1163 #[test]
1164 fn test_metadata_refinement_with_scheme() {
1165 let mut refinement = MetadataRefinement::new("creator", "role", "author");
1166 refinement.with_scheme("marc:relators");
1167
1168 assert_eq!(refinement.refines, "creator");
1169 assert_eq!(refinement.property, "role");
1170 assert_eq!(refinement.value, "author");
1171 assert_eq!(refinement.lang, None);
1172 assert_eq!(refinement.scheme, Some("marc:relators".to_string()));
1173 }
1174
1175 #[test]
1176 fn test_metadata_refinement_build() {
1177 let mut refinement = MetadataRefinement::new("title", "alternate-script", "テスト");
1178 refinement.with_lang("ja").with_scheme("iso-15924");
1179
1180 let built = refinement.build();
1181
1182 assert_eq!(built.refines, "title");
1183 assert_eq!(built.property, "alternate-script");
1184 assert_eq!(built.value, "テスト");
1185 assert_eq!(built.lang, Some("ja".to_string()));
1186 assert_eq!(built.scheme, Some("iso-15924".to_string()));
1187 }
1188
1189 #[test]
1190 fn test_metadata_refinement_builder_chaining() {
1191 let mut refinement = MetadataRefinement::new("creator", "file-as", "Doe, John");
1192 refinement.with_lang("en").with_scheme("dcterms");
1193
1194 let built = refinement.build();
1195
1196 assert_eq!(built.refines, "creator");
1197 assert_eq!(built.property, "file-as");
1198 assert_eq!(built.value, "Doe, John");
1199 assert_eq!(built.lang, Some("en".to_string()));
1200 assert_eq!(built.scheme, Some("dcterms".to_string()));
1201 }
1202
1203 #[test]
1204 fn test_metadata_refinement_attributes() {
1205 let mut refinement = MetadataRefinement::new("title", "title-type", "main");
1206 refinement.with_lang("en").with_scheme("onix:codelist5");
1207
1208 let attributes = refinement.attributes();
1209
1210 assert!(
1211 attributes
1212 .iter()
1213 .any(|(k, v)| k == &"refines" && v == &"title")
1214 );
1215 assert!(
1216 attributes
1217 .iter()
1218 .any(|(k, v)| k == &"property" && v == &"title-type")
1219 );
1220 assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1221 assert!(
1222 attributes
1223 .iter()
1224 .any(|(k, v)| k == &"scheme" && v == &"onix:codelist5")
1225 );
1226 }
1227
1228 #[test]
1229 fn test_metadata_refinement_attributes_optional_fields() {
1230 let refinement = MetadataRefinement::new("creator", "role", "author");
1231 let attributes = refinement.attributes();
1232
1233 assert!(
1234 attributes
1235 .iter()
1236 .any(|(k, v)| k == &"refines" && v == &"creator")
1237 );
1238 assert!(
1239 attributes
1240 .iter()
1241 .any(|(k, v)| k == &"property" && v == &"role")
1242 );
1243
1244 assert!(!attributes.iter().any(|(k, _)| k == &"lang"));
1246 assert!(!attributes.iter().any(|(k, _)| k == &"scheme"));
1247 }
1248 }
1249
1250 mod manifest_item {
1251 use std::path::PathBuf;
1252
1253 use crate::types::ManifestItem;
1254
1255 #[test]
1256 fn test_manifest_item_new() {
1257 let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1258 assert!(manifest_item.is_ok());
1259
1260 let manifest_item = manifest_item.unwrap();
1261 assert_eq!(manifest_item.id, "cover");
1262 assert_eq!(manifest_item.path, PathBuf::from("images/cover.jpg"));
1263 assert_eq!(manifest_item.mime, "");
1264 assert_eq!(manifest_item.properties, None);
1265 assert_eq!(manifest_item.fallback, None);
1266 }
1267
1268 #[test]
1269 fn test_manifest_item_append_property() {
1270 let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1271 assert!(manifest_item.is_ok());
1272
1273 let mut manifest_item = manifest_item.unwrap();
1274 manifest_item.append_property("nav");
1275
1276 assert_eq!(manifest_item.id, "nav");
1277 assert_eq!(manifest_item.path, PathBuf::from("nav.xhtml"));
1278 assert_eq!(manifest_item.mime, "");
1279 assert_eq!(manifest_item.properties, Some("nav".to_string()));
1280 assert_eq!(manifest_item.fallback, None);
1281 }
1282
1283 #[test]
1284 fn test_manifest_item_append_multiple_properties() {
1285 let manifest_item = ManifestItem::new("content", "content.xhtml");
1286 assert!(manifest_item.is_ok());
1287
1288 let mut manifest_item = manifest_item.unwrap();
1289 manifest_item
1290 .append_property("nav")
1291 .append_property("scripted")
1292 .append_property("svg");
1293
1294 assert_eq!(
1295 manifest_item.properties,
1296 Some("nav scripted svg".to_string())
1297 );
1298 }
1299
1300 #[test]
1301 fn test_manifest_item_with_fallback() {
1302 let manifest_item = ManifestItem::new("image", "image.tiff");
1303 assert!(manifest_item.is_ok());
1304
1305 let mut manifest_item = manifest_item.unwrap();
1306 manifest_item.with_fallback("image-fallback");
1307
1308 assert_eq!(manifest_item.id, "image");
1309 assert_eq!(manifest_item.path, PathBuf::from("image.tiff"));
1310 assert_eq!(manifest_item.mime, "");
1311 assert_eq!(manifest_item.properties, None);
1312 assert_eq!(manifest_item.fallback, Some("image-fallback".to_string()));
1313 }
1314
1315 #[test]
1316 fn test_manifest_item_set_mime() {
1317 let manifest_item = ManifestItem::new("style", "style.css");
1318 assert!(manifest_item.is_ok());
1319
1320 let manifest_item = manifest_item.unwrap();
1321 let updated_item = manifest_item.set_mime("text/css");
1322
1323 assert_eq!(updated_item.id, "style");
1324 assert_eq!(updated_item.path, PathBuf::from("style.css"));
1325 assert_eq!(updated_item.mime, "text/css");
1326 assert_eq!(updated_item.properties, None);
1327 assert_eq!(updated_item.fallback, None);
1328 }
1329
1330 #[test]
1331 fn test_manifest_item_build() {
1332 let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1333 assert!(manifest_item.is_ok());
1334
1335 let mut manifest_item = manifest_item.unwrap();
1336 manifest_item
1337 .append_property("cover-image")
1338 .with_fallback("cover-fallback");
1339
1340 let built = manifest_item.build();
1341
1342 assert_eq!(built.id, "cover");
1343 assert_eq!(built.path, PathBuf::from("images/cover.jpg"));
1344 assert_eq!(built.mime, "");
1345 assert_eq!(built.properties, Some("cover-image".to_string()));
1346 assert_eq!(built.fallback, Some("cover-fallback".to_string()));
1347 }
1348
1349 #[test]
1350 fn test_manifest_item_builder_chaining() {
1351 let manifest_item = ManifestItem::new("content", "content.xhtml");
1352 assert!(manifest_item.is_ok());
1353
1354 let mut manifest_item = manifest_item.unwrap();
1355 manifest_item
1356 .append_property("scripted")
1357 .append_property("svg")
1358 .with_fallback("fallback-content");
1359
1360 let built = manifest_item.build();
1361
1362 assert_eq!(built.id, "content");
1363 assert_eq!(built.path, PathBuf::from("content.xhtml"));
1364 assert_eq!(built.mime, "");
1365 assert_eq!(built.properties, Some("scripted svg".to_string()));
1366 assert_eq!(built.fallback, Some("fallback-content".to_string()));
1367 }
1368
1369 #[test]
1370 fn test_manifest_item_attributes() {
1371 let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1372 assert!(manifest_item.is_ok());
1373
1374 let mut manifest_item = manifest_item.unwrap();
1375 manifest_item
1376 .append_property("nav")
1377 .with_fallback("fallback-nav");
1378
1379 let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1381 let attributes = manifest_item.attributes();
1382
1383 assert!(attributes.contains(&("id", "nav")));
1385 assert!(attributes.contains(&("href", "nav.xhtml")));
1386 assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1387 assert!(attributes.contains(&("properties", "nav")));
1388 assert!(attributes.contains(&("fallback", "fallback-nav")));
1389 }
1390
1391 #[test]
1392 fn test_manifest_item_attributes_optional_fields() {
1393 let manifest_item = ManifestItem::new("simple", "simple.xhtml");
1394 assert!(manifest_item.is_ok());
1395
1396 let manifest_item = manifest_item.unwrap();
1397 let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1398 let attributes = manifest_item.attributes();
1399
1400 assert!(attributes.contains(&("id", "simple")));
1402 assert!(attributes.contains(&("href", "simple.xhtml")));
1403 assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1404
1405 assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1407 assert!(!attributes.iter().any(|(k, _)| k == &"fallback"));
1408 }
1409
1410 #[test]
1411 fn test_manifest_item_path_handling() {
1412 let manifest_item = ManifestItem::new("test", "../images/test.png");
1413 assert!(manifest_item.is_err());
1414
1415 let err = manifest_item.unwrap_err();
1416 assert_eq!(
1417 err.to_string(),
1418 "Epub builder error: A manifest with id 'test' should not use a relative path starting with '../'."
1419 );
1420 }
1421 }
1422
1423 mod spine_item {
1424 use crate::types::SpineItem;
1425
1426 #[test]
1427 fn test_spine_item_new() {
1428 let spine_item = SpineItem::new("content_001");
1429
1430 assert_eq!(spine_item.idref, "content_001");
1431 assert_eq!(spine_item.id, None);
1432 assert_eq!(spine_item.properties, None);
1433 assert_eq!(spine_item.linear, true);
1434 }
1435
1436 #[test]
1437 fn test_spine_item_with_id() {
1438 let mut spine_item = SpineItem::new("content_001");
1439 spine_item.with_id("spine1");
1440
1441 assert_eq!(spine_item.idref, "content_001");
1442 assert_eq!(spine_item.id, Some("spine1".to_string()));
1443 assert_eq!(spine_item.properties, None);
1444 assert_eq!(spine_item.linear, true);
1445 }
1446
1447 #[test]
1448 fn test_spine_item_append_property() {
1449 let mut spine_item = SpineItem::new("content_001");
1450 spine_item.append_property("page-spread-left");
1451
1452 assert_eq!(spine_item.idref, "content_001");
1453 assert_eq!(spine_item.id, None);
1454 assert_eq!(spine_item.properties, Some("page-spread-left".to_string()));
1455 assert_eq!(spine_item.linear, true);
1456 }
1457
1458 #[test]
1459 fn test_spine_item_append_multiple_properties() {
1460 let mut spine_item = SpineItem::new("content_001");
1461 spine_item
1462 .append_property("page-spread-left")
1463 .append_property("rendition:layout-pre-paginated");
1464
1465 assert_eq!(
1466 spine_item.properties,
1467 Some("page-spread-left rendition:layout-pre-paginated".to_string())
1468 );
1469 }
1470
1471 #[test]
1472 fn test_spine_item_set_linear() {
1473 let mut spine_item = SpineItem::new("content_001");
1474 spine_item.set_linear(false);
1475
1476 assert_eq!(spine_item.idref, "content_001");
1477 assert_eq!(spine_item.id, None);
1478 assert_eq!(spine_item.properties, None);
1479 assert_eq!(spine_item.linear, false);
1480 }
1481
1482 #[test]
1483 fn test_spine_item_build() {
1484 let mut spine_item = SpineItem::new("content_001");
1485 spine_item
1486 .with_id("spine1")
1487 .append_property("page-spread-left")
1488 .set_linear(false);
1489
1490 let built = spine_item.build();
1491
1492 assert_eq!(built.idref, "content_001");
1493 assert_eq!(built.id, Some("spine1".to_string()));
1494 assert_eq!(built.properties, Some("page-spread-left".to_string()));
1495 assert_eq!(built.linear, false);
1496 }
1497
1498 #[test]
1499 fn test_spine_item_builder_chaining() {
1500 let mut spine_item = SpineItem::new("content_001");
1501 spine_item
1502 .with_id("spine1")
1503 .append_property("page-spread-left")
1504 .set_linear(false);
1505
1506 let built = spine_item.build();
1507
1508 assert_eq!(built.idref, "content_001");
1509 assert_eq!(built.id, Some("spine1".to_string()));
1510 assert_eq!(built.properties, Some("page-spread-left".to_string()));
1511 assert_eq!(built.linear, false);
1512 }
1513
1514 #[test]
1515 fn test_spine_item_attributes() {
1516 let mut spine_item = SpineItem::new("content_001");
1517 spine_item
1518 .with_id("spine1")
1519 .append_property("page-spread-left")
1520 .set_linear(false);
1521
1522 let attributes = spine_item.attributes();
1523
1524 assert!(attributes.contains(&("idref", "content_001")));
1526 assert!(attributes.contains(&("id", "spine1")));
1527 assert!(attributes.contains(&("properties", "page-spread-left")));
1528 assert!(attributes.contains(&("linear", "no"))); }
1530
1531 #[test]
1532 fn test_spine_item_attributes_linear_yes() {
1533 let spine_item = SpineItem::new("content_001");
1534 let attributes = spine_item.attributes();
1535
1536 assert!(attributes.contains(&("linear", "yes")));
1538 }
1539
1540 #[test]
1541 fn test_spine_item_attributes_optional_fields() {
1542 let spine_item = SpineItem::new("content_001");
1543 let attributes = spine_item.attributes();
1544
1545 assert!(attributes.contains(&("idref", "content_001")));
1547 assert!(attributes.contains(&("linear", "yes")));
1548
1549 assert!(!attributes.iter().any(|(k, _)| k == &"id"));
1551 assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1552 }
1553 }
1554
1555 mod navpoint {
1556
1557 use std::path::PathBuf;
1558
1559 use crate::types::NavPoint;
1560
1561 #[test]
1562 fn test_navpoint_new() {
1563 let navpoint = NavPoint::new("Test Chapter");
1564
1565 assert_eq!(navpoint.label, "Test Chapter");
1566 assert_eq!(navpoint.content, None);
1567 assert_eq!(navpoint.children.len(), 0);
1568 }
1569
1570 #[test]
1571 fn test_navpoint_with_content() {
1572 let mut navpoint = NavPoint::new("Test Chapter");
1573 navpoint.with_content("chapter1.html");
1574
1575 assert_eq!(navpoint.label, "Test Chapter");
1576 assert_eq!(navpoint.content, Some(PathBuf::from("chapter1.html")));
1577 assert_eq!(navpoint.children.len(), 0);
1578 }
1579
1580 #[test]
1581 fn test_navpoint_append_child() {
1582 let mut parent = NavPoint::new("Parent Chapter");
1583
1584 let mut child1 = NavPoint::new("Child Section 1");
1585 child1.with_content("section1.html");
1586
1587 let mut child2 = NavPoint::new("Child Section 2");
1588 child2.with_content("section2.html");
1589
1590 parent.append_child(child1.build());
1591 parent.append_child(child2.build());
1592
1593 assert_eq!(parent.children.len(), 2);
1594 assert_eq!(parent.children[0].label, "Child Section 1");
1595 assert_eq!(parent.children[1].label, "Child Section 2");
1596 }
1597
1598 #[test]
1599 fn test_navpoint_set_children() {
1600 let mut navpoint = NavPoint::new("Main Chapter");
1601 let children = vec![NavPoint::new("Section 1"), NavPoint::new("Section 2")];
1602
1603 navpoint.set_children(children);
1604
1605 assert_eq!(navpoint.children.len(), 2);
1606 assert_eq!(navpoint.children[0].label, "Section 1");
1607 assert_eq!(navpoint.children[1].label, "Section 2");
1608 }
1609
1610 #[test]
1611 fn test_navpoint_build() {
1612 let mut navpoint = NavPoint::new("Complete Chapter");
1613 navpoint.with_content("complete.html");
1614
1615 let child = NavPoint::new("Sub Section");
1616 navpoint.append_child(child.build());
1617
1618 let built = navpoint.build();
1619
1620 assert_eq!(built.label, "Complete Chapter");
1621 assert_eq!(built.content, Some(PathBuf::from("complete.html")));
1622 assert_eq!(built.children.len(), 1);
1623 assert_eq!(built.children[0].label, "Sub Section");
1624 }
1625
1626 #[test]
1627 fn test_navpoint_builder_chaining() {
1628 let mut navpoint = NavPoint::new("Chained Chapter");
1629
1630 navpoint
1631 .with_content("chained.html")
1632 .append_child(NavPoint::new("Child 1").build())
1633 .append_child(NavPoint::new("Child 2").build());
1634
1635 let built = navpoint.build();
1636
1637 assert_eq!(built.label, "Chained Chapter");
1638 assert_eq!(built.content, Some(PathBuf::from("chained.html")));
1639 assert_eq!(built.children.len(), 2);
1640 }
1641
1642 #[test]
1643 fn test_navpoint_empty_children() {
1644 let navpoint = NavPoint::new("No Children Chapter");
1645 let built = navpoint.build();
1646
1647 assert_eq!(built.children.len(), 0);
1648 }
1649
1650 #[test]
1651 fn test_navpoint_complex_hierarchy() {
1652 let mut root = NavPoint::new("Book");
1653
1654 let mut chapter1 = NavPoint::new("Chapter 1");
1655 chapter1
1656 .with_content("chapter1.html")
1657 .append_child(
1658 NavPoint::new("Section 1.1")
1659 .with_content("sec1_1.html")
1660 .build(),
1661 )
1662 .append_child(
1663 NavPoint::new("Section 1.2")
1664 .with_content("sec1_2.html")
1665 .build(),
1666 );
1667
1668 let mut chapter2 = NavPoint::new("Chapter 2");
1669 chapter2.with_content("chapter2.html").append_child(
1670 NavPoint::new("Section 2.1")
1671 .with_content("sec2_1.html")
1672 .build(),
1673 );
1674
1675 root.append_child(chapter1.build())
1676 .append_child(chapter2.build());
1677
1678 let book = root.build();
1679
1680 assert_eq!(book.label, "Book");
1681 assert_eq!(book.children.len(), 2);
1682
1683 let ch1 = &book.children[0];
1684 assert_eq!(ch1.label, "Chapter 1");
1685 assert_eq!(ch1.children.len(), 2);
1686
1687 let ch2 = &book.children[1];
1688 assert_eq!(ch2.label, "Chapter 2");
1689 assert_eq!(ch2.children.len(), 1);
1690 }
1691 }
1692 }
1693
1694 mod footnote_tests {
1695 use crate::types::Footnote;
1696
1697 #[test]
1698 fn test_footnote_basic_creation() {
1699 let footnote = Footnote {
1700 locate: 100,
1701 content: "Sample footnote".to_string(),
1702 };
1703
1704 assert_eq!(footnote.locate, 100);
1705 assert_eq!(footnote.content, "Sample footnote");
1706 }
1707
1708 #[test]
1709 fn test_footnote_equality() {
1710 let footnote1 = Footnote {
1711 locate: 100,
1712 content: "First note".to_string(),
1713 };
1714
1715 let footnote2 = Footnote {
1716 locate: 100,
1717 content: "First note".to_string(),
1718 };
1719
1720 let footnote3 = Footnote {
1721 locate: 100,
1722 content: "Different note".to_string(),
1723 };
1724
1725 let footnote4 = Footnote {
1726 locate: 200,
1727 content: "First note".to_string(),
1728 };
1729
1730 assert_eq!(footnote1, footnote2);
1731 assert_ne!(footnote1, footnote3);
1732 assert_ne!(footnote1, footnote4);
1733 }
1734
1735 #[test]
1736 fn test_footnote_ordering() {
1737 let footnote1 = Footnote {
1738 locate: 100,
1739 content: "First".to_string(),
1740 };
1741
1742 let footnote2 = Footnote {
1743 locate: 200,
1744 content: "Second".to_string(),
1745 };
1746
1747 let footnote3 = Footnote {
1748 locate: 150,
1749 content: "Middle".to_string(),
1750 };
1751
1752 assert!(footnote1 < footnote2);
1753 assert!(footnote2 > footnote1);
1754 assert!(footnote1 < footnote3);
1755 assert!(footnote3 < footnote2);
1756 assert_eq!(footnote1.cmp(&footnote1), std::cmp::Ordering::Equal);
1757 }
1758
1759 #[test]
1760 fn test_footnote_sorting() {
1761 let mut footnotes = vec![
1762 Footnote {
1763 locate: 300,
1764 content: "Third note".to_string(),
1765 },
1766 Footnote {
1767 locate: 100,
1768 content: "First note".to_string(),
1769 },
1770 Footnote {
1771 locate: 200,
1772 content: "Second note".to_string(),
1773 },
1774 ];
1775
1776 footnotes.sort();
1777
1778 assert_eq!(footnotes[0].locate, 100);
1779 assert_eq!(footnotes[1].locate, 200);
1780 assert_eq!(footnotes[2].locate, 300);
1781
1782 assert_eq!(footnotes[0].content, "First note");
1783 assert_eq!(footnotes[1].content, "Second note");
1784 assert_eq!(footnotes[2].content, "Third note");
1785 }
1786 }
1787}