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)]
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")]
850#[derive(Debug, Default, Clone)]
851pub struct StyleOptions {
852 pub text: TextStyle,
854
855 pub color_scheme: ColorScheme,
859
860 pub layout: PageLayout,
864}
865
866#[cfg(feature = "content-builder")]
867#[cfg(feature = "builder")]
868impl StyleOptions {
869 pub fn new() -> Self {
871 Self::default()
872 }
873
874 pub fn with_text(&mut self, text: TextStyle) -> &mut Self {
876 self.text = text;
877 self
878 }
879
880 pub fn with_color_scheme(&mut self, color_scheme: ColorScheme) -> &mut Self {
882 self.color_scheme = color_scheme;
883 self
884 }
885
886 pub fn with_layout(&mut self, layout: PageLayout) -> &mut Self {
888 self.layout = layout;
889 self
890 }
891
892 pub fn build(&self) -> Self {
894 Self { ..self.clone() }
895 }
896}
897
898#[cfg(feature = "content-builder")]
903#[derive(Debug, Clone)]
904pub struct TextStyle {
905 pub font_size: f32,
910
911 pub line_height: f32,
917
918 pub font_family: String,
923
924 pub font_weight: String,
929
930 pub font_style: String,
935
936 pub letter_spacing: String,
941
942 pub text_indent: f32,
947}
948
949#[cfg(feature = "content-builder")]
950impl Default for TextStyle {
951 fn default() -> Self {
952 Self {
953 font_size: 1.0,
954 line_height: 1.6,
955 font_family: "-apple-system, Roboto, sans-serif".to_string(),
956 font_weight: "normal".to_string(),
957 font_style: "normal".to_string(),
958 letter_spacing: "normal".to_string(),
959 text_indent: 2.0,
960 }
961 }
962}
963
964#[cfg(feature = "content-builder")]
965impl TextStyle {
966 pub fn new() -> Self {
968 Self::default()
969 }
970
971 pub fn with_font_size(&mut self, font_size: f32) -> &mut Self {
973 self.font_size = font_size;
974 self
975 }
976
977 pub fn with_line_height(&mut self, line_height: f32) -> &mut Self {
979 self.line_height = line_height;
980 self
981 }
982
983 pub fn with_font_family(&mut self, font_family: &str) -> &mut Self {
985 self.font_family = font_family.to_string();
986 self
987 }
988
989 pub fn with_font_weight(&mut self, font_weight: &str) -> &mut Self {
991 self.font_weight = font_weight.to_string();
992 self
993 }
994
995 pub fn with_font_style(&mut self, font_style: &str) -> &mut Self {
997 self.font_style = font_style.to_string();
998 self
999 }
1000
1001 pub fn with_letter_spacing(&mut self, letter_spacing: &str) -> &mut Self {
1003 self.letter_spacing = letter_spacing.to_string();
1004 self
1005 }
1006
1007 pub fn with_text_indent(&mut self, text_indent: f32) -> &mut Self {
1009 self.text_indent = text_indent;
1010 self
1011 }
1012
1013 pub fn build(&self) -> Self {
1015 Self { ..self.clone() }
1016 }
1017}
1018
1019#[cfg(feature = "content-builder")]
1024#[derive(Debug, Clone)]
1025pub struct ColorScheme {
1026 pub background: String,
1031
1032 pub text: String,
1037
1038 pub link: String,
1043}
1044
1045#[cfg(feature = "content-builder")]
1046impl Default for ColorScheme {
1047 fn default() -> Self {
1048 Self {
1049 background: "#FFFFFF".to_string(),
1050 text: "#000000".to_string(),
1051 link: "#6f6f6f".to_string(),
1052 }
1053 }
1054}
1055
1056#[cfg(feature = "content-builder")]
1057impl ColorScheme {
1058 pub fn new() -> Self {
1060 Self::default()
1061 }
1062
1063 pub fn with_background(&mut self, background: &str) -> &mut Self {
1065 self.background = background.to_string();
1066 self
1067 }
1068
1069 pub fn with_text(&mut self, text: &str) -> &mut Self {
1071 self.text = text.to_string();
1072 self
1073 }
1074
1075 pub fn with_link(&mut self, link: &str) -> &mut Self {
1077 self.link = link.to_string();
1078 self
1079 }
1080
1081 pub fn build(&self) -> Self {
1083 Self { ..self.clone() }
1084 }
1085}
1086
1087#[cfg(feature = "content-builder")]
1092#[derive(Debug, Clone)]
1093pub struct PageLayout {
1094 pub margin: usize,
1098
1099 pub text_align: TextAlign,
1103
1104 pub paragraph_spacing: usize,
1108}
1109
1110#[cfg(feature = "content-builder")]
1111impl Default for PageLayout {
1112 fn default() -> Self {
1113 Self {
1114 margin: 20,
1115 text_align: Default::default(),
1116 paragraph_spacing: 16,
1117 }
1118 }
1119}
1120
1121#[cfg(feature = "content-builder")]
1122impl PageLayout {
1123 pub fn new() -> Self {
1125 Self::default()
1126 }
1127
1128 pub fn with_margin(&mut self, margin: usize) -> &mut Self {
1130 self.margin = margin;
1131 self
1132 }
1133
1134 pub fn with_text_align(&mut self, text_align: TextAlign) -> &mut Self {
1136 self.text_align = text_align;
1137 self
1138 }
1139
1140 pub fn with_paragraph_spacing(&mut self, paragraph_spacing: usize) -> &mut Self {
1142 self.paragraph_spacing = paragraph_spacing;
1143 self
1144 }
1145
1146 pub fn build(&self) -> Self {
1148 Self { ..self.clone() }
1149 }
1150}
1151
1152#[cfg(feature = "content-builder")]
1156#[derive(Debug, Default, Clone, Copy, PartialEq)]
1157pub enum TextAlign {
1158 #[default]
1162 Left,
1163
1164 Right,
1168
1169 Justify,
1174
1175 Center,
1179}
1180
1181#[cfg(feature = "content-builder")]
1182impl std::fmt::Display for TextAlign {
1183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1184 match self {
1185 TextAlign::Left => write!(f, "left"),
1186 TextAlign::Right => write!(f, "right"),
1187 TextAlign::Justify => write!(f, "justify"),
1188 TextAlign::Center => write!(f, "center"),
1189 }
1190 }
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195 mod navpoint_tests {
1196 use std::path::PathBuf;
1197
1198 use crate::types::NavPoint;
1199
1200 #[test]
1202 fn test_navpoint_partial_eq() {
1203 let nav1 = NavPoint {
1204 label: "Chapter 1".to_string(),
1205 content: Some(PathBuf::from("chapter1.html")),
1206 children: vec![],
1207 play_order: Some(1),
1208 };
1209
1210 let nav2 = NavPoint {
1211 label: "Chapter 1".to_string(),
1212 content: Some(PathBuf::from("chapter2.html")),
1213 children: vec![],
1214 play_order: Some(1),
1215 };
1216
1217 let nav3 = NavPoint {
1218 label: "Chapter 2".to_string(),
1219 content: Some(PathBuf::from("chapter1.html")),
1220 children: vec![],
1221 play_order: Some(2),
1222 };
1223
1224 assert_eq!(nav1, nav2); assert_ne!(nav1, nav3); }
1227
1228 #[test]
1230 fn test_navpoint_ord() {
1231 let nav1 = NavPoint {
1232 label: "Chapter 1".to_string(),
1233 content: Some(PathBuf::from("chapter1.html")),
1234 children: vec![],
1235 play_order: Some(1),
1236 };
1237
1238 let nav2 = NavPoint {
1239 label: "Chapter 2".to_string(),
1240 content: Some(PathBuf::from("chapter2.html")),
1241 children: vec![],
1242 play_order: Some(2),
1243 };
1244
1245 let nav3 = NavPoint {
1246 label: "Chapter 3".to_string(),
1247 content: Some(PathBuf::from("chapter3.html")),
1248 children: vec![],
1249 play_order: Some(3),
1250 };
1251
1252 assert!(nav1 < nav2);
1254 assert!(nav2 > nav1);
1255 assert!(nav1 == nav1);
1256
1257 assert_eq!(nav1.partial_cmp(&nav2), Some(std::cmp::Ordering::Less));
1259 assert_eq!(nav2.partial_cmp(&nav1), Some(std::cmp::Ordering::Greater));
1260 assert_eq!(nav1.partial_cmp(&nav1), Some(std::cmp::Ordering::Equal));
1261
1262 let mut nav_points = vec![nav2.clone(), nav3.clone(), nav1.clone()];
1264 nav_points.sort();
1265 assert_eq!(nav_points, vec![nav1, nav2, nav3]);
1266 }
1267
1268 #[test]
1270 fn test_navpoint_ord_with_none_play_order() {
1271 let nav_with_order = NavPoint {
1272 label: "Chapter 1".to_string(),
1273 content: Some(PathBuf::from("chapter1.html")),
1274 children: vec![],
1275 play_order: Some(1),
1276 };
1277
1278 let nav_without_order = NavPoint {
1279 label: "Preface".to_string(),
1280 content: Some(PathBuf::from("preface.html")),
1281 children: vec![],
1282 play_order: None,
1283 };
1284
1285 assert!(nav_without_order < nav_with_order);
1286 assert!(nav_with_order > nav_without_order);
1287
1288 let nav_without_order2 = NavPoint {
1289 label: "Introduction".to_string(),
1290 content: Some(PathBuf::from("intro.html")),
1291 children: vec![],
1292 play_order: None,
1293 };
1294
1295 assert!(nav_without_order == nav_without_order2);
1296 }
1297
1298 #[test]
1300 fn test_navpoint_with_children() {
1301 let child1 = NavPoint {
1302 label: "Section 1.1".to_string(),
1303 content: Some(PathBuf::from("section1_1.html")),
1304 children: vec![],
1305 play_order: Some(1),
1306 };
1307
1308 let child2 = NavPoint {
1309 label: "Section 1.2".to_string(),
1310 content: Some(PathBuf::from("section1_2.html")),
1311 children: vec![],
1312 play_order: Some(2),
1313 };
1314
1315 let parent1 = NavPoint {
1316 label: "Chapter 1".to_string(),
1317 content: Some(PathBuf::from("chapter1.html")),
1318 children: vec![child1.clone(), child2.clone()],
1319 play_order: Some(1),
1320 };
1321
1322 let parent2 = NavPoint {
1323 label: "Chapter 1".to_string(),
1324 content: Some(PathBuf::from("chapter1.html")),
1325 children: vec![child1.clone(), child2.clone()],
1326 play_order: Some(1),
1327 };
1328
1329 assert!(parent1 == parent2);
1330
1331 let parent3 = NavPoint {
1332 label: "Chapter 2".to_string(),
1333 content: Some(PathBuf::from("chapter2.html")),
1334 children: vec![child1.clone(), child2.clone()],
1335 play_order: Some(2),
1336 };
1337
1338 assert!(parent1 != parent3);
1339 assert!(parent1 < parent3);
1340 }
1341
1342 #[test]
1344 fn test_navpoint_with_none_content() {
1345 let nav1 = NavPoint {
1346 label: "Chapter 1".to_string(),
1347 content: None,
1348 children: vec![],
1349 play_order: Some(1),
1350 };
1351
1352 let nav2 = NavPoint {
1353 label: "Chapter 1".to_string(),
1354 content: None,
1355 children: vec![],
1356 play_order: Some(1),
1357 };
1358
1359 assert!(nav1 == nav2);
1360 }
1361 }
1362
1363 #[cfg(feature = "builder")]
1364 mod builder_tests {
1365 mod metadata_item {
1366 use crate::types::{MetadataItem, MetadataRefinement};
1367
1368 #[test]
1369 fn test_metadata_item_new() {
1370 let metadata_item = MetadataItem::new("title", "EPUB Test Book");
1371
1372 assert_eq!(metadata_item.property, "title");
1373 assert_eq!(metadata_item.value, "EPUB Test Book");
1374 assert_eq!(metadata_item.id, None);
1375 assert_eq!(metadata_item.lang, None);
1376 assert_eq!(metadata_item.refined.len(), 0);
1377 }
1378
1379 #[test]
1380 fn test_metadata_item_with_id() {
1381 let mut metadata_item = MetadataItem::new("creator", "John Doe");
1382 metadata_item.with_id("creator-1");
1383
1384 assert_eq!(metadata_item.property, "creator");
1385 assert_eq!(metadata_item.value, "John Doe");
1386 assert_eq!(metadata_item.id, Some("creator-1".to_string()));
1387 assert_eq!(metadata_item.lang, None);
1388 assert_eq!(metadata_item.refined.len(), 0);
1389 }
1390
1391 #[test]
1392 fn test_metadata_item_with_lang() {
1393 let mut metadata_item = MetadataItem::new("title", "测试书籍");
1394 metadata_item.with_lang("zh-CN");
1395
1396 assert_eq!(metadata_item.property, "title");
1397 assert_eq!(metadata_item.value, "测试书籍");
1398 assert_eq!(metadata_item.id, None);
1399 assert_eq!(metadata_item.lang, Some("zh-CN".to_string()));
1400 assert_eq!(metadata_item.refined.len(), 0);
1401 }
1402
1403 #[test]
1404 fn test_metadata_item_append_refinement() {
1405 let mut metadata_item = MetadataItem::new("creator", "John Doe");
1406 metadata_item.with_id("creator-1"); let refinement = MetadataRefinement::new("creator-1", "role", "author");
1409 metadata_item.append_refinement(refinement);
1410
1411 assert_eq!(metadata_item.refined.len(), 1);
1412 assert_eq!(metadata_item.refined[0].refines, "creator-1");
1413 assert_eq!(metadata_item.refined[0].property, "role");
1414 assert_eq!(metadata_item.refined[0].value, "author");
1415 }
1416
1417 #[test]
1418 fn test_metadata_item_append_refinement_without_id() {
1419 let mut metadata_item = MetadataItem::new("title", "Test Book");
1420 let refinement = MetadataRefinement::new("title", "title-type", "main");
1423 metadata_item.append_refinement(refinement);
1424
1425 assert_eq!(metadata_item.refined.len(), 0);
1427 }
1428
1429 #[test]
1430 fn test_metadata_item_build() {
1431 let mut metadata_item = MetadataItem::new("identifier", "urn:isbn:1234567890");
1432 metadata_item.with_id("pub-id").with_lang("en");
1433
1434 let built = metadata_item.build();
1435
1436 assert_eq!(built.property, "identifier");
1437 assert_eq!(built.value, "urn:isbn:1234567890");
1438 assert_eq!(built.id, Some("pub-id".to_string()));
1439 assert_eq!(built.lang, Some("en".to_string()));
1440 assert_eq!(built.refined.len(), 0);
1441 }
1442
1443 #[test]
1444 fn test_metadata_item_builder_chaining() {
1445 let mut metadata_item = MetadataItem::new("title", "EPUB 3.3 Guide");
1446 metadata_item.with_id("title").with_lang("en");
1447
1448 let refinement = MetadataRefinement::new("title", "title-type", "main");
1449 metadata_item.append_refinement(refinement);
1450
1451 let built = metadata_item.build();
1452
1453 assert_eq!(built.property, "title");
1454 assert_eq!(built.value, "EPUB 3.3 Guide");
1455 assert_eq!(built.id, Some("title".to_string()));
1456 assert_eq!(built.lang, Some("en".to_string()));
1457 assert_eq!(built.refined.len(), 1);
1458 }
1459
1460 #[test]
1461 fn test_metadata_item_attributes_dc_namespace() {
1462 let mut metadata_item = MetadataItem::new("title", "Test Book");
1463 metadata_item.with_id("title-id");
1464
1465 let attributes = metadata_item.attributes();
1466
1467 assert!(!attributes.iter().any(|(k, _)| k == &"property"));
1469 assert!(
1470 attributes
1471 .iter()
1472 .any(|(k, v)| k == &"id" && v == &"title-id")
1473 );
1474 }
1475
1476 #[test]
1477 fn test_metadata_item_attributes_non_dc_namespace() {
1478 let mut metadata_item = MetadataItem::new("meta", "value");
1479 metadata_item.with_id("meta-id");
1480
1481 let attributes = metadata_item.attributes();
1482
1483 assert!(attributes.iter().any(|(k, _)| k == &"property"));
1485 assert!(
1486 attributes
1487 .iter()
1488 .any(|(k, v)| k == &"id" && v == &"meta-id")
1489 );
1490 }
1491
1492 #[test]
1493 fn test_metadata_item_attributes_with_lang() {
1494 let mut metadata_item = MetadataItem::new("title", "Test Book");
1495 metadata_item.with_id("title-id").with_lang("en");
1496
1497 let attributes = metadata_item.attributes();
1498
1499 assert!(
1500 attributes
1501 .iter()
1502 .any(|(k, v)| k == &"id" && v == &"title-id")
1503 );
1504 assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1505 }
1506 }
1507
1508 mod metadata_refinement {
1509 use crate::types::MetadataRefinement;
1510
1511 #[test]
1512 fn test_metadata_refinement_new() {
1513 let refinement = MetadataRefinement::new("title", "title-type", "main");
1514
1515 assert_eq!(refinement.refines, "title");
1516 assert_eq!(refinement.property, "title-type");
1517 assert_eq!(refinement.value, "main");
1518 assert_eq!(refinement.lang, None);
1519 assert_eq!(refinement.scheme, None);
1520 }
1521
1522 #[test]
1523 fn test_metadata_refinement_with_lang() {
1524 let mut refinement = MetadataRefinement::new("creator", "role", "author");
1525 refinement.with_lang("en");
1526
1527 assert_eq!(refinement.refines, "creator");
1528 assert_eq!(refinement.property, "role");
1529 assert_eq!(refinement.value, "author");
1530 assert_eq!(refinement.lang, Some("en".to_string()));
1531 assert_eq!(refinement.scheme, None);
1532 }
1533
1534 #[test]
1535 fn test_metadata_refinement_with_scheme() {
1536 let mut refinement = MetadataRefinement::new("creator", "role", "author");
1537 refinement.with_scheme("marc:relators");
1538
1539 assert_eq!(refinement.refines, "creator");
1540 assert_eq!(refinement.property, "role");
1541 assert_eq!(refinement.value, "author");
1542 assert_eq!(refinement.lang, None);
1543 assert_eq!(refinement.scheme, Some("marc:relators".to_string()));
1544 }
1545
1546 #[test]
1547 fn test_metadata_refinement_build() {
1548 let mut refinement = MetadataRefinement::new("title", "alternate-script", "テスト");
1549 refinement.with_lang("ja").with_scheme("iso-15924");
1550
1551 let built = refinement.build();
1552
1553 assert_eq!(built.refines, "title");
1554 assert_eq!(built.property, "alternate-script");
1555 assert_eq!(built.value, "テスト");
1556 assert_eq!(built.lang, Some("ja".to_string()));
1557 assert_eq!(built.scheme, Some("iso-15924".to_string()));
1558 }
1559
1560 #[test]
1561 fn test_metadata_refinement_builder_chaining() {
1562 let mut refinement = MetadataRefinement::new("creator", "file-as", "Doe, John");
1563 refinement.with_lang("en").with_scheme("dcterms");
1564
1565 let built = refinement.build();
1566
1567 assert_eq!(built.refines, "creator");
1568 assert_eq!(built.property, "file-as");
1569 assert_eq!(built.value, "Doe, John");
1570 assert_eq!(built.lang, Some("en".to_string()));
1571 assert_eq!(built.scheme, Some("dcterms".to_string()));
1572 }
1573
1574 #[test]
1575 fn test_metadata_refinement_attributes() {
1576 let mut refinement = MetadataRefinement::new("title", "title-type", "main");
1577 refinement.with_lang("en").with_scheme("onix:codelist5");
1578
1579 let attributes = refinement.attributes();
1580
1581 assert!(
1582 attributes
1583 .iter()
1584 .any(|(k, v)| k == &"refines" && v == &"title")
1585 );
1586 assert!(
1587 attributes
1588 .iter()
1589 .any(|(k, v)| k == &"property" && v == &"title-type")
1590 );
1591 assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1592 assert!(
1593 attributes
1594 .iter()
1595 .any(|(k, v)| k == &"scheme" && v == &"onix:codelist5")
1596 );
1597 }
1598
1599 #[test]
1600 fn test_metadata_refinement_attributes_optional_fields() {
1601 let refinement = MetadataRefinement::new("creator", "role", "author");
1602 let attributes = refinement.attributes();
1603
1604 assert!(
1605 attributes
1606 .iter()
1607 .any(|(k, v)| k == &"refines" && v == &"creator")
1608 );
1609 assert!(
1610 attributes
1611 .iter()
1612 .any(|(k, v)| k == &"property" && v == &"role")
1613 );
1614
1615 assert!(!attributes.iter().any(|(k, _)| k == &"lang"));
1617 assert!(!attributes.iter().any(|(k, _)| k == &"scheme"));
1618 }
1619 }
1620
1621 mod manifest_item {
1622 use std::path::PathBuf;
1623
1624 use crate::types::ManifestItem;
1625
1626 #[test]
1627 fn test_manifest_item_new() {
1628 let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1629 assert!(manifest_item.is_ok());
1630
1631 let manifest_item = manifest_item.unwrap();
1632 assert_eq!(manifest_item.id, "cover");
1633 assert_eq!(manifest_item.path, PathBuf::from("images/cover.jpg"));
1634 assert_eq!(manifest_item.mime, "");
1635 assert_eq!(manifest_item.properties, None);
1636 assert_eq!(manifest_item.fallback, None);
1637 }
1638
1639 #[test]
1640 fn test_manifest_item_append_property() {
1641 let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1642 assert!(manifest_item.is_ok());
1643
1644 let mut manifest_item = manifest_item.unwrap();
1645 manifest_item.append_property("nav");
1646
1647 assert_eq!(manifest_item.id, "nav");
1648 assert_eq!(manifest_item.path, PathBuf::from("nav.xhtml"));
1649 assert_eq!(manifest_item.mime, "");
1650 assert_eq!(manifest_item.properties, Some("nav".to_string()));
1651 assert_eq!(manifest_item.fallback, None);
1652 }
1653
1654 #[test]
1655 fn test_manifest_item_append_multiple_properties() {
1656 let manifest_item = ManifestItem::new("content", "content.xhtml");
1657 assert!(manifest_item.is_ok());
1658
1659 let mut manifest_item = manifest_item.unwrap();
1660 manifest_item
1661 .append_property("nav")
1662 .append_property("scripted")
1663 .append_property("svg");
1664
1665 assert_eq!(
1666 manifest_item.properties,
1667 Some("nav scripted svg".to_string())
1668 );
1669 }
1670
1671 #[test]
1672 fn test_manifest_item_with_fallback() {
1673 let manifest_item = ManifestItem::new("image", "image.tiff");
1674 assert!(manifest_item.is_ok());
1675
1676 let mut manifest_item = manifest_item.unwrap();
1677 manifest_item.with_fallback("image-fallback");
1678
1679 assert_eq!(manifest_item.id, "image");
1680 assert_eq!(manifest_item.path, PathBuf::from("image.tiff"));
1681 assert_eq!(manifest_item.mime, "");
1682 assert_eq!(manifest_item.properties, None);
1683 assert_eq!(manifest_item.fallback, Some("image-fallback".to_string()));
1684 }
1685
1686 #[test]
1687 fn test_manifest_item_set_mime() {
1688 let manifest_item = ManifestItem::new("style", "style.css");
1689 assert!(manifest_item.is_ok());
1690
1691 let manifest_item = manifest_item.unwrap();
1692 let updated_item = manifest_item.set_mime("text/css");
1693
1694 assert_eq!(updated_item.id, "style");
1695 assert_eq!(updated_item.path, PathBuf::from("style.css"));
1696 assert_eq!(updated_item.mime, "text/css");
1697 assert_eq!(updated_item.properties, None);
1698 assert_eq!(updated_item.fallback, None);
1699 }
1700
1701 #[test]
1702 fn test_manifest_item_build() {
1703 let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1704 assert!(manifest_item.is_ok());
1705
1706 let mut manifest_item = manifest_item.unwrap();
1707 manifest_item
1708 .append_property("cover-image")
1709 .with_fallback("cover-fallback");
1710
1711 let built = manifest_item.build();
1712
1713 assert_eq!(built.id, "cover");
1714 assert_eq!(built.path, PathBuf::from("images/cover.jpg"));
1715 assert_eq!(built.mime, "");
1716 assert_eq!(built.properties, Some("cover-image".to_string()));
1717 assert_eq!(built.fallback, Some("cover-fallback".to_string()));
1718 }
1719
1720 #[test]
1721 fn test_manifest_item_builder_chaining() {
1722 let manifest_item = ManifestItem::new("content", "content.xhtml");
1723 assert!(manifest_item.is_ok());
1724
1725 let mut manifest_item = manifest_item.unwrap();
1726 manifest_item
1727 .append_property("scripted")
1728 .append_property("svg")
1729 .with_fallback("fallback-content");
1730
1731 let built = manifest_item.build();
1732
1733 assert_eq!(built.id, "content");
1734 assert_eq!(built.path, PathBuf::from("content.xhtml"));
1735 assert_eq!(built.mime, "");
1736 assert_eq!(built.properties, Some("scripted svg".to_string()));
1737 assert_eq!(built.fallback, Some("fallback-content".to_string()));
1738 }
1739
1740 #[test]
1741 fn test_manifest_item_attributes() {
1742 let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1743 assert!(manifest_item.is_ok());
1744
1745 let mut manifest_item = manifest_item.unwrap();
1746 manifest_item
1747 .append_property("nav")
1748 .with_fallback("fallback-nav");
1749
1750 let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1752 let attributes = manifest_item.attributes();
1753
1754 assert!(attributes.contains(&("id", "nav")));
1756 assert!(attributes.contains(&("href", "nav.xhtml")));
1757 assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1758 assert!(attributes.contains(&("properties", "nav")));
1759 assert!(attributes.contains(&("fallback", "fallback-nav")));
1760 }
1761
1762 #[test]
1763 fn test_manifest_item_attributes_optional_fields() {
1764 let manifest_item = ManifestItem::new("simple", "simple.xhtml");
1765 assert!(manifest_item.is_ok());
1766
1767 let manifest_item = manifest_item.unwrap();
1768 let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1769 let attributes = manifest_item.attributes();
1770
1771 assert!(attributes.contains(&("id", "simple")));
1773 assert!(attributes.contains(&("href", "simple.xhtml")));
1774 assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1775
1776 assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1778 assert!(!attributes.iter().any(|(k, _)| k == &"fallback"));
1779 }
1780
1781 #[test]
1782 fn test_manifest_item_path_handling() {
1783 let manifest_item = ManifestItem::new("test", "../images/test.png");
1784 assert!(manifest_item.is_err());
1785
1786 let err = manifest_item.unwrap_err();
1787 assert_eq!(
1788 err.to_string(),
1789 "Epub builder error: A manifest with id 'test' should not use a relative path starting with '../'."
1790 );
1791 }
1792 }
1793
1794 mod spine_item {
1795 use crate::types::SpineItem;
1796
1797 #[test]
1798 fn test_spine_item_new() {
1799 let spine_item = SpineItem::new("content_001");
1800
1801 assert_eq!(spine_item.idref, "content_001");
1802 assert_eq!(spine_item.id, None);
1803 assert_eq!(spine_item.properties, None);
1804 assert_eq!(spine_item.linear, true);
1805 }
1806
1807 #[test]
1808 fn test_spine_item_with_id() {
1809 let mut spine_item = SpineItem::new("content_001");
1810 spine_item.with_id("spine1");
1811
1812 assert_eq!(spine_item.idref, "content_001");
1813 assert_eq!(spine_item.id, Some("spine1".to_string()));
1814 assert_eq!(spine_item.properties, None);
1815 assert_eq!(spine_item.linear, true);
1816 }
1817
1818 #[test]
1819 fn test_spine_item_append_property() {
1820 let mut spine_item = SpineItem::new("content_001");
1821 spine_item.append_property("page-spread-left");
1822
1823 assert_eq!(spine_item.idref, "content_001");
1824 assert_eq!(spine_item.id, None);
1825 assert_eq!(spine_item.properties, Some("page-spread-left".to_string()));
1826 assert_eq!(spine_item.linear, true);
1827 }
1828
1829 #[test]
1830 fn test_spine_item_append_multiple_properties() {
1831 let mut spine_item = SpineItem::new("content_001");
1832 spine_item
1833 .append_property("page-spread-left")
1834 .append_property("rendition:layout-pre-paginated");
1835
1836 assert_eq!(
1837 spine_item.properties,
1838 Some("page-spread-left rendition:layout-pre-paginated".to_string())
1839 );
1840 }
1841
1842 #[test]
1843 fn test_spine_item_set_linear() {
1844 let mut spine_item = SpineItem::new("content_001");
1845 spine_item.set_linear(false);
1846
1847 assert_eq!(spine_item.idref, "content_001");
1848 assert_eq!(spine_item.id, None);
1849 assert_eq!(spine_item.properties, None);
1850 assert_eq!(spine_item.linear, false);
1851 }
1852
1853 #[test]
1854 fn test_spine_item_build() {
1855 let mut spine_item = SpineItem::new("content_001");
1856 spine_item
1857 .with_id("spine1")
1858 .append_property("page-spread-left")
1859 .set_linear(false);
1860
1861 let built = spine_item.build();
1862
1863 assert_eq!(built.idref, "content_001");
1864 assert_eq!(built.id, Some("spine1".to_string()));
1865 assert_eq!(built.properties, Some("page-spread-left".to_string()));
1866 assert_eq!(built.linear, false);
1867 }
1868
1869 #[test]
1870 fn test_spine_item_builder_chaining() {
1871 let mut spine_item = SpineItem::new("content_001");
1872 spine_item
1873 .with_id("spine1")
1874 .append_property("page-spread-left")
1875 .set_linear(false);
1876
1877 let built = spine_item.build();
1878
1879 assert_eq!(built.idref, "content_001");
1880 assert_eq!(built.id, Some("spine1".to_string()));
1881 assert_eq!(built.properties, Some("page-spread-left".to_string()));
1882 assert_eq!(built.linear, false);
1883 }
1884
1885 #[test]
1886 fn test_spine_item_attributes() {
1887 let mut spine_item = SpineItem::new("content_001");
1888 spine_item
1889 .with_id("spine1")
1890 .append_property("page-spread-left")
1891 .set_linear(false);
1892
1893 let attributes = spine_item.attributes();
1894
1895 assert!(attributes.contains(&("idref", "content_001")));
1897 assert!(attributes.contains(&("id", "spine1")));
1898 assert!(attributes.contains(&("properties", "page-spread-left")));
1899 assert!(attributes.contains(&("linear", "no"))); }
1901
1902 #[test]
1903 fn test_spine_item_attributes_linear_yes() {
1904 let spine_item = SpineItem::new("content_001");
1905 let attributes = spine_item.attributes();
1906
1907 assert!(attributes.contains(&("linear", "yes")));
1909 }
1910
1911 #[test]
1912 fn test_spine_item_attributes_optional_fields() {
1913 let spine_item = SpineItem::new("content_001");
1914 let attributes = spine_item.attributes();
1915
1916 assert!(attributes.contains(&("idref", "content_001")));
1918 assert!(attributes.contains(&("linear", "yes")));
1919
1920 assert!(!attributes.iter().any(|(k, _)| k == &"id"));
1922 assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1923 }
1924 }
1925
1926 mod navpoint {
1927
1928 use std::path::PathBuf;
1929
1930 use crate::types::NavPoint;
1931
1932 #[test]
1933 fn test_navpoint_new() {
1934 let navpoint = NavPoint::new("Test Chapter");
1935
1936 assert_eq!(navpoint.label, "Test Chapter");
1937 assert_eq!(navpoint.content, None);
1938 assert_eq!(navpoint.children.len(), 0);
1939 }
1940
1941 #[test]
1942 fn test_navpoint_with_content() {
1943 let mut navpoint = NavPoint::new("Test Chapter");
1944 navpoint.with_content("chapter1.html");
1945
1946 assert_eq!(navpoint.label, "Test Chapter");
1947 assert_eq!(navpoint.content, Some(PathBuf::from("chapter1.html")));
1948 assert_eq!(navpoint.children.len(), 0);
1949 }
1950
1951 #[test]
1952 fn test_navpoint_append_child() {
1953 let mut parent = NavPoint::new("Parent Chapter");
1954
1955 let mut child1 = NavPoint::new("Child Section 1");
1956 child1.with_content("section1.html");
1957
1958 let mut child2 = NavPoint::new("Child Section 2");
1959 child2.with_content("section2.html");
1960
1961 parent.append_child(child1.build());
1962 parent.append_child(child2.build());
1963
1964 assert_eq!(parent.children.len(), 2);
1965 assert_eq!(parent.children[0].label, "Child Section 1");
1966 assert_eq!(parent.children[1].label, "Child Section 2");
1967 }
1968
1969 #[test]
1970 fn test_navpoint_set_children() {
1971 let mut navpoint = NavPoint::new("Main Chapter");
1972 let children = vec![NavPoint::new("Section 1"), NavPoint::new("Section 2")];
1973
1974 navpoint.set_children(children);
1975
1976 assert_eq!(navpoint.children.len(), 2);
1977 assert_eq!(navpoint.children[0].label, "Section 1");
1978 assert_eq!(navpoint.children[1].label, "Section 2");
1979 }
1980
1981 #[test]
1982 fn test_navpoint_build() {
1983 let mut navpoint = NavPoint::new("Complete Chapter");
1984 navpoint.with_content("complete.html");
1985
1986 let child = NavPoint::new("Sub Section");
1987 navpoint.append_child(child.build());
1988
1989 let built = navpoint.build();
1990
1991 assert_eq!(built.label, "Complete Chapter");
1992 assert_eq!(built.content, Some(PathBuf::from("complete.html")));
1993 assert_eq!(built.children.len(), 1);
1994 assert_eq!(built.children[0].label, "Sub Section");
1995 }
1996
1997 #[test]
1998 fn test_navpoint_builder_chaining() {
1999 let mut navpoint = NavPoint::new("Chained Chapter");
2000
2001 navpoint
2002 .with_content("chained.html")
2003 .append_child(NavPoint::new("Child 1").build())
2004 .append_child(NavPoint::new("Child 2").build());
2005
2006 let built = navpoint.build();
2007
2008 assert_eq!(built.label, "Chained Chapter");
2009 assert_eq!(built.content, Some(PathBuf::from("chained.html")));
2010 assert_eq!(built.children.len(), 2);
2011 }
2012
2013 #[test]
2014 fn test_navpoint_empty_children() {
2015 let navpoint = NavPoint::new("No Children Chapter");
2016 let built = navpoint.build();
2017
2018 assert_eq!(built.children.len(), 0);
2019 }
2020
2021 #[test]
2022 fn test_navpoint_complex_hierarchy() {
2023 let mut root = NavPoint::new("Book");
2024
2025 let mut chapter1 = NavPoint::new("Chapter 1");
2026 chapter1
2027 .with_content("chapter1.html")
2028 .append_child(
2029 NavPoint::new("Section 1.1")
2030 .with_content("sec1_1.html")
2031 .build(),
2032 )
2033 .append_child(
2034 NavPoint::new("Section 1.2")
2035 .with_content("sec1_2.html")
2036 .build(),
2037 );
2038
2039 let mut chapter2 = NavPoint::new("Chapter 2");
2040 chapter2.with_content("chapter2.html").append_child(
2041 NavPoint::new("Section 2.1")
2042 .with_content("sec2_1.html")
2043 .build(),
2044 );
2045
2046 root.append_child(chapter1.build())
2047 .append_child(chapter2.build());
2048
2049 let book = root.build();
2050
2051 assert_eq!(book.label, "Book");
2052 assert_eq!(book.children.len(), 2);
2053
2054 let ch1 = &book.children[0];
2055 assert_eq!(ch1.label, "Chapter 1");
2056 assert_eq!(ch1.children.len(), 2);
2057
2058 let ch2 = &book.children[1];
2059 assert_eq!(ch2.label, "Chapter 2");
2060 assert_eq!(ch2.children.len(), 1);
2061 }
2062 }
2063 }
2064
2065 #[cfg(feature = "content-builder")]
2066 mod footnote_tests {
2067 use crate::types::Footnote;
2068
2069 #[test]
2070 fn test_footnote_basic_creation() {
2071 let footnote = Footnote {
2072 locate: 100,
2073 content: "Sample footnote".to_string(),
2074 };
2075
2076 assert_eq!(footnote.locate, 100);
2077 assert_eq!(footnote.content, "Sample footnote");
2078 }
2079
2080 #[test]
2081 fn test_footnote_equality() {
2082 let footnote1 = Footnote {
2083 locate: 100,
2084 content: "First note".to_string(),
2085 };
2086
2087 let footnote2 = Footnote {
2088 locate: 100,
2089 content: "First note".to_string(),
2090 };
2091
2092 let footnote3 = Footnote {
2093 locate: 100,
2094 content: "Different note".to_string(),
2095 };
2096
2097 let footnote4 = Footnote {
2098 locate: 200,
2099 content: "First note".to_string(),
2100 };
2101
2102 assert_eq!(footnote1, footnote2);
2103 assert_ne!(footnote1, footnote3);
2104 assert_ne!(footnote1, footnote4);
2105 }
2106
2107 #[test]
2108 fn test_footnote_ordering() {
2109 let footnote1 = Footnote {
2110 locate: 100,
2111 content: "First".to_string(),
2112 };
2113
2114 let footnote2 = Footnote {
2115 locate: 200,
2116 content: "Second".to_string(),
2117 };
2118
2119 let footnote3 = Footnote {
2120 locate: 150,
2121 content: "Middle".to_string(),
2122 };
2123
2124 assert!(footnote1 < footnote2);
2125 assert!(footnote2 > footnote1);
2126 assert!(footnote1 < footnote3);
2127 assert!(footnote3 < footnote2);
2128 assert_eq!(footnote1.cmp(&footnote1), std::cmp::Ordering::Equal);
2129 }
2130
2131 #[test]
2132 fn test_footnote_sorting() {
2133 let mut footnotes = vec![
2134 Footnote {
2135 locate: 300,
2136 content: "Third note".to_string(),
2137 },
2138 Footnote {
2139 locate: 100,
2140 content: "First note".to_string(),
2141 },
2142 Footnote {
2143 locate: 200,
2144 content: "Second note".to_string(),
2145 },
2146 ];
2147
2148 footnotes.sort();
2149
2150 assert_eq!(footnotes[0].locate, 100);
2151 assert_eq!(footnotes[1].locate, 200);
2152 assert_eq!(footnotes[2].locate, 300);
2153
2154 assert_eq!(footnotes[0].content, "First note");
2155 assert_eq!(footnotes[1].content, "Second note");
2156 assert_eq!(footnotes[2].content, "Third note");
2157 }
2158 }
2159
2160 #[cfg(feature = "content-builder")]
2161 mod block_type_tests {
2162 use crate::types::BlockType;
2163
2164 #[test]
2165 fn test_block_type_variants() {
2166 let _ = BlockType::Text;
2167 let _ = BlockType::Quote;
2168 let _ = BlockType::Title;
2169 let _ = BlockType::Image;
2170 let _ = BlockType::Audio;
2171 let _ = BlockType::Video;
2172 let _ = BlockType::MathML;
2173 }
2174
2175 #[test]
2176 fn test_block_type_debug() {
2177 let text = format!("{:?}", BlockType::Text);
2178 assert_eq!(text, "Text");
2179
2180 let quote = format!("{:?}", BlockType::Quote);
2181 assert_eq!(quote, "Quote");
2182
2183 let image = format!("{:?}", BlockType::Image);
2184 assert_eq!(image, "Image");
2185 }
2186 }
2187
2188 #[cfg(feature = "content-builder")]
2189 mod style_options_tests {
2190 use crate::types::{ColorScheme, PageLayout, StyleOptions, TextAlign, TextStyle};
2191
2192 #[test]
2193 fn test_style_options_default() {
2194 let options = StyleOptions::default();
2195
2196 assert_eq!(options.text.font_size, 1.0);
2197 assert_eq!(options.text.line_height, 1.6);
2198 assert_eq!(
2199 options.text.font_family,
2200 "-apple-system, Roboto, sans-serif"
2201 );
2202 assert_eq!(options.text.font_weight, "normal");
2203 assert_eq!(options.text.font_style, "normal");
2204 assert_eq!(options.text.letter_spacing, "normal");
2205 assert_eq!(options.text.text_indent, 2.0);
2206
2207 assert_eq!(options.color_scheme.background, "#FFFFFF");
2208 assert_eq!(options.color_scheme.text, "#000000");
2209 assert_eq!(options.color_scheme.link, "#6f6f6f");
2210
2211 assert_eq!(options.layout.margin, 20);
2212 assert_eq!(options.layout.text_align, TextAlign::Left);
2213 assert_eq!(options.layout.paragraph_spacing, 16);
2214 }
2215
2216 #[test]
2217 fn test_style_options_custom_values() {
2218 let text = TextStyle {
2219 font_size: 1.5,
2220 line_height: 2.0,
2221 font_family: "Georgia, serif".to_string(),
2222 font_weight: "bold".to_string(),
2223 font_style: "italic".to_string(),
2224 letter_spacing: "0.1em".to_string(),
2225 text_indent: 3.0,
2226 };
2227
2228 let color_scheme = ColorScheme {
2229 background: "#F0F0F0".to_string(),
2230 text: "#333333".to_string(),
2231 link: "#0066CC".to_string(),
2232 };
2233
2234 let layout = PageLayout {
2235 margin: 30,
2236 text_align: TextAlign::Center,
2237 paragraph_spacing: 20,
2238 };
2239
2240 let options = StyleOptions { text, color_scheme, layout };
2241
2242 assert_eq!(options.text.font_size, 1.5);
2243 assert_eq!(options.text.font_weight, "bold");
2244 assert_eq!(options.color_scheme.background, "#F0F0F0");
2245 assert_eq!(options.layout.text_align, TextAlign::Center);
2246 }
2247
2248 #[test]
2249 fn test_text_style_default() {
2250 let style = TextStyle::default();
2251
2252 assert_eq!(style.font_size, 1.0);
2253 assert_eq!(style.line_height, 1.6);
2254 assert_eq!(style.font_family, "-apple-system, Roboto, sans-serif");
2255 assert_eq!(style.font_weight, "normal");
2256 assert_eq!(style.font_style, "normal");
2257 assert_eq!(style.letter_spacing, "normal");
2258 assert_eq!(style.text_indent, 2.0);
2259 }
2260
2261 #[test]
2262 fn test_text_style_custom_values() {
2263 let style = TextStyle {
2264 font_size: 2.0,
2265 line_height: 1.8,
2266 font_family: "Times New Roman".to_string(),
2267 font_weight: "bold".to_string(),
2268 font_style: "italic".to_string(),
2269 letter_spacing: "0.05em".to_string(),
2270 text_indent: 0.0,
2271 };
2272
2273 assert_eq!(style.font_size, 2.0);
2274 assert_eq!(style.line_height, 1.8);
2275 assert_eq!(style.font_family, "Times New Roman");
2276 assert_eq!(style.font_weight, "bold");
2277 assert_eq!(style.font_style, "italic");
2278 assert_eq!(style.letter_spacing, "0.05em");
2279 assert_eq!(style.text_indent, 0.0);
2280 }
2281
2282 #[test]
2283 fn test_text_style_debug() {
2284 let style = TextStyle::default();
2285 let debug_str = format!("{:?}", style);
2286 assert!(debug_str.contains("TextStyle"));
2287 assert!(debug_str.contains("font_size"));
2288 }
2289
2290 #[test]
2291 fn test_color_scheme_default() {
2292 let scheme = ColorScheme::default();
2293
2294 assert_eq!(scheme.background, "#FFFFFF");
2295 assert_eq!(scheme.text, "#000000");
2296 assert_eq!(scheme.link, "#6f6f6f");
2297 }
2298
2299 #[test]
2300 fn test_color_scheme_custom_values() {
2301 let scheme = ColorScheme {
2302 background: "#000000".to_string(),
2303 text: "#FFFFFF".to_string(),
2304 link: "#00FF00".to_string(),
2305 };
2306
2307 assert_eq!(scheme.background, "#000000");
2308 assert_eq!(scheme.text, "#FFFFFF");
2309 assert_eq!(scheme.link, "#00FF00");
2310 }
2311
2312 #[test]
2313 fn test_color_scheme_debug() {
2314 let scheme = ColorScheme::default();
2315 let debug_str = format!("{:?}", scheme);
2316 assert!(debug_str.contains("ColorScheme"));
2317 assert!(debug_str.contains("background"));
2318 }
2319
2320 #[test]
2321 fn test_page_layout_default() {
2322 let layout = PageLayout::default();
2323
2324 assert_eq!(layout.margin, 20);
2325 assert_eq!(layout.text_align, TextAlign::Left);
2326 assert_eq!(layout.paragraph_spacing, 16);
2327 }
2328
2329 #[test]
2330 fn test_page_layout_custom_values() {
2331 let layout = PageLayout {
2332 margin: 40,
2333 text_align: TextAlign::Justify,
2334 paragraph_spacing: 24,
2335 };
2336
2337 assert_eq!(layout.margin, 40);
2338 assert_eq!(layout.text_align, TextAlign::Justify);
2339 assert_eq!(layout.paragraph_spacing, 24);
2340 }
2341
2342 #[test]
2343 fn test_page_layout_debug() {
2344 let layout = PageLayout::default();
2345 let debug_str = format!("{:?}", layout);
2346 assert!(debug_str.contains("PageLayout"));
2347 assert!(debug_str.contains("margin"));
2348 }
2349
2350 #[test]
2351 fn test_text_align_default() {
2352 let align = TextAlign::default();
2353 assert_eq!(align, TextAlign::Left);
2354 }
2355
2356 #[test]
2357 fn test_text_align_display() {
2358 assert_eq!(TextAlign::Left.to_string(), "left");
2359 assert_eq!(TextAlign::Right.to_string(), "right");
2360 assert_eq!(TextAlign::Justify.to_string(), "justify");
2361 assert_eq!(TextAlign::Center.to_string(), "center");
2362 }
2363
2364 #[test]
2365 fn test_text_align_all_variants() {
2366 let left = TextAlign::Left;
2367 let right = TextAlign::Right;
2368 let justify = TextAlign::Justify;
2369 let center = TextAlign::Center;
2370
2371 assert!(matches!(left, TextAlign::Left));
2372 assert!(matches!(right, TextAlign::Right));
2373 assert!(matches!(justify, TextAlign::Justify));
2374 assert!(matches!(center, TextAlign::Center));
2375 }
2376
2377 #[test]
2378 fn test_text_align_debug() {
2379 assert_eq!(format!("{:?}", TextAlign::Left), "Left");
2380 assert_eq!(format!("{:?}", TextAlign::Right), "Right");
2381 assert_eq!(format!("{:?}", TextAlign::Justify), "Justify");
2382 assert_eq!(format!("{:?}", TextAlign::Center), "Center");
2383 }
2384
2385 #[test]
2386 fn test_style_options_builder_new() {
2387 let options = StyleOptions::new();
2388 assert_eq!(options.text.font_size, 1.0);
2389 assert_eq!(options.color_scheme.background, "#FFFFFF");
2390 assert_eq!(options.layout.margin, 20);
2391 }
2392
2393 #[test]
2394 fn test_style_options_builder_with_text() {
2395 let mut options = StyleOptions::new();
2396 let text_style = TextStyle::new()
2397 .with_font_size(2.0)
2398 .with_font_weight("bold")
2399 .build();
2400 options.with_text(text_style);
2401
2402 assert_eq!(options.text.font_size, 2.0);
2403 assert_eq!(options.text.font_weight, "bold");
2404 }
2405
2406 #[test]
2407 fn test_style_options_builder_with_color_scheme() {
2408 let mut options = StyleOptions::new();
2409 let color = ColorScheme::new()
2410 .with_background("#000000")
2411 .with_text("#FFFFFF")
2412 .build();
2413 options.with_color_scheme(color);
2414
2415 assert_eq!(options.color_scheme.background, "#000000");
2416 assert_eq!(options.color_scheme.text, "#FFFFFF");
2417 }
2418
2419 #[test]
2420 fn test_style_options_builder_with_layout() {
2421 let mut options = StyleOptions::new();
2422 let layout = PageLayout::new()
2423 .with_margin(40)
2424 .with_text_align(TextAlign::Justify)
2425 .with_paragraph_spacing(24)
2426 .build();
2427 options.with_layout(layout);
2428
2429 assert_eq!(options.layout.margin, 40);
2430 assert_eq!(options.layout.text_align, TextAlign::Justify);
2431 assert_eq!(options.layout.paragraph_spacing, 24);
2432 }
2433
2434 #[test]
2435 fn test_style_options_builder_build() {
2436 let options = StyleOptions::new()
2437 .with_text(TextStyle::new().with_font_size(1.5).build())
2438 .with_color_scheme(ColorScheme::new().with_link("#FF0000").build())
2439 .with_layout(PageLayout::new().with_margin(30).build())
2440 .build();
2441
2442 assert_eq!(options.text.font_size, 1.5);
2443 assert_eq!(options.color_scheme.link, "#FF0000");
2444 assert_eq!(options.layout.margin, 30);
2445 }
2446
2447 #[test]
2448 fn test_style_options_builder_chaining() {
2449 let options = StyleOptions::new()
2450 .with_text(
2451 TextStyle::new()
2452 .with_font_size(1.5)
2453 .with_line_height(2.0)
2454 .with_font_family("Arial")
2455 .with_font_weight("bold")
2456 .with_font_style("italic")
2457 .with_letter_spacing("0.1em")
2458 .with_text_indent(1.5)
2459 .build(),
2460 )
2461 .with_color_scheme(
2462 ColorScheme::new()
2463 .with_background("#CCCCCC")
2464 .with_text("#111111")
2465 .with_link("#0000FF")
2466 .build(),
2467 )
2468 .with_layout(
2469 PageLayout::new()
2470 .with_margin(25)
2471 .with_text_align(TextAlign::Right)
2472 .with_paragraph_spacing(20)
2473 .build(),
2474 )
2475 .build();
2476
2477 assert_eq!(options.text.font_size, 1.5);
2478 assert_eq!(options.text.line_height, 2.0);
2479 assert_eq!(options.text.font_family, "Arial");
2480 assert_eq!(options.text.font_weight, "bold");
2481 assert_eq!(options.text.font_style, "italic");
2482 assert_eq!(options.text.letter_spacing, "0.1em");
2483 assert_eq!(options.text.text_indent, 1.5);
2484
2485 assert_eq!(options.color_scheme.background, "#CCCCCC");
2486 assert_eq!(options.color_scheme.text, "#111111");
2487 assert_eq!(options.color_scheme.link, "#0000FF");
2488
2489 assert_eq!(options.layout.margin, 25);
2490 assert_eq!(options.layout.text_align, TextAlign::Right);
2491 assert_eq!(options.layout.paragraph_spacing, 20);
2492 }
2493
2494 #[test]
2495 fn test_text_style_builder_new() {
2496 let style = TextStyle::new();
2497 assert_eq!(style.font_size, 1.0);
2498 assert_eq!(style.line_height, 1.6);
2499 }
2500
2501 #[test]
2502 fn test_text_style_builder_with_font_size() {
2503 let mut style = TextStyle::new();
2504 style.with_font_size(2.5);
2505 assert_eq!(style.font_size, 2.5);
2506 }
2507
2508 #[test]
2509 fn test_text_style_builder_with_line_height() {
2510 let mut style = TextStyle::new();
2511 style.with_line_height(2.0);
2512 assert_eq!(style.line_height, 2.0);
2513 }
2514
2515 #[test]
2516 fn test_text_style_builder_with_font_family() {
2517 let mut style = TextStyle::new();
2518 style.with_font_family("Helvetica, Arial");
2519 assert_eq!(style.font_family, "Helvetica, Arial");
2520 }
2521
2522 #[test]
2523 fn test_text_style_builder_with_font_weight() {
2524 let mut style = TextStyle::new();
2525 style.with_font_weight("bold");
2526 assert_eq!(style.font_weight, "bold");
2527 }
2528
2529 #[test]
2530 fn test_text_style_builder_with_font_style() {
2531 let mut style = TextStyle::new();
2532 style.with_font_style("italic");
2533 assert_eq!(style.font_style, "italic");
2534 }
2535
2536 #[test]
2537 fn test_text_style_builder_with_letter_spacing() {
2538 let mut style = TextStyle::new();
2539 style.with_letter_spacing("0.05em");
2540 assert_eq!(style.letter_spacing, "0.05em");
2541 }
2542
2543 #[test]
2544 fn test_text_style_builder_with_text_indent() {
2545 let mut style = TextStyle::new();
2546 style.with_text_indent(3.0);
2547 assert_eq!(style.text_indent, 3.0);
2548 }
2549
2550 #[test]
2551 fn test_text_style_builder_build() {
2552 let style = TextStyle::new()
2553 .with_font_size(1.8)
2554 .with_line_height(1.9)
2555 .build();
2556
2557 assert_eq!(style.font_size, 1.8);
2558 assert_eq!(style.line_height, 1.9);
2559 }
2560
2561 #[test]
2562 fn test_text_style_builder_chaining() {
2563 let style = TextStyle::new()
2564 .with_font_size(2.0)
2565 .with_line_height(1.8)
2566 .with_font_family("Georgia")
2567 .with_font_weight("bold")
2568 .with_font_style("italic")
2569 .with_letter_spacing("0.1em")
2570 .with_text_indent(0.5)
2571 .build();
2572
2573 assert_eq!(style.font_size, 2.0);
2574 assert_eq!(style.line_height, 1.8);
2575 assert_eq!(style.font_family, "Georgia");
2576 assert_eq!(style.font_weight, "bold");
2577 assert_eq!(style.font_style, "italic");
2578 assert_eq!(style.letter_spacing, "0.1em");
2579 assert_eq!(style.text_indent, 0.5);
2580 }
2581
2582 #[test]
2583 fn test_color_scheme_builder_new() {
2584 let scheme = ColorScheme::new();
2585 assert_eq!(scheme.background, "#FFFFFF");
2586 assert_eq!(scheme.text, "#000000");
2587 }
2588
2589 #[test]
2590 fn test_color_scheme_builder_with_background() {
2591 let mut scheme = ColorScheme::new();
2592 scheme.with_background("#FF0000");
2593 assert_eq!(scheme.background, "#FF0000");
2594 }
2595
2596 #[test]
2597 fn test_color_scheme_builder_with_text() {
2598 let mut scheme = ColorScheme::new();
2599 scheme.with_text("#333333");
2600 assert_eq!(scheme.text, "#333333");
2601 }
2602
2603 #[test]
2604 fn test_color_scheme_builder_with_link() {
2605 let mut scheme = ColorScheme::new();
2606 scheme.with_link("#0000FF");
2607 assert_eq!(scheme.link, "#0000FF");
2608 }
2609
2610 #[test]
2611 fn test_color_scheme_builder_build() {
2612 let scheme = ColorScheme::new().with_background("#123456").build();
2613
2614 assert_eq!(scheme.background, "#123456");
2615 assert_eq!(scheme.text, "#000000");
2616 }
2617
2618 #[test]
2619 fn test_color_scheme_builder_chaining() {
2620 let scheme = ColorScheme::new()
2621 .with_background("#AABBCC")
2622 .with_text("#DDEEFF")
2623 .with_link("#112233")
2624 .build();
2625
2626 assert_eq!(scheme.background, "#AABBCC");
2627 assert_eq!(scheme.text, "#DDEEFF");
2628 assert_eq!(scheme.link, "#112233");
2629 }
2630
2631 #[test]
2632 fn test_page_layout_builder_new() {
2633 let layout = PageLayout::new();
2634 assert_eq!(layout.margin, 20);
2635 assert_eq!(layout.text_align, TextAlign::Left);
2636 assert_eq!(layout.paragraph_spacing, 16);
2637 }
2638
2639 #[test]
2640 fn test_page_layout_builder_with_margin() {
2641 let mut layout = PageLayout::new();
2642 layout.with_margin(50);
2643 assert_eq!(layout.margin, 50);
2644 }
2645
2646 #[test]
2647 fn test_page_layout_builder_with_text_align() {
2648 let mut layout = PageLayout::new();
2649 layout.with_text_align(TextAlign::Center);
2650 assert_eq!(layout.text_align, TextAlign::Center);
2651 }
2652
2653 #[test]
2654 fn test_page_layout_builder_with_paragraph_spacing() {
2655 let mut layout = PageLayout::new();
2656 layout.with_paragraph_spacing(30);
2657 assert_eq!(layout.paragraph_spacing, 30);
2658 }
2659
2660 #[test]
2661 fn test_page_layout_builder_build() {
2662 let layout = PageLayout::new().with_margin(35).build();
2663
2664 assert_eq!(layout.margin, 35);
2665 assert_eq!(layout.text_align, TextAlign::Left);
2666 }
2667
2668 #[test]
2669 fn test_page_layout_builder_chaining() {
2670 let layout = PageLayout::new()
2671 .with_margin(45)
2672 .with_text_align(TextAlign::Justify)
2673 .with_paragraph_spacing(28)
2674 .build();
2675
2676 assert_eq!(layout.margin, 45);
2677 assert_eq!(layout.text_align, TextAlign::Justify);
2678 assert_eq!(layout.paragraph_spacing, 28);
2679 }
2680
2681 #[test]
2682 fn test_page_layout_builder_all_text_align_variants() {
2683 let left = PageLayout::new().with_text_align(TextAlign::Left).build();
2684 assert_eq!(left.text_align, TextAlign::Left);
2685
2686 let right = PageLayout::new().with_text_align(TextAlign::Right).build();
2687 assert_eq!(right.text_align, TextAlign::Right);
2688
2689 let center = PageLayout::new().with_text_align(TextAlign::Center).build();
2690 assert_eq!(center.text_align, TextAlign::Center);
2691
2692 let justify = PageLayout::new()
2693 .with_text_align(TextAlign::Justify)
2694 .build();
2695 assert_eq!(justify.text_align, TextAlign::Justify);
2696 }
2697 }
2698}