oxihuman_export/
epub_export.rs1#![allow(dead_code)]
4
5#[derive(Debug, Clone)]
9pub struct EpubChapter {
10 pub title: String,
11 pub content_html: String,
12 pub file_name: String,
13}
14
15impl EpubChapter {
16 pub fn new(title: &str, content_html: &str) -> Self {
18 let file_name = format!("{}.xhtml", title.to_lowercase().replace(' ', "_"));
19 Self {
20 title: title.to_string(),
21 content_html: content_html.to_string(),
22 file_name,
23 }
24 }
25
26 pub fn content_bytes(&self) -> usize {
28 self.content_html.len()
29 }
30}
31
32#[derive(Debug, Clone)]
34pub struct EpubMeta {
35 pub title: String,
36 pub author: String,
37 pub language: String,
38 pub identifier: String,
39}
40
41impl Default for EpubMeta {
42 fn default() -> Self {
43 Self {
44 title: "Untitled".to_string(),
45 author: "Unknown".to_string(),
46 language: "en".to_string(),
47 identifier: "urn:uuid:00000000-0000-0000-0000-000000000000".to_string(),
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct EpubExport {
55 pub meta: EpubMeta,
56 pub chapters: Vec<EpubChapter>,
57}
58
59impl EpubExport {
60 pub fn new(meta: EpubMeta) -> Self {
62 Self {
63 meta,
64 chapters: Vec::new(),
65 }
66 }
67
68 pub fn add_chapter(&mut self, chapter: EpubChapter) {
70 self.chapters.push(chapter);
71 }
72
73 pub fn chapter_count(&self) -> usize {
75 self.chapters.len()
76 }
77
78 pub fn total_content_bytes(&self) -> usize {
80 self.chapters.iter().map(|c| c.content_bytes()).sum()
81 }
82}
83
84pub fn opf_manifest_stub(doc: &EpubExport) -> String {
86 let items: String = doc
87 .chapters
88 .iter()
89 .enumerate()
90 .map(|(i, c)| {
91 format!(
92 "<item id=\"ch{}\" href=\"{}\" media-type=\"application/xhtml+xml\"/>",
93 i, c.file_name
94 )
95 })
96 .collect::<Vec<_>>()
97 .join("\n");
98 format!("<manifest>\n{}\n</manifest>", items)
99}
100
101pub fn validate_epub(doc: &EpubExport) -> bool {
103 !doc.meta.title.is_empty() && !doc.chapters.is_empty()
104}
105
106pub fn epub_metadata_json(doc: &EpubExport) -> String {
108 format!(
109 "{{\"title\":\"{}\",\"author\":\"{}\",\"chapters\":{}}}",
110 doc.meta.title,
111 doc.meta.author,
112 doc.chapter_count()
113 )
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 fn sample_doc() -> EpubExport {
121 let meta = EpubMeta {
122 title: "OxiHuman Guide".into(),
123 author: "KitaSan".into(),
124 language: "en".into(),
125 identifier: "urn:uuid:1234".into(),
126 };
127 let mut doc = EpubExport::new(meta);
128 doc.add_chapter(EpubChapter::new("Introduction", "<p>Hello!</p>"));
129 doc.add_chapter(EpubChapter::new("Mesh Basics", "<p>Meshes...</p>"));
130 doc
131 }
132
133 #[test]
134 fn test_chapter_count() {
135 assert_eq!(sample_doc().chapter_count(), 2);
137 }
138
139 #[test]
140 fn test_total_content_bytes() {
141 let d = sample_doc();
143 assert!(d.total_content_bytes() > 0);
144 }
145
146 #[test]
147 fn test_validate_valid() {
148 assert!(validate_epub(&sample_doc()));
150 }
151
152 #[test]
153 fn test_validate_empty_title() {
154 let meta = EpubMeta {
156 title: "".into(),
157 ..Default::default()
158 };
159 let mut doc = EpubExport::new(meta);
160 doc.add_chapter(EpubChapter::new("Ch1", "content"));
161 assert!(!validate_epub(&doc));
162 }
163
164 #[test]
165 fn test_opf_manifest_stub() {
166 let d = sample_doc();
168 let opf = opf_manifest_stub(&d);
169 assert!(opf.contains("application/xhtml+xml"));
170 }
171
172 #[test]
173 fn test_metadata_json_contains_title() {
174 let json = epub_metadata_json(&sample_doc());
176 assert!(json.contains("OxiHuman Guide"));
177 }
178
179 #[test]
180 fn test_chapter_filename() {
181 let c = EpubChapter::new("Mesh Basics", "content");
183 assert!(c.file_name.contains("mesh_basics"));
184 }
185
186 #[test]
187 fn test_empty_document_invalid() {
188 let meta = EpubMeta {
190 title: "Valid Title".into(),
191 ..Default::default()
192 };
193 let doc = EpubExport::new(meta);
194 assert!(!validate_epub(&doc));
195 }
196}