1use std::path::PathBuf;
18
19#[cfg(feature = "builder")]
20use crate::{
21 error::{EpubBuilderError, EpubError},
22 utils::ELEMENT_IN_DC_NAMESPACE,
23};
24
25#[derive(Debug, PartialEq, Eq)]
29pub enum EpubVersion {
30 Version2_0,
31 Version3_0,
32}
33
34#[derive(Debug, Clone)]
59pub struct MetadataItem {
60 pub id: Option<String>,
65
66 pub property: String,
72
73 pub value: String,
75
76 pub lang: Option<String>,
78
79 pub refined: Vec<MetadataRefinement>,
87}
88
89#[cfg(feature = "builder")]
90impl MetadataItem {
91 pub fn new(property: &str, value: &str) -> Self {
99 Self {
100 id: None,
101 property: property.to_string(),
102 value: value.to_string(),
103 lang: None,
104 refined: vec![],
105 }
106 }
107
108 pub fn with_id(&mut self, id: &str) -> &mut Self {
115 self.id = Some(id.to_string());
116 self
117 }
118
119 pub fn with_lang(&mut self, lang: &str) -> &mut Self {
126 self.lang = Some(lang.to_string());
127 self
128 }
129
130 pub fn append_refinement(&mut self, refine: MetadataRefinement) -> &mut Self {
140 if self.id.is_some() {
141 self.refined.push(refine);
142 } else {
143 }
145
146 self
147 }
148
149 pub fn build(&self) -> Self {
153 Self { ..self.clone() }
154 }
155
156 pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
158 let mut attributes = Vec::new();
159
160 if !ELEMENT_IN_DC_NAMESPACE.contains(&self.property.as_str()) {
161 attributes.push(("property", self.property.as_str()));
162 }
163
164 if let Some(id) = &self.id {
165 attributes.push(("id", id.as_str()));
166 };
167
168 if let Some(lang) = &self.lang {
169 attributes.push(("lang", lang.as_str()));
170 };
171
172 attributes
173 }
174}
175
176#[derive(Debug, Clone)]
200pub struct MetadataRefinement {
201 pub refines: String,
202
203 pub property: String,
208
209 pub value: String,
211
212 pub lang: Option<String>,
214
215 pub scheme: Option<String>,
220}
221
222#[cfg(feature = "builder")]
223impl MetadataRefinement {
224 pub fn new(refines: &str, property: &str, value: &str) -> Self {
233 Self {
234 refines: refines.to_string(),
235 property: property.to_string(),
236 value: value.to_string(),
237 lang: None,
238 scheme: None,
239 }
240 }
241
242 pub fn with_lang(&mut self, lang: &str) -> &mut Self {
249 self.lang = Some(lang.to_string());
250 self
251 }
252
253 pub fn with_scheme(&mut self, scheme: &str) -> &mut Self {
260 self.scheme = Some(scheme.to_string());
261 self
262 }
263
264 pub fn build(&self) -> Self {
268 Self { ..self.clone() }
269 }
270
271 pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
273 let mut attributes = Vec::new();
274
275 attributes.push(("refines", self.refines.as_str()));
276 attributes.push(("property", self.property.as_str()));
277
278 if let Some(lang) = &self.lang {
279 attributes.push(("lang", lang.as_str()));
280 };
281
282 if let Some(scheme) = &self.scheme {
283 attributes.push(("scheme", scheme.as_str()));
284 };
285
286 attributes
287 }
288}
289
290#[derive(Debug)]
299pub struct MetadataLinkItem {
300 pub href: String,
302
303 pub rel: String,
305
306 pub hreflang: Option<String>,
308
309 pub id: Option<String>,
313
314 pub mime: Option<String>,
316
317 pub properties: Option<String>,
322
323 pub refines: Option<String>,
328}
329
330#[derive(Debug, Clone)]
361pub struct ManifestItem {
362 pub id: String,
364
365 pub path: PathBuf,
371
372 pub mime: String,
374
375 pub properties: Option<String>,
381
382 pub fallback: Option<String>,
393}
394
395#[cfg(feature = "builder")]
396impl ManifestItem {
397 pub fn new(id: &str, path: &str) -> Result<Self, EpubError> {
408 if path.starts_with("../") {
409 return Err(
410 EpubBuilderError::IllegalManifestPath { manifest_id: id.to_string() }.into(),
411 );
412 }
413
414 Ok(Self {
415 id: id.to_string(),
416 path: PathBuf::from(path),
417 mime: String::new(),
418 properties: None,
419 fallback: None,
420 })
421 }
422
423 pub(crate) fn set_mime(self, mime: &str) -> Self {
425 Self {
426 id: self.id,
427 path: self.path,
428 mime: mime.to_string(),
429 properties: self.properties,
430 fallback: self.fallback,
431 }
432 }
433
434 pub fn append_property(&mut self, property: &str) -> &mut Self {
441 let new_properties = if let Some(properties) = &self.properties {
442 format!("{} {}", properties, property)
443 } else {
444 property.to_string()
445 };
446
447 self.properties = Some(new_properties);
448 self
449 }
450
451 pub fn with_fallback(&mut self, fallback: &str) -> &mut Self {
458 self.fallback = Some(fallback.to_string());
459 self
460 }
461
462 pub fn build(&self) -> Self {
466 Self { ..self.clone() }
467 }
468
469 pub fn attributes(&self) -> Vec<(&str, &str)> {
471 let mut attributes = Vec::new();
472
473 attributes.push(("id", self.id.as_str()));
474 attributes.push(("href", self.path.to_str().unwrap()));
475 attributes.push(("media-type", self.mime.as_str()));
476
477 if let Some(properties) = &self.properties {
478 attributes.push(("properties", properties.as_str()));
479 }
480
481 if let Some(fallback) = &self.fallback {
482 attributes.push(("fallback", fallback.as_str()));
483 }
484
485 attributes
486 }
487}
488
489#[derive(Debug, Clone)]
516pub struct SpineItem {
517 pub idref: String,
523
524 pub id: Option<String>,
526
527 pub properties: Option<String>,
533
534 pub linear: bool,
544}
545
546#[cfg(feature = "builder")]
547impl SpineItem {
548 pub fn new(idref: &str) -> Self {
557 Self {
558 idref: idref.to_string(),
559 id: None,
560 properties: None,
561 linear: true,
562 }
563 }
564
565 pub fn with_id(&mut self, id: &str) -> &mut Self {
572 self.id = Some(id.to_string());
573 self
574 }
575
576 pub fn append_property(&mut self, property: &str) -> &mut Self {
583 let new_properties = if let Some(properties) = &self.properties {
584 format!("{} {}", properties, property)
585 } else {
586 property.to_string()
587 };
588
589 self.properties = Some(new_properties);
590 self
591 }
592
593 pub fn set_linear(&mut self, linear: bool) -> &mut Self {
600 self.linear = linear;
601 self
602 }
603
604 pub fn build(&self) -> Self {
608 Self { ..self.clone() }
609 }
610
611 pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
613 let mut attributes = Vec::new();
614
615 attributes.push(("idref", self.idref.as_str()));
616 attributes.push(("linear", if self.linear { "yes" } else { "no" }));
617
618 if let Some(id) = &self.id {
619 attributes.push(("id", id.as_str()));
620 }
621
622 if let Some(properties) = &self.properties {
623 attributes.push(("properties", properties.as_str()));
624 }
625
626 attributes
627 }
628}
629
630#[derive(Debug, Clone)]
636pub struct EncryptionData {
637 pub method: String,
644
645 pub data: String,
650}
651
652#[derive(Debug, Eq, Clone)]
677pub struct NavPoint {
678 pub label: String,
682
683 pub content: Option<PathBuf>,
688
689 pub children: Vec<NavPoint>,
691
692 pub play_order: Option<usize>,
697}
698
699#[cfg(feature = "builder")]
700impl NavPoint {
701 pub fn new(label: &str) -> Self {
708 Self {
709 label: label.to_string(),
710 content: None,
711 children: vec![],
712 play_order: None,
713 }
714 }
715
716 pub fn with_content(&mut self, content: &str) -> &mut Self {
723 self.content = Some(PathBuf::from(content));
724 self
725 }
726
727 pub fn append_child(&mut self, child: NavPoint) -> &mut Self {
734 self.children.push(child);
735 self
736 }
737
738 pub fn set_children(&mut self, children: Vec<NavPoint>) -> &mut Self {
745 self.children = children;
746 self
747 }
748
749 pub fn build(&self) -> Self {
753 Self { ..self.clone() }
754 }
755}
756
757impl Ord for NavPoint {
758 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
759 self.play_order.cmp(&other.play_order)
760 }
761}
762
763impl PartialOrd for NavPoint {
764 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
765 Some(self.cmp(other))
766 }
767}
768
769impl PartialEq for NavPoint {
770 fn eq(&self, other: &Self) -> bool {
771 self.play_order == other.play_order
772 }
773}
774
775#[cfg(feature = "content-builder")]
780#[derive(Debug, Clone, Eq, PartialEq)]
781pub struct Footnote {
782 pub locate: usize,
784
785 pub content: String,
787}
788
789#[cfg(feature = "content-builder")]
790impl Ord for Footnote {
791 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
792 self.locate.cmp(&other.locate)
793 }
794}
795
796#[cfg(feature = "content-builder")]
797impl PartialOrd for Footnote {
798 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
799 Some(self.cmp(other))
800 }
801}
802
803#[cfg(feature = "content-builder")]
805#[derive(Debug, Copy, Clone)]
806pub enum BlockType {
807 Text,
811
812 Quote,
817
818 Title,
822
823 Image,
827
828 Audio,
832
833 Video,
837
838 MathML,
843}
844
845#[cfg(feature = "content-builder")]
846impl std::fmt::Display for BlockType {
847 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
848 match self {
849 BlockType::Text => write!(f, "Text"),
850 BlockType::Quote => write!(f, "Quote"),
851 BlockType::Title => write!(f, "Title"),
852 BlockType::Image => write!(f, "Image"),
853 BlockType::Audio => write!(f, "Audio"),
854 BlockType::Video => write!(f, "Video"),
855 BlockType::MathML => write!(f, "MathML"),
856 }
857 }
858}
859
860#[cfg(feature = "content-builder")]
865#[derive(Debug, Default, Clone)]
866pub struct StyleOptions {
867 pub text: TextStyle,
869
870 pub color_scheme: ColorScheme,
874
875 pub layout: PageLayout,
879}
880
881#[cfg(feature = "content-builder")]
882#[cfg(feature = "builder")]
883impl StyleOptions {
884 pub fn new() -> Self {
886 Self::default()
887 }
888
889 pub fn with_text(&mut self, text: TextStyle) -> &mut Self {
891 self.text = text;
892 self
893 }
894
895 pub fn with_color_scheme(&mut self, color_scheme: ColorScheme) -> &mut Self {
897 self.color_scheme = color_scheme;
898 self
899 }
900
901 pub fn with_layout(&mut self, layout: PageLayout) -> &mut Self {
903 self.layout = layout;
904 self
905 }
906
907 pub fn build(&self) -> Self {
909 Self { ..self.clone() }
910 }
911}
912
913#[cfg(feature = "content-builder")]
918#[derive(Debug, Clone)]
919pub struct TextStyle {
920 pub font_size: f32,
925
926 pub line_height: f32,
932
933 pub font_family: String,
938
939 pub font_weight: String,
944
945 pub font_style: String,
950
951 pub letter_spacing: String,
956
957 pub text_indent: f32,
962}
963
964#[cfg(feature = "content-builder")]
965impl Default for TextStyle {
966 fn default() -> Self {
967 Self {
968 font_size: 1.0,
969 line_height: 1.6,
970 font_family: "-apple-system, Roboto, sans-serif".to_string(),
971 font_weight: "normal".to_string(),
972 font_style: "normal".to_string(),
973 letter_spacing: "normal".to_string(),
974 text_indent: 2.0,
975 }
976 }
977}
978
979#[cfg(feature = "content-builder")]
980impl TextStyle {
981 pub fn new() -> Self {
983 Self::default()
984 }
985
986 pub fn with_font_size(&mut self, font_size: f32) -> &mut Self {
988 self.font_size = font_size;
989 self
990 }
991
992 pub fn with_line_height(&mut self, line_height: f32) -> &mut Self {
994 self.line_height = line_height;
995 self
996 }
997
998 pub fn with_font_family(&mut self, font_family: &str) -> &mut Self {
1000 self.font_family = font_family.to_string();
1001 self
1002 }
1003
1004 pub fn with_font_weight(&mut self, font_weight: &str) -> &mut Self {
1006 self.font_weight = font_weight.to_string();
1007 self
1008 }
1009
1010 pub fn with_font_style(&mut self, font_style: &str) -> &mut Self {
1012 self.font_style = font_style.to_string();
1013 self
1014 }
1015
1016 pub fn with_letter_spacing(&mut self, letter_spacing: &str) -> &mut Self {
1018 self.letter_spacing = letter_spacing.to_string();
1019 self
1020 }
1021
1022 pub fn with_text_indent(&mut self, text_indent: f32) -> &mut Self {
1024 self.text_indent = text_indent;
1025 self
1026 }
1027
1028 pub fn build(&self) -> Self {
1030 Self { ..self.clone() }
1031 }
1032}
1033
1034#[cfg(feature = "content-builder")]
1039#[derive(Debug, Clone)]
1040pub struct ColorScheme {
1041 pub background: String,
1046
1047 pub text: String,
1052
1053 pub link: String,
1058}
1059
1060#[cfg(feature = "content-builder")]
1061impl Default for ColorScheme {
1062 fn default() -> Self {
1063 Self {
1064 background: "#FFFFFF".to_string(),
1065 text: "#000000".to_string(),
1066 link: "#6f6f6f".to_string(),
1067 }
1068 }
1069}
1070
1071#[cfg(feature = "content-builder")]
1072impl ColorScheme {
1073 pub fn new() -> Self {
1075 Self::default()
1076 }
1077
1078 pub fn with_background(&mut self, background: &str) -> &mut Self {
1080 self.background = background.to_string();
1081 self
1082 }
1083
1084 pub fn with_text(&mut self, text: &str) -> &mut Self {
1086 self.text = text.to_string();
1087 self
1088 }
1089
1090 pub fn with_link(&mut self, link: &str) -> &mut Self {
1092 self.link = link.to_string();
1093 self
1094 }
1095
1096 pub fn build(&self) -> Self {
1098 Self { ..self.clone() }
1099 }
1100}
1101
1102#[cfg(feature = "content-builder")]
1107#[derive(Debug, Clone)]
1108pub struct PageLayout {
1109 pub margin: usize,
1113
1114 pub text_align: TextAlign,
1118
1119 pub paragraph_spacing: usize,
1123}
1124
1125#[cfg(feature = "content-builder")]
1126impl Default for PageLayout {
1127 fn default() -> Self {
1128 Self {
1129 margin: 20,
1130 text_align: Default::default(),
1131 paragraph_spacing: 16,
1132 }
1133 }
1134}
1135
1136#[cfg(feature = "content-builder")]
1137impl PageLayout {
1138 pub fn new() -> Self {
1140 Self::default()
1141 }
1142
1143 pub fn with_margin(&mut self, margin: usize) -> &mut Self {
1145 self.margin = margin;
1146 self
1147 }
1148
1149 pub fn with_text_align(&mut self, text_align: TextAlign) -> &mut Self {
1151 self.text_align = text_align;
1152 self
1153 }
1154
1155 pub fn with_paragraph_spacing(&mut self, paragraph_spacing: usize) -> &mut Self {
1157 self.paragraph_spacing = paragraph_spacing;
1158 self
1159 }
1160
1161 pub fn build(&self) -> Self {
1163 Self { ..self.clone() }
1164 }
1165}
1166
1167#[cfg(feature = "content-builder")]
1171#[derive(Debug, Default, Clone, Copy, PartialEq)]
1172pub enum TextAlign {
1173 #[default]
1177 Left,
1178
1179 Right,
1183
1184 Justify,
1189
1190 Center,
1194}
1195
1196#[cfg(feature = "content-builder")]
1197impl std::fmt::Display for TextAlign {
1198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1199 match self {
1200 TextAlign::Left => write!(f, "left"),
1201 TextAlign::Right => write!(f, "right"),
1202 TextAlign::Justify => write!(f, "justify"),
1203 TextAlign::Center => write!(f, "center"),
1204 }
1205 }
1206}
1207
1208#[cfg(test)]
1209mod tests {
1210 mod navpoint_tests {
1211 use std::path::PathBuf;
1212
1213 use crate::types::NavPoint;
1214
1215 #[test]
1217 fn test_navpoint_partial_eq() {
1218 let nav1 = NavPoint {
1219 label: "Chapter 1".to_string(),
1220 content: Some(PathBuf::from("chapter1.html")),
1221 children: vec![],
1222 play_order: Some(1),
1223 };
1224
1225 let nav2 = NavPoint {
1226 label: "Chapter 1".to_string(),
1227 content: Some(PathBuf::from("chapter2.html")),
1228 children: vec![],
1229 play_order: Some(1),
1230 };
1231
1232 let nav3 = NavPoint {
1233 label: "Chapter 2".to_string(),
1234 content: Some(PathBuf::from("chapter1.html")),
1235 children: vec![],
1236 play_order: Some(2),
1237 };
1238
1239 assert_eq!(nav1, nav2); assert_ne!(nav1, nav3); }
1242
1243 #[test]
1245 fn test_navpoint_ord() {
1246 let nav1 = NavPoint {
1247 label: "Chapter 1".to_string(),
1248 content: Some(PathBuf::from("chapter1.html")),
1249 children: vec![],
1250 play_order: Some(1),
1251 };
1252
1253 let nav2 = NavPoint {
1254 label: "Chapter 2".to_string(),
1255 content: Some(PathBuf::from("chapter2.html")),
1256 children: vec![],
1257 play_order: Some(2),
1258 };
1259
1260 let nav3 = NavPoint {
1261 label: "Chapter 3".to_string(),
1262 content: Some(PathBuf::from("chapter3.html")),
1263 children: vec![],
1264 play_order: Some(3),
1265 };
1266
1267 assert!(nav1 < nav2);
1269 assert!(nav2 > nav1);
1270 assert!(nav1 == nav1);
1271
1272 assert_eq!(nav1.partial_cmp(&nav2), Some(std::cmp::Ordering::Less));
1274 assert_eq!(nav2.partial_cmp(&nav1), Some(std::cmp::Ordering::Greater));
1275 assert_eq!(nav1.partial_cmp(&nav1), Some(std::cmp::Ordering::Equal));
1276
1277 let mut nav_points = vec![nav2.clone(), nav3.clone(), nav1.clone()];
1279 nav_points.sort();
1280 assert_eq!(nav_points, vec![nav1, nav2, nav3]);
1281 }
1282
1283 #[test]
1285 fn test_navpoint_ord_with_none_play_order() {
1286 let nav_with_order = NavPoint {
1287 label: "Chapter 1".to_string(),
1288 content: Some(PathBuf::from("chapter1.html")),
1289 children: vec![],
1290 play_order: Some(1),
1291 };
1292
1293 let nav_without_order = NavPoint {
1294 label: "Preface".to_string(),
1295 content: Some(PathBuf::from("preface.html")),
1296 children: vec![],
1297 play_order: None,
1298 };
1299
1300 assert!(nav_without_order < nav_with_order);
1301 assert!(nav_with_order > nav_without_order);
1302
1303 let nav_without_order2 = NavPoint {
1304 label: "Introduction".to_string(),
1305 content: Some(PathBuf::from("intro.html")),
1306 children: vec![],
1307 play_order: None,
1308 };
1309
1310 assert!(nav_without_order == nav_without_order2);
1311 }
1312
1313 #[test]
1315 fn test_navpoint_with_children() {
1316 let child1 = NavPoint {
1317 label: "Section 1.1".to_string(),
1318 content: Some(PathBuf::from("section1_1.html")),
1319 children: vec![],
1320 play_order: Some(1),
1321 };
1322
1323 let child2 = NavPoint {
1324 label: "Section 1.2".to_string(),
1325 content: Some(PathBuf::from("section1_2.html")),
1326 children: vec![],
1327 play_order: Some(2),
1328 };
1329
1330 let parent1 = NavPoint {
1331 label: "Chapter 1".to_string(),
1332 content: Some(PathBuf::from("chapter1.html")),
1333 children: vec![child1.clone(), child2.clone()],
1334 play_order: Some(1),
1335 };
1336
1337 let parent2 = NavPoint {
1338 label: "Chapter 1".to_string(),
1339 content: Some(PathBuf::from("chapter1.html")),
1340 children: vec![child1.clone(), child2.clone()],
1341 play_order: Some(1),
1342 };
1343
1344 assert!(parent1 == parent2);
1345
1346 let parent3 = NavPoint {
1347 label: "Chapter 2".to_string(),
1348 content: Some(PathBuf::from("chapter2.html")),
1349 children: vec![child1.clone(), child2.clone()],
1350 play_order: Some(2),
1351 };
1352
1353 assert!(parent1 != parent3);
1354 assert!(parent1 < parent3);
1355 }
1356
1357 #[test]
1359 fn test_navpoint_with_none_content() {
1360 let nav1 = NavPoint {
1361 label: "Chapter 1".to_string(),
1362 content: None,
1363 children: vec![],
1364 play_order: Some(1),
1365 };
1366
1367 let nav2 = NavPoint {
1368 label: "Chapter 1".to_string(),
1369 content: None,
1370 children: vec![],
1371 play_order: Some(1),
1372 };
1373
1374 assert!(nav1 == nav2);
1375 }
1376 }
1377
1378 #[cfg(feature = "builder")]
1379 mod builder_tests {
1380 mod metadata_item {
1381 use crate::types::{MetadataItem, MetadataRefinement};
1382
1383 #[test]
1384 fn test_metadata_item_new() {
1385 let metadata_item = MetadataItem::new("title", "EPUB Test Book");
1386
1387 assert_eq!(metadata_item.property, "title");
1388 assert_eq!(metadata_item.value, "EPUB Test Book");
1389 assert_eq!(metadata_item.id, None);
1390 assert_eq!(metadata_item.lang, None);
1391 assert_eq!(metadata_item.refined.len(), 0);
1392 }
1393
1394 #[test]
1395 fn test_metadata_item_with_id() {
1396 let mut metadata_item = MetadataItem::new("creator", "John Doe");
1397 metadata_item.with_id("creator-1");
1398
1399 assert_eq!(metadata_item.property, "creator");
1400 assert_eq!(metadata_item.value, "John Doe");
1401 assert_eq!(metadata_item.id, Some("creator-1".to_string()));
1402 assert_eq!(metadata_item.lang, None);
1403 assert_eq!(metadata_item.refined.len(), 0);
1404 }
1405
1406 #[test]
1407 fn test_metadata_item_with_lang() {
1408 let mut metadata_item = MetadataItem::new("title", "测试书籍");
1409 metadata_item.with_lang("zh-CN");
1410
1411 assert_eq!(metadata_item.property, "title");
1412 assert_eq!(metadata_item.value, "测试书籍");
1413 assert_eq!(metadata_item.id, None);
1414 assert_eq!(metadata_item.lang, Some("zh-CN".to_string()));
1415 assert_eq!(metadata_item.refined.len(), 0);
1416 }
1417
1418 #[test]
1419 fn test_metadata_item_append_refinement() {
1420 let mut metadata_item = MetadataItem::new("creator", "John Doe");
1421 metadata_item.with_id("creator-1"); let refinement = MetadataRefinement::new("creator-1", "role", "author");
1424 metadata_item.append_refinement(refinement);
1425
1426 assert_eq!(metadata_item.refined.len(), 1);
1427 assert_eq!(metadata_item.refined[0].refines, "creator-1");
1428 assert_eq!(metadata_item.refined[0].property, "role");
1429 assert_eq!(metadata_item.refined[0].value, "author");
1430 }
1431
1432 #[test]
1433 fn test_metadata_item_append_refinement_without_id() {
1434 let mut metadata_item = MetadataItem::new("title", "Test Book");
1435 let refinement = MetadataRefinement::new("title", "title-type", "main");
1438 metadata_item.append_refinement(refinement);
1439
1440 assert_eq!(metadata_item.refined.len(), 0);
1442 }
1443
1444 #[test]
1445 fn test_metadata_item_build() {
1446 let mut metadata_item = MetadataItem::new("identifier", "urn:isbn:1234567890");
1447 metadata_item.with_id("pub-id").with_lang("en");
1448
1449 let built = metadata_item.build();
1450
1451 assert_eq!(built.property, "identifier");
1452 assert_eq!(built.value, "urn:isbn:1234567890");
1453 assert_eq!(built.id, Some("pub-id".to_string()));
1454 assert_eq!(built.lang, Some("en".to_string()));
1455 assert_eq!(built.refined.len(), 0);
1456 }
1457
1458 #[test]
1459 fn test_metadata_item_builder_chaining() {
1460 let mut metadata_item = MetadataItem::new("title", "EPUB 3.3 Guide");
1461 metadata_item.with_id("title").with_lang("en");
1462
1463 let refinement = MetadataRefinement::new("title", "title-type", "main");
1464 metadata_item.append_refinement(refinement);
1465
1466 let built = metadata_item.build();
1467
1468 assert_eq!(built.property, "title");
1469 assert_eq!(built.value, "EPUB 3.3 Guide");
1470 assert_eq!(built.id, Some("title".to_string()));
1471 assert_eq!(built.lang, Some("en".to_string()));
1472 assert_eq!(built.refined.len(), 1);
1473 }
1474
1475 #[test]
1476 fn test_metadata_item_attributes_dc_namespace() {
1477 let mut metadata_item = MetadataItem::new("title", "Test Book");
1478 metadata_item.with_id("title-id");
1479
1480 let attributes = metadata_item.attributes();
1481
1482 assert!(!attributes.iter().any(|(k, _)| k == &"property"));
1484 assert!(
1485 attributes
1486 .iter()
1487 .any(|(k, v)| k == &"id" && v == &"title-id")
1488 );
1489 }
1490
1491 #[test]
1492 fn test_metadata_item_attributes_non_dc_namespace() {
1493 let mut metadata_item = MetadataItem::new("meta", "value");
1494 metadata_item.with_id("meta-id");
1495
1496 let attributes = metadata_item.attributes();
1497
1498 assert!(attributes.iter().any(|(k, _)| k == &"property"));
1500 assert!(
1501 attributes
1502 .iter()
1503 .any(|(k, v)| k == &"id" && v == &"meta-id")
1504 );
1505 }
1506
1507 #[test]
1508 fn test_metadata_item_attributes_with_lang() {
1509 let mut metadata_item = MetadataItem::new("title", "Test Book");
1510 metadata_item.with_id("title-id").with_lang("en");
1511
1512 let attributes = metadata_item.attributes();
1513
1514 assert!(
1515 attributes
1516 .iter()
1517 .any(|(k, v)| k == &"id" && v == &"title-id")
1518 );
1519 assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1520 }
1521 }
1522
1523 mod metadata_refinement {
1524 use crate::types::MetadataRefinement;
1525
1526 #[test]
1527 fn test_metadata_refinement_new() {
1528 let refinement = MetadataRefinement::new("title", "title-type", "main");
1529
1530 assert_eq!(refinement.refines, "title");
1531 assert_eq!(refinement.property, "title-type");
1532 assert_eq!(refinement.value, "main");
1533 assert_eq!(refinement.lang, None);
1534 assert_eq!(refinement.scheme, None);
1535 }
1536
1537 #[test]
1538 fn test_metadata_refinement_with_lang() {
1539 let mut refinement = MetadataRefinement::new("creator", "role", "author");
1540 refinement.with_lang("en");
1541
1542 assert_eq!(refinement.refines, "creator");
1543 assert_eq!(refinement.property, "role");
1544 assert_eq!(refinement.value, "author");
1545 assert_eq!(refinement.lang, Some("en".to_string()));
1546 assert_eq!(refinement.scheme, None);
1547 }
1548
1549 #[test]
1550 fn test_metadata_refinement_with_scheme() {
1551 let mut refinement = MetadataRefinement::new("creator", "role", "author");
1552 refinement.with_scheme("marc:relators");
1553
1554 assert_eq!(refinement.refines, "creator");
1555 assert_eq!(refinement.property, "role");
1556 assert_eq!(refinement.value, "author");
1557 assert_eq!(refinement.lang, None);
1558 assert_eq!(refinement.scheme, Some("marc:relators".to_string()));
1559 }
1560
1561 #[test]
1562 fn test_metadata_refinement_build() {
1563 let mut refinement = MetadataRefinement::new("title", "alternate-script", "テスト");
1564 refinement.with_lang("ja").with_scheme("iso-15924");
1565
1566 let built = refinement.build();
1567
1568 assert_eq!(built.refines, "title");
1569 assert_eq!(built.property, "alternate-script");
1570 assert_eq!(built.value, "テスト");
1571 assert_eq!(built.lang, Some("ja".to_string()));
1572 assert_eq!(built.scheme, Some("iso-15924".to_string()));
1573 }
1574
1575 #[test]
1576 fn test_metadata_refinement_builder_chaining() {
1577 let mut refinement = MetadataRefinement::new("creator", "file-as", "Doe, John");
1578 refinement.with_lang("en").with_scheme("dcterms");
1579
1580 let built = refinement.build();
1581
1582 assert_eq!(built.refines, "creator");
1583 assert_eq!(built.property, "file-as");
1584 assert_eq!(built.value, "Doe, John");
1585 assert_eq!(built.lang, Some("en".to_string()));
1586 assert_eq!(built.scheme, Some("dcterms".to_string()));
1587 }
1588
1589 #[test]
1590 fn test_metadata_refinement_attributes() {
1591 let mut refinement = MetadataRefinement::new("title", "title-type", "main");
1592 refinement.with_lang("en").with_scheme("onix:codelist5");
1593
1594 let attributes = refinement.attributes();
1595
1596 assert!(
1597 attributes
1598 .iter()
1599 .any(|(k, v)| k == &"refines" && v == &"title")
1600 );
1601 assert!(
1602 attributes
1603 .iter()
1604 .any(|(k, v)| k == &"property" && v == &"title-type")
1605 );
1606 assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1607 assert!(
1608 attributes
1609 .iter()
1610 .any(|(k, v)| k == &"scheme" && v == &"onix:codelist5")
1611 );
1612 }
1613
1614 #[test]
1615 fn test_metadata_refinement_attributes_optional_fields() {
1616 let refinement = MetadataRefinement::new("creator", "role", "author");
1617 let attributes = refinement.attributes();
1618
1619 assert!(
1620 attributes
1621 .iter()
1622 .any(|(k, v)| k == &"refines" && v == &"creator")
1623 );
1624 assert!(
1625 attributes
1626 .iter()
1627 .any(|(k, v)| k == &"property" && v == &"role")
1628 );
1629
1630 assert!(!attributes.iter().any(|(k, _)| k == &"lang"));
1632 assert!(!attributes.iter().any(|(k, _)| k == &"scheme"));
1633 }
1634 }
1635
1636 mod manifest_item {
1637 use std::path::PathBuf;
1638
1639 use crate::types::ManifestItem;
1640
1641 #[test]
1642 fn test_manifest_item_new() {
1643 let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1644 assert!(manifest_item.is_ok());
1645
1646 let manifest_item = manifest_item.unwrap();
1647 assert_eq!(manifest_item.id, "cover");
1648 assert_eq!(manifest_item.path, PathBuf::from("images/cover.jpg"));
1649 assert_eq!(manifest_item.mime, "");
1650 assert_eq!(manifest_item.properties, None);
1651 assert_eq!(manifest_item.fallback, None);
1652 }
1653
1654 #[test]
1655 fn test_manifest_item_append_property() {
1656 let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1657 assert!(manifest_item.is_ok());
1658
1659 let mut manifest_item = manifest_item.unwrap();
1660 manifest_item.append_property("nav");
1661
1662 assert_eq!(manifest_item.id, "nav");
1663 assert_eq!(manifest_item.path, PathBuf::from("nav.xhtml"));
1664 assert_eq!(manifest_item.mime, "");
1665 assert_eq!(manifest_item.properties, Some("nav".to_string()));
1666 assert_eq!(manifest_item.fallback, None);
1667 }
1668
1669 #[test]
1670 fn test_manifest_item_append_multiple_properties() {
1671 let manifest_item = ManifestItem::new("content", "content.xhtml");
1672 assert!(manifest_item.is_ok());
1673
1674 let mut manifest_item = manifest_item.unwrap();
1675 manifest_item
1676 .append_property("nav")
1677 .append_property("scripted")
1678 .append_property("svg");
1679
1680 assert_eq!(
1681 manifest_item.properties,
1682 Some("nav scripted svg".to_string())
1683 );
1684 }
1685
1686 #[test]
1687 fn test_manifest_item_with_fallback() {
1688 let manifest_item = ManifestItem::new("image", "image.tiff");
1689 assert!(manifest_item.is_ok());
1690
1691 let mut manifest_item = manifest_item.unwrap();
1692 manifest_item.with_fallback("image-fallback");
1693
1694 assert_eq!(manifest_item.id, "image");
1695 assert_eq!(manifest_item.path, PathBuf::from("image.tiff"));
1696 assert_eq!(manifest_item.mime, "");
1697 assert_eq!(manifest_item.properties, None);
1698 assert_eq!(manifest_item.fallback, Some("image-fallback".to_string()));
1699 }
1700
1701 #[test]
1702 fn test_manifest_item_set_mime() {
1703 let manifest_item = ManifestItem::new("style", "style.css");
1704 assert!(manifest_item.is_ok());
1705
1706 let manifest_item = manifest_item.unwrap();
1707 let updated_item = manifest_item.set_mime("text/css");
1708
1709 assert_eq!(updated_item.id, "style");
1710 assert_eq!(updated_item.path, PathBuf::from("style.css"));
1711 assert_eq!(updated_item.mime, "text/css");
1712 assert_eq!(updated_item.properties, None);
1713 assert_eq!(updated_item.fallback, None);
1714 }
1715
1716 #[test]
1717 fn test_manifest_item_build() {
1718 let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1719 assert!(manifest_item.is_ok());
1720
1721 let mut manifest_item = manifest_item.unwrap();
1722 manifest_item
1723 .append_property("cover-image")
1724 .with_fallback("cover-fallback");
1725
1726 let built = manifest_item.build();
1727
1728 assert_eq!(built.id, "cover");
1729 assert_eq!(built.path, PathBuf::from("images/cover.jpg"));
1730 assert_eq!(built.mime, "");
1731 assert_eq!(built.properties, Some("cover-image".to_string()));
1732 assert_eq!(built.fallback, Some("cover-fallback".to_string()));
1733 }
1734
1735 #[test]
1736 fn test_manifest_item_builder_chaining() {
1737 let manifest_item = ManifestItem::new("content", "content.xhtml");
1738 assert!(manifest_item.is_ok());
1739
1740 let mut manifest_item = manifest_item.unwrap();
1741 manifest_item
1742 .append_property("scripted")
1743 .append_property("svg")
1744 .with_fallback("fallback-content");
1745
1746 let built = manifest_item.build();
1747
1748 assert_eq!(built.id, "content");
1749 assert_eq!(built.path, PathBuf::from("content.xhtml"));
1750 assert_eq!(built.mime, "");
1751 assert_eq!(built.properties, Some("scripted svg".to_string()));
1752 assert_eq!(built.fallback, Some("fallback-content".to_string()));
1753 }
1754
1755 #[test]
1756 fn test_manifest_item_attributes() {
1757 let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1758 assert!(manifest_item.is_ok());
1759
1760 let mut manifest_item = manifest_item.unwrap();
1761 manifest_item
1762 .append_property("nav")
1763 .with_fallback("fallback-nav");
1764
1765 let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1767 let attributes = manifest_item.attributes();
1768
1769 assert!(attributes.contains(&("id", "nav")));
1771 assert!(attributes.contains(&("href", "nav.xhtml")));
1772 assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1773 assert!(attributes.contains(&("properties", "nav")));
1774 assert!(attributes.contains(&("fallback", "fallback-nav")));
1775 }
1776
1777 #[test]
1778 fn test_manifest_item_attributes_optional_fields() {
1779 let manifest_item = ManifestItem::new("simple", "simple.xhtml");
1780 assert!(manifest_item.is_ok());
1781
1782 let manifest_item = manifest_item.unwrap();
1783 let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1784 let attributes = manifest_item.attributes();
1785
1786 assert!(attributes.contains(&("id", "simple")));
1788 assert!(attributes.contains(&("href", "simple.xhtml")));
1789 assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1790
1791 assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1793 assert!(!attributes.iter().any(|(k, _)| k == &"fallback"));
1794 }
1795
1796 #[test]
1797 fn test_manifest_item_path_handling() {
1798 let manifest_item = ManifestItem::new("test", "../images/test.png");
1799 assert!(manifest_item.is_err());
1800
1801 let err = manifest_item.unwrap_err();
1802 assert_eq!(
1803 err.to_string(),
1804 "Epub builder error: A manifest with id 'test' should not use a relative path starting with '../'."
1805 );
1806 }
1807 }
1808
1809 mod spine_item {
1810 use crate::types::SpineItem;
1811
1812 #[test]
1813 fn test_spine_item_new() {
1814 let spine_item = SpineItem::new("content_001");
1815
1816 assert_eq!(spine_item.idref, "content_001");
1817 assert_eq!(spine_item.id, None);
1818 assert_eq!(spine_item.properties, None);
1819 assert_eq!(spine_item.linear, true);
1820 }
1821
1822 #[test]
1823 fn test_spine_item_with_id() {
1824 let mut spine_item = SpineItem::new("content_001");
1825 spine_item.with_id("spine1");
1826
1827 assert_eq!(spine_item.idref, "content_001");
1828 assert_eq!(spine_item.id, Some("spine1".to_string()));
1829 assert_eq!(spine_item.properties, None);
1830 assert_eq!(spine_item.linear, true);
1831 }
1832
1833 #[test]
1834 fn test_spine_item_append_property() {
1835 let mut spine_item = SpineItem::new("content_001");
1836 spine_item.append_property("page-spread-left");
1837
1838 assert_eq!(spine_item.idref, "content_001");
1839 assert_eq!(spine_item.id, None);
1840 assert_eq!(spine_item.properties, Some("page-spread-left".to_string()));
1841 assert_eq!(spine_item.linear, true);
1842 }
1843
1844 #[test]
1845 fn test_spine_item_append_multiple_properties() {
1846 let mut spine_item = SpineItem::new("content_001");
1847 spine_item
1848 .append_property("page-spread-left")
1849 .append_property("rendition:layout-pre-paginated");
1850
1851 assert_eq!(
1852 spine_item.properties,
1853 Some("page-spread-left rendition:layout-pre-paginated".to_string())
1854 );
1855 }
1856
1857 #[test]
1858 fn test_spine_item_set_linear() {
1859 let mut spine_item = SpineItem::new("content_001");
1860 spine_item.set_linear(false);
1861
1862 assert_eq!(spine_item.idref, "content_001");
1863 assert_eq!(spine_item.id, None);
1864 assert_eq!(spine_item.properties, None);
1865 assert_eq!(spine_item.linear, false);
1866 }
1867
1868 #[test]
1869 fn test_spine_item_build() {
1870 let mut spine_item = SpineItem::new("content_001");
1871 spine_item
1872 .with_id("spine1")
1873 .append_property("page-spread-left")
1874 .set_linear(false);
1875
1876 let built = spine_item.build();
1877
1878 assert_eq!(built.idref, "content_001");
1879 assert_eq!(built.id, Some("spine1".to_string()));
1880 assert_eq!(built.properties, Some("page-spread-left".to_string()));
1881 assert_eq!(built.linear, false);
1882 }
1883
1884 #[test]
1885 fn test_spine_item_builder_chaining() {
1886 let mut spine_item = SpineItem::new("content_001");
1887 spine_item
1888 .with_id("spine1")
1889 .append_property("page-spread-left")
1890 .set_linear(false);
1891
1892 let built = spine_item.build();
1893
1894 assert_eq!(built.idref, "content_001");
1895 assert_eq!(built.id, Some("spine1".to_string()));
1896 assert_eq!(built.properties, Some("page-spread-left".to_string()));
1897 assert_eq!(built.linear, false);
1898 }
1899
1900 #[test]
1901 fn test_spine_item_attributes() {
1902 let mut spine_item = SpineItem::new("content_001");
1903 spine_item
1904 .with_id("spine1")
1905 .append_property("page-spread-left")
1906 .set_linear(false);
1907
1908 let attributes = spine_item.attributes();
1909
1910 assert!(attributes.contains(&("idref", "content_001")));
1912 assert!(attributes.contains(&("id", "spine1")));
1913 assert!(attributes.contains(&("properties", "page-spread-left")));
1914 assert!(attributes.contains(&("linear", "no"))); }
1916
1917 #[test]
1918 fn test_spine_item_attributes_linear_yes() {
1919 let spine_item = SpineItem::new("content_001");
1920 let attributes = spine_item.attributes();
1921
1922 assert!(attributes.contains(&("linear", "yes")));
1924 }
1925
1926 #[test]
1927 fn test_spine_item_attributes_optional_fields() {
1928 let spine_item = SpineItem::new("content_001");
1929 let attributes = spine_item.attributes();
1930
1931 assert!(attributes.contains(&("idref", "content_001")));
1933 assert!(attributes.contains(&("linear", "yes")));
1934
1935 assert!(!attributes.iter().any(|(k, _)| k == &"id"));
1937 assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1938 }
1939 }
1940
1941 mod navpoint {
1942
1943 use std::path::PathBuf;
1944
1945 use crate::types::NavPoint;
1946
1947 #[test]
1948 fn test_navpoint_new() {
1949 let navpoint = NavPoint::new("Test Chapter");
1950
1951 assert_eq!(navpoint.label, "Test Chapter");
1952 assert_eq!(navpoint.content, None);
1953 assert_eq!(navpoint.children.len(), 0);
1954 }
1955
1956 #[test]
1957 fn test_navpoint_with_content() {
1958 let mut navpoint = NavPoint::new("Test Chapter");
1959 navpoint.with_content("chapter1.html");
1960
1961 assert_eq!(navpoint.label, "Test Chapter");
1962 assert_eq!(navpoint.content, Some(PathBuf::from("chapter1.html")));
1963 assert_eq!(navpoint.children.len(), 0);
1964 }
1965
1966 #[test]
1967 fn test_navpoint_append_child() {
1968 let mut parent = NavPoint::new("Parent Chapter");
1969
1970 let mut child1 = NavPoint::new("Child Section 1");
1971 child1.with_content("section1.html");
1972
1973 let mut child2 = NavPoint::new("Child Section 2");
1974 child2.with_content("section2.html");
1975
1976 parent.append_child(child1.build());
1977 parent.append_child(child2.build());
1978
1979 assert_eq!(parent.children.len(), 2);
1980 assert_eq!(parent.children[0].label, "Child Section 1");
1981 assert_eq!(parent.children[1].label, "Child Section 2");
1982 }
1983
1984 #[test]
1985 fn test_navpoint_set_children() {
1986 let mut navpoint = NavPoint::new("Main Chapter");
1987 let children = vec![NavPoint::new("Section 1"), NavPoint::new("Section 2")];
1988
1989 navpoint.set_children(children);
1990
1991 assert_eq!(navpoint.children.len(), 2);
1992 assert_eq!(navpoint.children[0].label, "Section 1");
1993 assert_eq!(navpoint.children[1].label, "Section 2");
1994 }
1995
1996 #[test]
1997 fn test_navpoint_build() {
1998 let mut navpoint = NavPoint::new("Complete Chapter");
1999 navpoint.with_content("complete.html");
2000
2001 let child = NavPoint::new("Sub Section");
2002 navpoint.append_child(child.build());
2003
2004 let built = navpoint.build();
2005
2006 assert_eq!(built.label, "Complete Chapter");
2007 assert_eq!(built.content, Some(PathBuf::from("complete.html")));
2008 assert_eq!(built.children.len(), 1);
2009 assert_eq!(built.children[0].label, "Sub Section");
2010 }
2011
2012 #[test]
2013 fn test_navpoint_builder_chaining() {
2014 let mut navpoint = NavPoint::new("Chained Chapter");
2015
2016 navpoint
2017 .with_content("chained.html")
2018 .append_child(NavPoint::new("Child 1").build())
2019 .append_child(NavPoint::new("Child 2").build());
2020
2021 let built = navpoint.build();
2022
2023 assert_eq!(built.label, "Chained Chapter");
2024 assert_eq!(built.content, Some(PathBuf::from("chained.html")));
2025 assert_eq!(built.children.len(), 2);
2026 }
2027
2028 #[test]
2029 fn test_navpoint_empty_children() {
2030 let navpoint = NavPoint::new("No Children Chapter");
2031 let built = navpoint.build();
2032
2033 assert_eq!(built.children.len(), 0);
2034 }
2035
2036 #[test]
2037 fn test_navpoint_complex_hierarchy() {
2038 let mut root = NavPoint::new("Book");
2039
2040 let mut chapter1 = NavPoint::new("Chapter 1");
2041 chapter1
2042 .with_content("chapter1.html")
2043 .append_child(
2044 NavPoint::new("Section 1.1")
2045 .with_content("sec1_1.html")
2046 .build(),
2047 )
2048 .append_child(
2049 NavPoint::new("Section 1.2")
2050 .with_content("sec1_2.html")
2051 .build(),
2052 );
2053
2054 let mut chapter2 = NavPoint::new("Chapter 2");
2055 chapter2.with_content("chapter2.html").append_child(
2056 NavPoint::new("Section 2.1")
2057 .with_content("sec2_1.html")
2058 .build(),
2059 );
2060
2061 root.append_child(chapter1.build())
2062 .append_child(chapter2.build());
2063
2064 let book = root.build();
2065
2066 assert_eq!(book.label, "Book");
2067 assert_eq!(book.children.len(), 2);
2068
2069 let ch1 = &book.children[0];
2070 assert_eq!(ch1.label, "Chapter 1");
2071 assert_eq!(ch1.children.len(), 2);
2072
2073 let ch2 = &book.children[1];
2074 assert_eq!(ch2.label, "Chapter 2");
2075 assert_eq!(ch2.children.len(), 1);
2076 }
2077 }
2078 }
2079
2080 #[cfg(feature = "content-builder")]
2081 mod footnote_tests {
2082 use crate::types::Footnote;
2083
2084 #[test]
2085 fn test_footnote_basic_creation() {
2086 let footnote = Footnote {
2087 locate: 100,
2088 content: "Sample footnote".to_string(),
2089 };
2090
2091 assert_eq!(footnote.locate, 100);
2092 assert_eq!(footnote.content, "Sample footnote");
2093 }
2094
2095 #[test]
2096 fn test_footnote_equality() {
2097 let footnote1 = Footnote {
2098 locate: 100,
2099 content: "First note".to_string(),
2100 };
2101
2102 let footnote2 = Footnote {
2103 locate: 100,
2104 content: "First note".to_string(),
2105 };
2106
2107 let footnote3 = Footnote {
2108 locate: 100,
2109 content: "Different note".to_string(),
2110 };
2111
2112 let footnote4 = Footnote {
2113 locate: 200,
2114 content: "First note".to_string(),
2115 };
2116
2117 assert_eq!(footnote1, footnote2);
2118 assert_ne!(footnote1, footnote3);
2119 assert_ne!(footnote1, footnote4);
2120 }
2121
2122 #[test]
2123 fn test_footnote_ordering() {
2124 let footnote1 = Footnote {
2125 locate: 100,
2126 content: "First".to_string(),
2127 };
2128
2129 let footnote2 = Footnote {
2130 locate: 200,
2131 content: "Second".to_string(),
2132 };
2133
2134 let footnote3 = Footnote {
2135 locate: 150,
2136 content: "Middle".to_string(),
2137 };
2138
2139 assert!(footnote1 < footnote2);
2140 assert!(footnote2 > footnote1);
2141 assert!(footnote1 < footnote3);
2142 assert!(footnote3 < footnote2);
2143 assert_eq!(footnote1.cmp(&footnote1), std::cmp::Ordering::Equal);
2144 }
2145
2146 #[test]
2147 fn test_footnote_sorting() {
2148 let mut footnotes = vec![
2149 Footnote {
2150 locate: 300,
2151 content: "Third note".to_string(),
2152 },
2153 Footnote {
2154 locate: 100,
2155 content: "First note".to_string(),
2156 },
2157 Footnote {
2158 locate: 200,
2159 content: "Second note".to_string(),
2160 },
2161 ];
2162
2163 footnotes.sort();
2164
2165 assert_eq!(footnotes[0].locate, 100);
2166 assert_eq!(footnotes[1].locate, 200);
2167 assert_eq!(footnotes[2].locate, 300);
2168
2169 assert_eq!(footnotes[0].content, "First note");
2170 assert_eq!(footnotes[1].content, "Second note");
2171 assert_eq!(footnotes[2].content, "Third note");
2172 }
2173 }
2174
2175 #[cfg(feature = "content-builder")]
2176 mod block_type_tests {
2177 use crate::types::BlockType;
2178
2179 #[test]
2180 fn test_block_type_variants() {
2181 let _ = BlockType::Text;
2182 let _ = BlockType::Quote;
2183 let _ = BlockType::Title;
2184 let _ = BlockType::Image;
2185 let _ = BlockType::Audio;
2186 let _ = BlockType::Video;
2187 let _ = BlockType::MathML;
2188 }
2189
2190 #[test]
2191 fn test_block_type_debug() {
2192 let text = format!("{:?}", BlockType::Text);
2193 assert_eq!(text, "Text");
2194
2195 let quote = format!("{:?}", BlockType::Quote);
2196 assert_eq!(quote, "Quote");
2197
2198 let image = format!("{:?}", BlockType::Image);
2199 assert_eq!(image, "Image");
2200 }
2201 }
2202
2203 #[cfg(feature = "content-builder")]
2204 mod style_options_tests {
2205 use crate::types::{ColorScheme, PageLayout, StyleOptions, TextAlign, TextStyle};
2206
2207 #[test]
2208 fn test_style_options_default() {
2209 let options = StyleOptions::default();
2210
2211 assert_eq!(options.text.font_size, 1.0);
2212 assert_eq!(options.text.line_height, 1.6);
2213 assert_eq!(
2214 options.text.font_family,
2215 "-apple-system, Roboto, sans-serif"
2216 );
2217 assert_eq!(options.text.font_weight, "normal");
2218 assert_eq!(options.text.font_style, "normal");
2219 assert_eq!(options.text.letter_spacing, "normal");
2220 assert_eq!(options.text.text_indent, 2.0);
2221
2222 assert_eq!(options.color_scheme.background, "#FFFFFF");
2223 assert_eq!(options.color_scheme.text, "#000000");
2224 assert_eq!(options.color_scheme.link, "#6f6f6f");
2225
2226 assert_eq!(options.layout.margin, 20);
2227 assert_eq!(options.layout.text_align, TextAlign::Left);
2228 assert_eq!(options.layout.paragraph_spacing, 16);
2229 }
2230
2231 #[test]
2232 fn test_style_options_custom_values() {
2233 let text = TextStyle {
2234 font_size: 1.5,
2235 line_height: 2.0,
2236 font_family: "Georgia, serif".to_string(),
2237 font_weight: "bold".to_string(),
2238 font_style: "italic".to_string(),
2239 letter_spacing: "0.1em".to_string(),
2240 text_indent: 3.0,
2241 };
2242
2243 let color_scheme = ColorScheme {
2244 background: "#F0F0F0".to_string(),
2245 text: "#333333".to_string(),
2246 link: "#0066CC".to_string(),
2247 };
2248
2249 let layout = PageLayout {
2250 margin: 30,
2251 text_align: TextAlign::Center,
2252 paragraph_spacing: 20,
2253 };
2254
2255 let options = StyleOptions { text, color_scheme, layout };
2256
2257 assert_eq!(options.text.font_size, 1.5);
2258 assert_eq!(options.text.font_weight, "bold");
2259 assert_eq!(options.color_scheme.background, "#F0F0F0");
2260 assert_eq!(options.layout.text_align, TextAlign::Center);
2261 }
2262
2263 #[test]
2264 fn test_text_style_default() {
2265 let style = TextStyle::default();
2266
2267 assert_eq!(style.font_size, 1.0);
2268 assert_eq!(style.line_height, 1.6);
2269 assert_eq!(style.font_family, "-apple-system, Roboto, sans-serif");
2270 assert_eq!(style.font_weight, "normal");
2271 assert_eq!(style.font_style, "normal");
2272 assert_eq!(style.letter_spacing, "normal");
2273 assert_eq!(style.text_indent, 2.0);
2274 }
2275
2276 #[test]
2277 fn test_text_style_custom_values() {
2278 let style = TextStyle {
2279 font_size: 2.0,
2280 line_height: 1.8,
2281 font_family: "Times New Roman".to_string(),
2282 font_weight: "bold".to_string(),
2283 font_style: "italic".to_string(),
2284 letter_spacing: "0.05em".to_string(),
2285 text_indent: 0.0,
2286 };
2287
2288 assert_eq!(style.font_size, 2.0);
2289 assert_eq!(style.line_height, 1.8);
2290 assert_eq!(style.font_family, "Times New Roman");
2291 assert_eq!(style.font_weight, "bold");
2292 assert_eq!(style.font_style, "italic");
2293 assert_eq!(style.letter_spacing, "0.05em");
2294 assert_eq!(style.text_indent, 0.0);
2295 }
2296
2297 #[test]
2298 fn test_text_style_debug() {
2299 let style = TextStyle::default();
2300 let debug_str = format!("{:?}", style);
2301 assert!(debug_str.contains("TextStyle"));
2302 assert!(debug_str.contains("font_size"));
2303 }
2304
2305 #[test]
2306 fn test_color_scheme_default() {
2307 let scheme = ColorScheme::default();
2308
2309 assert_eq!(scheme.background, "#FFFFFF");
2310 assert_eq!(scheme.text, "#000000");
2311 assert_eq!(scheme.link, "#6f6f6f");
2312 }
2313
2314 #[test]
2315 fn test_color_scheme_custom_values() {
2316 let scheme = ColorScheme {
2317 background: "#000000".to_string(),
2318 text: "#FFFFFF".to_string(),
2319 link: "#00FF00".to_string(),
2320 };
2321
2322 assert_eq!(scheme.background, "#000000");
2323 assert_eq!(scheme.text, "#FFFFFF");
2324 assert_eq!(scheme.link, "#00FF00");
2325 }
2326
2327 #[test]
2328 fn test_color_scheme_debug() {
2329 let scheme = ColorScheme::default();
2330 let debug_str = format!("{:?}", scheme);
2331 assert!(debug_str.contains("ColorScheme"));
2332 assert!(debug_str.contains("background"));
2333 }
2334
2335 #[test]
2336 fn test_page_layout_default() {
2337 let layout = PageLayout::default();
2338
2339 assert_eq!(layout.margin, 20);
2340 assert_eq!(layout.text_align, TextAlign::Left);
2341 assert_eq!(layout.paragraph_spacing, 16);
2342 }
2343
2344 #[test]
2345 fn test_page_layout_custom_values() {
2346 let layout = PageLayout {
2347 margin: 40,
2348 text_align: TextAlign::Justify,
2349 paragraph_spacing: 24,
2350 };
2351
2352 assert_eq!(layout.margin, 40);
2353 assert_eq!(layout.text_align, TextAlign::Justify);
2354 assert_eq!(layout.paragraph_spacing, 24);
2355 }
2356
2357 #[test]
2358 fn test_page_layout_debug() {
2359 let layout = PageLayout::default();
2360 let debug_str = format!("{:?}", layout);
2361 assert!(debug_str.contains("PageLayout"));
2362 assert!(debug_str.contains("margin"));
2363 }
2364
2365 #[test]
2366 fn test_text_align_default() {
2367 let align = TextAlign::default();
2368 assert_eq!(align, TextAlign::Left);
2369 }
2370
2371 #[test]
2372 fn test_text_align_display() {
2373 assert_eq!(TextAlign::Left.to_string(), "left");
2374 assert_eq!(TextAlign::Right.to_string(), "right");
2375 assert_eq!(TextAlign::Justify.to_string(), "justify");
2376 assert_eq!(TextAlign::Center.to_string(), "center");
2377 }
2378
2379 #[test]
2380 fn test_text_align_all_variants() {
2381 let left = TextAlign::Left;
2382 let right = TextAlign::Right;
2383 let justify = TextAlign::Justify;
2384 let center = TextAlign::Center;
2385
2386 assert!(matches!(left, TextAlign::Left));
2387 assert!(matches!(right, TextAlign::Right));
2388 assert!(matches!(justify, TextAlign::Justify));
2389 assert!(matches!(center, TextAlign::Center));
2390 }
2391
2392 #[test]
2393 fn test_text_align_debug() {
2394 assert_eq!(format!("{:?}", TextAlign::Left), "Left");
2395 assert_eq!(format!("{:?}", TextAlign::Right), "Right");
2396 assert_eq!(format!("{:?}", TextAlign::Justify), "Justify");
2397 assert_eq!(format!("{:?}", TextAlign::Center), "Center");
2398 }
2399
2400 #[test]
2401 fn test_style_options_builder_new() {
2402 let options = StyleOptions::new();
2403 assert_eq!(options.text.font_size, 1.0);
2404 assert_eq!(options.color_scheme.background, "#FFFFFF");
2405 assert_eq!(options.layout.margin, 20);
2406 }
2407
2408 #[test]
2409 fn test_style_options_builder_with_text() {
2410 let mut options = StyleOptions::new();
2411 let text_style = TextStyle::new()
2412 .with_font_size(2.0)
2413 .with_font_weight("bold")
2414 .build();
2415 options.with_text(text_style);
2416
2417 assert_eq!(options.text.font_size, 2.0);
2418 assert_eq!(options.text.font_weight, "bold");
2419 }
2420
2421 #[test]
2422 fn test_style_options_builder_with_color_scheme() {
2423 let mut options = StyleOptions::new();
2424 let color = ColorScheme::new()
2425 .with_background("#000000")
2426 .with_text("#FFFFFF")
2427 .build();
2428 options.with_color_scheme(color);
2429
2430 assert_eq!(options.color_scheme.background, "#000000");
2431 assert_eq!(options.color_scheme.text, "#FFFFFF");
2432 }
2433
2434 #[test]
2435 fn test_style_options_builder_with_layout() {
2436 let mut options = StyleOptions::new();
2437 let layout = PageLayout::new()
2438 .with_margin(40)
2439 .with_text_align(TextAlign::Justify)
2440 .with_paragraph_spacing(24)
2441 .build();
2442 options.with_layout(layout);
2443
2444 assert_eq!(options.layout.margin, 40);
2445 assert_eq!(options.layout.text_align, TextAlign::Justify);
2446 assert_eq!(options.layout.paragraph_spacing, 24);
2447 }
2448
2449 #[test]
2450 fn test_style_options_builder_build() {
2451 let options = StyleOptions::new()
2452 .with_text(TextStyle::new().with_font_size(1.5).build())
2453 .with_color_scheme(ColorScheme::new().with_link("#FF0000").build())
2454 .with_layout(PageLayout::new().with_margin(30).build())
2455 .build();
2456
2457 assert_eq!(options.text.font_size, 1.5);
2458 assert_eq!(options.color_scheme.link, "#FF0000");
2459 assert_eq!(options.layout.margin, 30);
2460 }
2461
2462 #[test]
2463 fn test_style_options_builder_chaining() {
2464 let options = StyleOptions::new()
2465 .with_text(
2466 TextStyle::new()
2467 .with_font_size(1.5)
2468 .with_line_height(2.0)
2469 .with_font_family("Arial")
2470 .with_font_weight("bold")
2471 .with_font_style("italic")
2472 .with_letter_spacing("0.1em")
2473 .with_text_indent(1.5)
2474 .build(),
2475 )
2476 .with_color_scheme(
2477 ColorScheme::new()
2478 .with_background("#CCCCCC")
2479 .with_text("#111111")
2480 .with_link("#0000FF")
2481 .build(),
2482 )
2483 .with_layout(
2484 PageLayout::new()
2485 .with_margin(25)
2486 .with_text_align(TextAlign::Right)
2487 .with_paragraph_spacing(20)
2488 .build(),
2489 )
2490 .build();
2491
2492 assert_eq!(options.text.font_size, 1.5);
2493 assert_eq!(options.text.line_height, 2.0);
2494 assert_eq!(options.text.font_family, "Arial");
2495 assert_eq!(options.text.font_weight, "bold");
2496 assert_eq!(options.text.font_style, "italic");
2497 assert_eq!(options.text.letter_spacing, "0.1em");
2498 assert_eq!(options.text.text_indent, 1.5);
2499
2500 assert_eq!(options.color_scheme.background, "#CCCCCC");
2501 assert_eq!(options.color_scheme.text, "#111111");
2502 assert_eq!(options.color_scheme.link, "#0000FF");
2503
2504 assert_eq!(options.layout.margin, 25);
2505 assert_eq!(options.layout.text_align, TextAlign::Right);
2506 assert_eq!(options.layout.paragraph_spacing, 20);
2507 }
2508
2509 #[test]
2510 fn test_text_style_builder_new() {
2511 let style = TextStyle::new();
2512 assert_eq!(style.font_size, 1.0);
2513 assert_eq!(style.line_height, 1.6);
2514 }
2515
2516 #[test]
2517 fn test_text_style_builder_with_font_size() {
2518 let mut style = TextStyle::new();
2519 style.with_font_size(2.5);
2520 assert_eq!(style.font_size, 2.5);
2521 }
2522
2523 #[test]
2524 fn test_text_style_builder_with_line_height() {
2525 let mut style = TextStyle::new();
2526 style.with_line_height(2.0);
2527 assert_eq!(style.line_height, 2.0);
2528 }
2529
2530 #[test]
2531 fn test_text_style_builder_with_font_family() {
2532 let mut style = TextStyle::new();
2533 style.with_font_family("Helvetica, Arial");
2534 assert_eq!(style.font_family, "Helvetica, Arial");
2535 }
2536
2537 #[test]
2538 fn test_text_style_builder_with_font_weight() {
2539 let mut style = TextStyle::new();
2540 style.with_font_weight("bold");
2541 assert_eq!(style.font_weight, "bold");
2542 }
2543
2544 #[test]
2545 fn test_text_style_builder_with_font_style() {
2546 let mut style = TextStyle::new();
2547 style.with_font_style("italic");
2548 assert_eq!(style.font_style, "italic");
2549 }
2550
2551 #[test]
2552 fn test_text_style_builder_with_letter_spacing() {
2553 let mut style = TextStyle::new();
2554 style.with_letter_spacing("0.05em");
2555 assert_eq!(style.letter_spacing, "0.05em");
2556 }
2557
2558 #[test]
2559 fn test_text_style_builder_with_text_indent() {
2560 let mut style = TextStyle::new();
2561 style.with_text_indent(3.0);
2562 assert_eq!(style.text_indent, 3.0);
2563 }
2564
2565 #[test]
2566 fn test_text_style_builder_build() {
2567 let style = TextStyle::new()
2568 .with_font_size(1.8)
2569 .with_line_height(1.9)
2570 .build();
2571
2572 assert_eq!(style.font_size, 1.8);
2573 assert_eq!(style.line_height, 1.9);
2574 }
2575
2576 #[test]
2577 fn test_text_style_builder_chaining() {
2578 let style = TextStyle::new()
2579 .with_font_size(2.0)
2580 .with_line_height(1.8)
2581 .with_font_family("Georgia")
2582 .with_font_weight("bold")
2583 .with_font_style("italic")
2584 .with_letter_spacing("0.1em")
2585 .with_text_indent(0.5)
2586 .build();
2587
2588 assert_eq!(style.font_size, 2.0);
2589 assert_eq!(style.line_height, 1.8);
2590 assert_eq!(style.font_family, "Georgia");
2591 assert_eq!(style.font_weight, "bold");
2592 assert_eq!(style.font_style, "italic");
2593 assert_eq!(style.letter_spacing, "0.1em");
2594 assert_eq!(style.text_indent, 0.5);
2595 }
2596
2597 #[test]
2598 fn test_color_scheme_builder_new() {
2599 let scheme = ColorScheme::new();
2600 assert_eq!(scheme.background, "#FFFFFF");
2601 assert_eq!(scheme.text, "#000000");
2602 }
2603
2604 #[test]
2605 fn test_color_scheme_builder_with_background() {
2606 let mut scheme = ColorScheme::new();
2607 scheme.with_background("#FF0000");
2608 assert_eq!(scheme.background, "#FF0000");
2609 }
2610
2611 #[test]
2612 fn test_color_scheme_builder_with_text() {
2613 let mut scheme = ColorScheme::new();
2614 scheme.with_text("#333333");
2615 assert_eq!(scheme.text, "#333333");
2616 }
2617
2618 #[test]
2619 fn test_color_scheme_builder_with_link() {
2620 let mut scheme = ColorScheme::new();
2621 scheme.with_link("#0000FF");
2622 assert_eq!(scheme.link, "#0000FF");
2623 }
2624
2625 #[test]
2626 fn test_color_scheme_builder_build() {
2627 let scheme = ColorScheme::new().with_background("#123456").build();
2628
2629 assert_eq!(scheme.background, "#123456");
2630 assert_eq!(scheme.text, "#000000");
2631 }
2632
2633 #[test]
2634 fn test_color_scheme_builder_chaining() {
2635 let scheme = ColorScheme::new()
2636 .with_background("#AABBCC")
2637 .with_text("#DDEEFF")
2638 .with_link("#112233")
2639 .build();
2640
2641 assert_eq!(scheme.background, "#AABBCC");
2642 assert_eq!(scheme.text, "#DDEEFF");
2643 assert_eq!(scheme.link, "#112233");
2644 }
2645
2646 #[test]
2647 fn test_page_layout_builder_new() {
2648 let layout = PageLayout::new();
2649 assert_eq!(layout.margin, 20);
2650 assert_eq!(layout.text_align, TextAlign::Left);
2651 assert_eq!(layout.paragraph_spacing, 16);
2652 }
2653
2654 #[test]
2655 fn test_page_layout_builder_with_margin() {
2656 let mut layout = PageLayout::new();
2657 layout.with_margin(50);
2658 assert_eq!(layout.margin, 50);
2659 }
2660
2661 #[test]
2662 fn test_page_layout_builder_with_text_align() {
2663 let mut layout = PageLayout::new();
2664 layout.with_text_align(TextAlign::Center);
2665 assert_eq!(layout.text_align, TextAlign::Center);
2666 }
2667
2668 #[test]
2669 fn test_page_layout_builder_with_paragraph_spacing() {
2670 let mut layout = PageLayout::new();
2671 layout.with_paragraph_spacing(30);
2672 assert_eq!(layout.paragraph_spacing, 30);
2673 }
2674
2675 #[test]
2676 fn test_page_layout_builder_build() {
2677 let layout = PageLayout::new().with_margin(35).build();
2678
2679 assert_eq!(layout.margin, 35);
2680 assert_eq!(layout.text_align, TextAlign::Left);
2681 }
2682
2683 #[test]
2684 fn test_page_layout_builder_chaining() {
2685 let layout = PageLayout::new()
2686 .with_margin(45)
2687 .with_text_align(TextAlign::Justify)
2688 .with_paragraph_spacing(28)
2689 .build();
2690
2691 assert_eq!(layout.margin, 45);
2692 assert_eq!(layout.text_align, TextAlign::Justify);
2693 assert_eq!(layout.paragraph_spacing, 28);
2694 }
2695
2696 #[test]
2697 fn test_page_layout_builder_all_text_align_variants() {
2698 let left = PageLayout::new().with_text_align(TextAlign::Left).build();
2699 assert_eq!(left.text_align, TextAlign::Left);
2700
2701 let right = PageLayout::new().with_text_align(TextAlign::Right).build();
2702 assert_eq!(right.text_align, TextAlign::Right);
2703
2704 let center = PageLayout::new().with_text_align(TextAlign::Center).build();
2705 assert_eq!(center.text_align, TextAlign::Center);
2706
2707 let justify = PageLayout::new()
2708 .with_text_align(TextAlign::Justify)
2709 .build();
2710 assert_eq!(justify.text_align, TextAlign::Justify);
2711 }
2712 }
2713}