1use std::{
37 collections::HashMap,
38 env,
39 fs::{self, File},
40 io::{Cursor, Read},
41 path::{Path, PathBuf},
42};
43
44use infer::{Infer, MatcherType};
45use log::warn;
46use quick_xml::{
47 Reader, Writer,
48 events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event},
49};
50
51use crate::{
52 builder::XmlWriter,
53 error::{EpubBuilderError, EpubError},
54 types::{BlockType, Footnote},
55 utils::local_time,
56};
57
58#[non_exhaustive]
63#[derive(Debug)]
64pub enum Block {
65 #[non_exhaustive]
69 Text {
70 content: String,
71 footnotes: Vec<Footnote>,
72 },
73
74 #[non_exhaustive]
78 Quote {
79 content: String,
80 footnotes: Vec<Footnote>,
81 },
82
83 #[non_exhaustive]
85 Title {
86 content: String,
87 footnotes: Vec<Footnote>,
88
89 level: usize,
93 },
94
95 #[non_exhaustive]
97 Image {
98 url: PathBuf,
100
101 alt: Option<String>,
103
104 caption: Option<String>,
106
107 footnotes: Vec<Footnote>,
108 },
109
110 #[non_exhaustive]
112 Audio {
113 url: PathBuf,
115
116 fallback: String,
120
121 caption: Option<String>,
123
124 footnotes: Vec<Footnote>,
125 },
126
127 #[non_exhaustive]
129 Video {
130 url: PathBuf,
132
133 fallback: String,
137
138 caption: Option<String>,
140
141 footnotes: Vec<Footnote>,
142 },
143
144 #[non_exhaustive]
146 MathML {
147 element_str: String,
152
153 fallback_image: Option<PathBuf>,
158
159 caption: Option<String>,
161
162 footnotes: Vec<Footnote>,
163 },
164}
165
166impl Block {
167 pub(crate) fn make(
171 &mut self,
172 writer: &mut XmlWriter,
173 start_index: usize,
174 ) -> Result<(), EpubError> {
175 match self {
176 Block::Text { content, footnotes } => {
177 writer.write_event(Event::Start(
178 BytesStart::new("p").with_attributes([("class", "content-block")]),
179 ))?;
180
181 Self::make_text(writer, content, footnotes, start_index)?;
182
183 writer.write_event(Event::End(BytesEnd::new("p")))?;
184 }
185
186 Block::Quote { content, footnotes } => {
187 writer.write_event(Event::Start(BytesStart::new("blockquote").with_attributes(
188 [
189 ("class", "content-block"),
190 ("cite", "SOME ATTR NEED TO BE SET"),
191 ],
192 )))?;
193 writer.write_event(Event::Start(BytesStart::new("p")))?;
194
195 Self::make_text(writer, content, footnotes, start_index)?;
196
197 writer.write_event(Event::End(BytesEnd::new("p")))?;
198 writer.write_event(Event::End(BytesEnd::new("blockquote")))?;
199 }
200
201 Block::Title { content, footnotes, level } => {
202 let tag_name = format!("h{}", level);
203 writer.write_event(Event::Start(
204 BytesStart::new(tag_name.as_str())
205 .with_attributes([("class", "content-block")]),
206 ))?;
207
208 Self::make_text(writer, content, footnotes, start_index)?;
209
210 writer.write_event(Event::End(BytesEnd::new(tag_name)))?;
211 }
212
213 Block::Image { url, alt, caption, footnotes } => {
214 let url = format!("./img/{}", url.file_name().unwrap().to_string_lossy());
215
216 let mut attr = Vec::new();
217 attr.push(("src", url.as_str()));
218 attr.push(("class", "image-block"));
219 if let Some(alt) = alt {
220 attr.push(("alt", alt.as_str()));
221 }
222
223 writer.write_event(Event::Start(
224 BytesStart::new("figure").with_attributes([("class", "content-block")]),
225 ))?;
226 writer.write_event(Event::Empty(BytesStart::new("img").with_attributes(attr)))?;
227
228 if let Some(caption) = caption {
229 writer.write_event(Event::Start(BytesStart::new("figcaption")))?;
230
231 Self::make_text(writer, caption, footnotes, start_index)?;
232
233 writer.write_event(Event::End(BytesEnd::new("figcaption")))?;
234 }
235
236 writer.write_event(Event::End(BytesEnd::new("figure")))?;
237 }
238
239 Block::Audio { url, fallback, caption, footnotes } => {
240 let url = format!("./audio/{}", url.file_name().unwrap().to_string_lossy());
241
242 let attr = vec![
243 ("src", url.as_str()),
244 ("class", "audio-block"),
245 ("controls", "controls"), ];
247
248 writer.write_event(Event::Start(
249 BytesStart::new("figure").with_attributes([("class", "content-block")]),
250 ))?;
251 writer.write_event(Event::Start(BytesStart::new("audio").with_attributes(attr)))?;
252
253 writer.write_event(Event::Start(BytesStart::new("p")))?;
254 writer.write_event(Event::Text(BytesText::new(fallback.as_str())))?;
255 writer.write_event(Event::End(BytesEnd::new("p")))?;
256
257 writer.write_event(Event::End(BytesEnd::new("audio")))?;
258
259 if let Some(caption) = caption {
260 writer.write_event(Event::Start(BytesStart::new("figcaption")))?;
261
262 Self::make_text(writer, caption, footnotes, start_index)?;
263
264 writer.write_event(Event::End(BytesEnd::new("figcaption")))?;
265 }
266
267 writer.write_event(Event::End(BytesEnd::new("figure")))?;
268 }
269
270 Block::Video { url, fallback, caption, footnotes } => {
271 let url = format!("./video/{}", url.file_name().unwrap().to_string_lossy());
272
273 let attr = vec![
274 ("src", url.as_str()),
275 ("class", "video-block"),
276 ("controls", "controls"), ];
278
279 writer.write_event(Event::Start(
280 BytesStart::new("figure").with_attributes([("class", "content-block")]),
281 ))?;
282 writer.write_event(Event::Start(BytesStart::new("video").with_attributes(attr)))?;
283
284 writer.write_event(Event::Start(BytesStart::new("p")))?;
285 writer.write_event(Event::Text(BytesText::new(fallback.as_str())))?;
286 writer.write_event(Event::End(BytesEnd::new("p")))?;
287
288 writer.write_event(Event::End(BytesEnd::new("video")))?;
289
290 if let Some(caption) = caption {
291 writer.write_event(Event::Start(BytesStart::new("figcaption")))?;
292
293 Self::make_text(writer, caption, footnotes, start_index)?;
294
295 writer.write_event(Event::End(BytesEnd::new("figcaption")))?;
296 }
297
298 writer.write_event(Event::End(BytesEnd::new("figure")))?;
299 }
300
301 Block::MathML {
302 element_str,
303 fallback_image,
304 caption,
305 footnotes,
306 } => {
307 writer.write_event(Event::Start(
308 BytesStart::new("figure").with_attributes([("class", "content-block")]),
309 ))?;
310
311 Self::write_mathml_element(writer, element_str)?;
312
313 if let Some(fallback_path) = fallback_image {
314 let img_url = format!(
315 "./img/{}",
316 fallback_path.file_name().unwrap().to_string_lossy()
317 );
318
319 writer.write_event(Event::Empty(BytesStart::new("img").with_attributes([
320 ("src", img_url.as_str()),
321 ("class", "mathml-fallback"),
322 ("alt", "Mathematical formula"),
323 ])))?;
324 }
325
326 if let Some(caption) = caption {
327 writer.write_event(Event::Start(BytesStart::new("figcaption")))?;
328
329 Self::make_text(writer, caption, footnotes, start_index)?;
330
331 writer.write_event(Event::End(BytesEnd::new("figcaption")))?;
332 }
333
334 writer.write_event(Event::End(BytesEnd::new("figure")))?;
335 }
336 }
337
338 Ok(())
339 }
340
341 pub fn take_footnotes(&self) -> Vec<Footnote> {
342 match self {
343 Block::Text { footnotes, .. } => footnotes.to_vec(),
344 Block::Quote { footnotes, .. } => footnotes.to_vec(),
345 Block::Title { footnotes, .. } => footnotes.to_vec(),
346 Block::Image { footnotes, .. } => footnotes.to_vec(),
347 Block::Audio { footnotes, .. } => footnotes.to_vec(),
348 Block::Video { footnotes, .. } => footnotes.to_vec(),
349 Block::MathML { footnotes, .. } => footnotes.to_vec(),
350 }
351 }
352
353 fn split_content_by_index(content: &str, index_list: &[usize]) -> Vec<String> {
359 if index_list.is_empty() {
360 return vec![content.to_string()];
361 }
362
363 let mut result = Vec::with_capacity(index_list.len() + 1);
365 let mut char_iter = content.chars().enumerate();
366
367 let mut current_char_idx = 0;
368 for &target_idx in index_list {
369 let mut segment = String::new();
370
371 while current_char_idx < target_idx {
374 if let Some((_, ch)) = char_iter.next() {
375 segment.push(ch);
376 current_char_idx += 1;
377 } else {
378 break;
379 }
380 }
381
382 if !segment.is_empty() {
383 result.push(segment);
384 }
385 }
386
387 let remainder = char_iter.map(|(_, ch)| ch).collect::<String>();
388 if !remainder.is_empty() {
389 result.push(remainder);
390 }
391
392 result
393 }
394
395 fn make_text(
405 writer: &mut XmlWriter,
406 content: &str,
407 footnotes: &mut [Footnote],
408 start_index: usize,
409 ) -> Result<(), EpubError> {
410 if footnotes.is_empty() {
411 writer.write_event(Event::Text(BytesText::new(content)))?;
412 return Ok(());
413 }
414
415 footnotes.sort_unstable();
416
417 let mut position_to_count = HashMap::new();
419 for footnote in footnotes.iter() {
420 *position_to_count.entry(footnote.locate).or_insert(0usize) += 1;
421 }
422
423 let mut positions = position_to_count.keys().copied().collect::<Vec<usize>>();
424 positions.sort_unstable();
425
426 let mut current_index = start_index;
427 let content_list = Self::split_content_by_index(content, &positions);
428 for (index, segment) in content_list.iter().enumerate() {
429 writer.write_event(Event::Text(BytesText::new(segment)))?;
430
431 if let Some(&position) = positions.get(index) {
433 if let Some(&count) = position_to_count.get(&position) {
435 for _ in 0..count {
436 Self::make_footnotes(writer, current_index)?;
437 current_index += 1;
438 }
439 }
440 }
441 }
442
443 Ok(())
444 }
445
446 #[inline]
448 fn make_footnotes(writer: &mut XmlWriter, index: usize) -> Result<(), EpubError> {
449 writer.write_event(Event::Start(BytesStart::new("a").with_attributes([
450 ("href", format!("#footnote-{}", index).as_str()),
451 ("id", format!("ref-{}", index).as_str()),
452 ("class", "footnote-ref"),
453 ])))?;
454 writer.write_event(Event::Text(BytesText::new(&format!("[{}]", index))))?;
455 writer.write_event(Event::End(BytesEnd::new("a")))?;
456
457 Ok(())
458 }
459
460 fn write_mathml_element(writer: &mut XmlWriter, element_str: &str) -> Result<(), EpubError> {
464 let mut reader = Reader::from_str(element_str);
465
466 loop {
467 match reader.read_event() {
468 Ok(Event::Eof) => break,
469
470 Ok(event) => writer.write_event(event)?,
471
472 Err(err) => {
473 return Err(
474 EpubBuilderError::InvalidMathMLFormat { error: err.to_string() }.into(),
475 );
476 }
477 }
478 }
479
480 Ok(())
481 }
482}
483
484pub struct BlockBuilder {
509 block_type: BlockType,
511
512 content: Option<String>,
514
515 level: Option<usize>,
517
518 url: Option<PathBuf>,
520
521 alt: Option<String>,
523
524 caption: Option<String>,
526
527 fallback: Option<String>,
529
530 element_str: Option<String>,
532
533 fallback_image: Option<PathBuf>,
535
536 footnotes: Vec<Footnote>,
538}
539
540impl BlockBuilder {
541 pub fn new(block_type: BlockType) -> Self {
548 Self {
549 block_type,
550 content: None,
551 level: None,
552 url: None,
553 alt: None,
554 caption: None,
555 fallback: None,
556 element_str: None,
557 fallback_image: None,
558 footnotes: vec![],
559 }
560 }
561
562 pub fn set_content(&mut self, content: &str) -> &mut Self {
569 self.content = Some(content.to_string());
570 self
571 }
572
573 pub fn set_title_level(&mut self, level: usize) -> &mut Self {
582 if !(1..=6).contains(&level) {
583 return self;
584 }
585
586 self.level = Some(level);
587 self
588 }
589
590 pub fn set_url(&mut self, url: &PathBuf) -> Result<&mut Self, EpubError> {
602 match Self::is_target_type(
603 url,
604 vec![MatcherType::Image, MatcherType::Audio, MatcherType::Video],
605 ) {
606 Ok(_) => {
607 self.url = Some(url.to_path_buf());
608 Ok(self)
609 }
610 Err(err) => Err(err),
611 }
612 }
613
614 pub fn set_alt(&mut self, alt: &str) -> &mut Self {
622 self.alt = Some(alt.to_string());
623 self
624 }
625
626 pub fn set_caption(&mut self, caption: &str) -> &mut Self {
634 self.caption = Some(caption.to_string());
635 self
636 }
637
638 pub fn set_fallback(&mut self, fallback: &str) -> &mut Self {
646 self.fallback = Some(fallback.to_string());
647 self
648 }
649
650 pub fn set_mathml_element(&mut self, element_str: &str) -> &mut Self {
659 self.element_str = Some(element_str.to_string());
660 self
661 }
662
663 pub fn set_fallback_image(&mut self, fallback_image: PathBuf) -> Result<&mut Self, EpubError> {
676 match Self::is_target_type(&fallback_image, vec![MatcherType::Image]) {
677 Ok(_) => {
678 self.fallback_image = Some(fallback_image);
679 Ok(self)
680 }
681 Err(err) => Err(err),
682 }
683 }
684
685 pub fn add_footnote(&mut self, footnote: Footnote) -> &mut Self {
693 self.footnotes.push(footnote);
694 self
695 }
696
697 pub fn set_footnotes(&mut self, footnotes: Vec<Footnote>) -> &mut Self {
705 self.footnotes = footnotes;
706 self
707 }
708
709 pub fn remove_last_footnote(&mut self) -> &mut Self {
714 self.footnotes.pop();
715 self
716 }
717
718 pub fn clear_footnotes(&mut self) -> &mut Self {
722 self.footnotes.clear();
723 self
724 }
725
726 pub fn build(self) -> Result<Block, EpubError> {
736 let block = match self.block_type {
737 BlockType::Text => {
738 if let Some(content) = self.content {
739 Block::Text { content, footnotes: self.footnotes }
740 } else {
741 return Err(EpubBuilderError::MissingNecessaryBlockData {
742 block_type: "Text".to_string(),
743 missing_data: "'content'".to_string(),
744 }
745 .into());
746 }
747 }
748
749 BlockType::Quote => {
750 if let Some(content) = self.content {
751 Block::Quote { content, footnotes: self.footnotes }
752 } else {
753 return Err(EpubBuilderError::MissingNecessaryBlockData {
754 block_type: "Quote".to_string(),
755 missing_data: "'content'".to_string(),
756 }
757 .into());
758 }
759 }
760
761 BlockType::Title => match (self.content, self.level) {
762 (Some(content), Some(level)) => Block::Title {
763 content,
764 level,
765 footnotes: self.footnotes,
766 },
767 _ => {
768 return Err(EpubBuilderError::MissingNecessaryBlockData {
769 block_type: "Title".to_string(),
770 missing_data: "'content' or 'level'".to_string(),
771 }
772 .into());
773 }
774 },
775
776 BlockType::Image => {
777 if let Some(url) = self.url {
778 Block::Image {
779 url,
780 alt: self.alt,
781 caption: self.caption,
782 footnotes: self.footnotes,
783 }
784 } else {
785 return Err(EpubBuilderError::MissingNecessaryBlockData {
786 block_type: "Image".to_string(),
787 missing_data: "'url'".to_string(),
788 }
789 .into());
790 }
791 }
792
793 BlockType::Audio => match (self.url, self.fallback) {
794 (Some(url), Some(fallback)) => Block::Audio {
795 url,
796 fallback,
797 caption: self.caption,
798 footnotes: self.footnotes,
799 },
800 _ => {
801 return Err(EpubBuilderError::MissingNecessaryBlockData {
802 block_type: "Audio".to_string(),
803 missing_data: "'url' or 'fallback'".to_string(),
804 }
805 .into());
806 }
807 },
808
809 BlockType::Video => match (self.url, self.fallback) {
810 (Some(url), Some(fallback)) => Block::Video {
811 url,
812 fallback,
813 caption: self.caption,
814 footnotes: self.footnotes,
815 },
816 _ => {
817 return Err(EpubBuilderError::MissingNecessaryBlockData {
818 block_type: "Video".to_string(),
819 missing_data: "'url' or 'fallback'".to_string(),
820 }
821 .into());
822 }
823 },
824
825 BlockType::MathML => {
826 if let Some(element_str) = self.element_str {
827 Block::MathML {
828 element_str,
829 fallback_image: self.fallback_image,
830 caption: self.caption,
831 footnotes: self.footnotes,
832 }
833 } else {
834 return Err(EpubBuilderError::MissingNecessaryBlockData {
835 block_type: "MathML".to_string(),
836 missing_data: "'element_str'".to_string(),
837 }
838 .into());
839 }
840 }
841 };
842
843 Self::validate_footnotes(&block)?;
844 Ok(block)
845 }
846
847 fn is_target_type(path: &PathBuf, types: Vec<MatcherType>) -> Result<(), EpubError> {
857 if !path.is_file() {
858 return Err(EpubBuilderError::TargetIsNotFile {
859 target_path: path.to_string_lossy().to_string(),
860 }
861 .into());
862 }
863
864 let mut file = File::open(path)?;
865 let mut buf = [0; 512];
866 let read_size = file.read(&mut buf)?;
867 let header_bytes = &buf[..read_size];
868
869 match Infer::new().get(header_bytes) {
870 Some(file_type) if !types.contains(&file_type.matcher_type()) => {
871 Err(EpubBuilderError::NotExpectedFileFormat.into())
872 }
873
874 None => Err(EpubBuilderError::UnknownFileFormat {
875 file_path: path.to_string_lossy().to_string(),
876 }
877 .into()),
878
879 _ => Ok(()),
880 }
881 }
882
883 fn validate_footnotes(block: &Block) -> Result<(), EpubError> {
890 match block {
891 Block::Text { content, footnotes }
892 | Block::Quote { content, footnotes }
893 | Block::Title { content, footnotes, .. } => {
894 let max_locate = content.chars().count();
895 for footnote in footnotes.iter() {
896 if footnote.locate == 0 || footnote.locate > content.chars().count() {
897 return Err(EpubBuilderError::InvalidFootnoteLocate { max_locate }.into());
898 }
899 }
900
901 Ok(())
902 }
903
904 Block::Image { caption, footnotes, .. }
905 | Block::MathML { caption, footnotes, .. }
906 | Block::Video { caption, footnotes, .. }
907 | Block::Audio { caption, footnotes, .. } => {
908 if let Some(caption) = caption {
909 let max_locate = caption.chars().count();
910 for footnote in footnotes.iter() {
911 if footnote.locate == 0 || footnote.locate > caption.chars().count() {
912 return Err(
913 EpubBuilderError::InvalidFootnoteLocate { max_locate }.into()
914 );
915 }
916 }
917 } else if !footnotes.is_empty() {
918 return Err(EpubBuilderError::InvalidFootnoteLocate { max_locate: 0 }.into());
919 }
920
921 Ok(())
922 }
923 }
924 }
925}
926
927#[derive(Debug)]
933pub struct ContentBuilder {
934 pub id: String,
940
941 blocks: Vec<Block>,
942 language: String,
943 title: String,
944
945 pub(crate) temp_dir: PathBuf,
946}
947
948impl ContentBuilder {
949 pub fn new(id: &str, language: &str) -> Result<Self, EpubError> {
957 let temp_dir = env::temp_dir().join(local_time());
958 fs::create_dir(&temp_dir)?;
959
960 Ok(Self {
961 id: id.to_string(),
962 blocks: vec![],
963 language: language.to_string(),
964 title: String::new(),
965 temp_dir,
966 })
967 }
968
969 pub fn set_title(&mut self, title: &str) -> &mut Self {
976 self.title = title.to_string();
977 self
978 }
979
980 pub fn add_block(&mut self, block: Block) -> Result<&mut Self, EpubError> {
987 self.blocks.push(block);
988
989 match self.blocks.last() {
990 Some(Block::Image { .. }) | Some(Block::Audio { .. }) | Some(Block::Video { .. }) => {
991 self.handle_resource()?
992 }
993
994 Some(Block::MathML { fallback_image, .. }) if fallback_image.is_some() => {
995 self.handle_resource()?;
996 }
997
998 _ => {}
999 }
1000
1001 Ok(self)
1002 }
1003
1004 pub fn add_text_block(
1012 &mut self,
1013 content: &str,
1014 footnotes: Vec<Footnote>,
1015 ) -> Result<&mut Self, EpubError> {
1016 let mut builder = BlockBuilder::new(BlockType::Text);
1017 builder.set_content(content).set_footnotes(footnotes);
1018
1019 self.blocks.push(builder.build()?);
1020 Ok(self)
1021 }
1022
1023 pub fn add_quote_block(
1031 &mut self,
1032 content: &str,
1033 footnotes: Vec<Footnote>,
1034 ) -> Result<&mut Self, EpubError> {
1035 let mut builder = BlockBuilder::new(BlockType::Quote);
1036 builder.set_content(content).set_footnotes(footnotes);
1037
1038 self.blocks.push(builder.build()?);
1039 Ok(self)
1040 }
1041
1042 pub fn add_title_block(
1051 &mut self,
1052 content: &str,
1053 level: usize,
1054 footnotes: Vec<Footnote>,
1055 ) -> Result<&mut Self, EpubError> {
1056 let mut builder = BlockBuilder::new(BlockType::Title);
1057 builder
1058 .set_content(content)
1059 .set_title_level(level)
1060 .set_footnotes(footnotes);
1061
1062 self.blocks.push(builder.build()?);
1063 Ok(self)
1064 }
1065
1066 pub fn add_image_block(
1077 &mut self,
1078 url: PathBuf,
1079 alt: Option<String>,
1080 caption: Option<String>,
1081 footnotes: Vec<Footnote>,
1082 ) -> Result<&mut Self, EpubError> {
1083 let mut builder = BlockBuilder::new(BlockType::Image);
1084 builder.set_url(&url)?.set_footnotes(footnotes);
1085
1086 if let Some(alt) = &alt {
1087 builder.set_alt(alt);
1088 }
1089
1090 if let Some(caption) = &caption {
1091 builder.set_caption(caption);
1092 }
1093
1094 self.blocks.push(builder.build()?);
1095 self.handle_resource()?;
1096 Ok(self)
1097 }
1098
1099 pub fn add_audio_block(
1110 &mut self,
1111 url: PathBuf,
1112 fallback: String,
1113 caption: Option<String>,
1114 footnotes: Vec<Footnote>,
1115 ) -> Result<&mut Self, EpubError> {
1116 let mut builder = BlockBuilder::new(BlockType::Audio);
1117 builder
1118 .set_url(&url)?
1119 .set_fallback(&fallback)
1120 .set_footnotes(footnotes);
1121
1122 if let Some(caption) = &caption {
1123 builder.set_caption(caption);
1124 }
1125
1126 self.blocks.push(builder.build()?);
1127 self.handle_resource()?;
1128 Ok(self)
1129 }
1130
1131 pub fn add_video_block(
1142 &mut self,
1143 url: PathBuf,
1144 fallback: String,
1145 caption: Option<String>,
1146 footnotes: Vec<Footnote>,
1147 ) -> Result<&mut Self, EpubError> {
1148 let mut builder = BlockBuilder::new(BlockType::Video);
1149 builder
1150 .set_url(&url)?
1151 .set_fallback(&fallback)
1152 .set_footnotes(footnotes);
1153
1154 if let Some(caption) = &caption {
1155 builder.set_caption(caption);
1156 }
1157
1158 self.blocks.push(builder.build()?);
1159 self.handle_resource()?;
1160 Ok(self)
1161 }
1162
1163 pub fn add_mathml_block(
1174 &mut self,
1175 element_str: String,
1176 fallback_image: Option<PathBuf>,
1177 caption: Option<String>,
1178 footnotes: Vec<Footnote>,
1179 ) -> Result<&mut Self, EpubError> {
1180 let mut builder = BlockBuilder::new(BlockType::MathML);
1181 builder
1182 .set_mathml_element(&element_str)
1183 .set_footnotes(footnotes);
1184
1185 if let Some(fallback_image) = fallback_image {
1186 builder.set_fallback_image(fallback_image)?;
1187 }
1188
1189 if let Some(caption) = &caption {
1190 builder.set_caption(caption);
1191 }
1192
1193 self.blocks.push(builder.build()?);
1194 self.handle_resource()?;
1195 Ok(self)
1196 }
1197
1198 pub fn remove_last_block(&mut self) -> &mut Self {
1202 self.blocks.pop();
1203 self
1204 }
1205
1206 pub fn take_last_block(&mut self) -> Option<Block> {
1215 self.blocks.pop()
1216 }
1217
1218 pub fn clear_blocks(&mut self) -> &mut Self {
1222 self.blocks.clear();
1223 self
1224 }
1225
1226 pub fn make<P: AsRef<Path>>(&mut self, target: P) -> Result<Vec<PathBuf>, EpubError> {
1235 let mut result = Vec::new();
1236
1237 let target_dir = match target.as_ref().parent() {
1239 Some(path) => {
1240 fs::create_dir_all(path)?;
1241 path.to_path_buf()
1242 }
1243 None => {
1244 return Err(EpubBuilderError::InvalidTargetPath {
1245 target_path: target.as_ref().to_string_lossy().to_string(),
1246 }
1247 .into());
1248 }
1249 };
1250
1251 self.make_content(&target)?;
1252 result.push(target.as_ref().to_path_buf());
1253
1254 for resource_type in ["img", "audio", "video"] {
1256 let source = self.temp_dir.join(resource_type);
1257 if source.exists() && source.is_dir() {
1258 let target = target_dir.join(resource_type);
1259 fs::create_dir_all(&target)?;
1260
1261 for entry in fs::read_dir(&source)? {
1262 let entry = entry?;
1263 if entry.file_type()?.is_file() {
1264 let file_name = entry.file_name();
1265 let target = target.join(&file_name);
1266
1267 fs::copy(source.join(&file_name), &target)?;
1268 result.push(target);
1269 }
1270 }
1271 }
1272 }
1273
1274 Ok(result)
1275 }
1276
1277 fn make_content<P: AsRef<Path>>(&mut self, target_path: P) -> Result<(), EpubError> {
1284 let mut writer = Writer::new(Cursor::new(Vec::new()));
1285
1286 writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
1287 writer.write_event(Event::Start(BytesStart::new("html").with_attributes([
1288 ("xmlns", "http://www.w3.org/1999/xhtml"),
1289 ("xml:lang", self.language.as_str()),
1290 ])))?;
1291
1292 writer.write_event(Event::Start(BytesStart::new("head")))?;
1294 writer.write_event(Event::Start(BytesStart::new("title")))?;
1295 writer.write_event(Event::Text(BytesText::new(&self.title)))?;
1296 writer.write_event(Event::End(BytesEnd::new("title")))?;
1297 writer.write_event(Event::End(BytesEnd::new("head")))?;
1298
1299 writer.write_event(Event::Start(BytesStart::new("body")))?;
1301
1302 let mut footnote_index = 1;
1303 let mut footnotes = Vec::new();
1304 for block in self.blocks.iter_mut() {
1305 block.make(&mut writer, footnote_index)?;
1306
1307 footnotes.append(&mut block.take_footnotes());
1308 footnote_index = footnotes.len() + 1;
1309 }
1310
1311 Self::make_footnotes(&mut writer, footnotes)?;
1312 writer.write_event(Event::End(BytesEnd::new("body")))?;
1313 writer.write_event(Event::End(BytesEnd::new("html")))?;
1314
1315 let file_path = PathBuf::from(target_path.as_ref());
1316 let file_data = writer.into_inner().into_inner();
1317 fs::write(file_path, file_data)?;
1318
1319 Ok(())
1320 }
1321
1322 fn make_footnotes(writer: &mut XmlWriter, footnotes: Vec<Footnote>) -> Result<(), EpubError> {
1327 writer.write_event(Event::Start(BytesStart::new("aside")))?;
1328 writer.write_event(Event::Start(BytesStart::new("ul")))?;
1329
1330 let mut index = 1;
1331 for footnote in footnotes.into_iter() {
1332 writer.write_event(Event::Start(
1333 BytesStart::new("li")
1334 .with_attributes([("id", format!("footnote-{}", index).as_str())]),
1335 ))?;
1336 writer.write_event(Event::Start(BytesStart::new("p")))?;
1337
1338 writer.write_event(Event::Start(
1339 BytesStart::new("a")
1340 .with_attributes([("href", format!("#ref-{}", index).as_str())]),
1341 ))?;
1342 writer.write_event(Event::Text(BytesText::new(&format!("[{}]", index,))))?;
1343 writer.write_event(Event::End(BytesEnd::new("a")))?;
1344 writer.write_event(Event::Text(BytesText::new(&footnote.content)))?;
1345
1346 writer.write_event(Event::End(BytesEnd::new("p")))?;
1347 writer.write_event(Event::End(BytesEnd::new("li")))?;
1348
1349 index += 1;
1350 }
1351
1352 writer.write_event(Event::End(BytesEnd::new("ul")))?;
1353 writer.write_event(Event::End(BytesEnd::new("aside")))?;
1354
1355 Ok(())
1356 }
1357
1358 fn handle_resource(&mut self) -> Result<(), EpubError> {
1360 match self.blocks.last() {
1361 Some(Block::Image { url, .. }) => {
1362 let target_dir = self.temp_dir.join("img");
1363 fs::create_dir_all(&target_dir)?;
1364
1365 let target_path = target_dir.join(url.file_name().unwrap());
1366 fs::copy(url, &target_path)?;
1367 }
1368
1369 Some(Block::Video { url, .. }) => {
1370 let target_dir = self.temp_dir.join("video");
1371 fs::create_dir_all(&target_dir)?;
1372
1373 let target_path = target_dir.join(url.file_name().unwrap());
1374 fs::copy(url, &target_path)?;
1375 }
1376
1377 Some(Block::Audio { url, .. }) => {
1378 let target_dir = self.temp_dir.join("audio");
1379 fs::create_dir_all(&target_dir)?;
1380
1381 let target_path = target_dir.join(url.file_name().unwrap());
1382 fs::copy(url, &target_path)?;
1383 }
1384
1385 Some(Block::MathML { fallback_image, .. }) if fallback_image.is_some() => {
1386 let target_dir = self.temp_dir.join("img");
1387 fs::create_dir_all(&target_dir)?;
1388
1389 let target_path =
1390 target_dir.join(fallback_image.as_ref().unwrap().file_name().unwrap());
1391
1392 fs::copy(fallback_image.as_ref().unwrap(), &target_path)?;
1393 }
1394
1395 Some(_) => {}
1396 None => {}
1397 }
1398
1399 Ok(())
1400 }
1401}
1402
1403impl Drop for ContentBuilder {
1404 fn drop(&mut self) {
1405 if let Err(err) = fs::remove_dir_all(&self.temp_dir) {
1406 warn!("{}", err);
1407 };
1408 }
1409}