Skip to main content

oxihuman_export/
epub_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! EPUB document stub export.
6
7/// An EPUB chapter.
8#[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    /// Create a new chapter.
17    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    /// Content length in bytes.
27    pub fn content_bytes(&self) -> usize {
28        self.content_html.len()
29    }
30}
31
32/// EPUB document metadata.
33#[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/// EPUB export document.
53#[derive(Debug, Clone)]
54pub struct EpubExport {
55    pub meta: EpubMeta,
56    pub chapters: Vec<EpubChapter>,
57}
58
59impl EpubExport {
60    /// Create a new EPUB document.
61    pub fn new(meta: EpubMeta) -> Self {
62        Self {
63            meta,
64            chapters: Vec::new(),
65        }
66    }
67
68    /// Add a chapter.
69    pub fn add_chapter(&mut self, chapter: EpubChapter) {
70        self.chapters.push(chapter);
71    }
72
73    /// Chapter count.
74    pub fn chapter_count(&self) -> usize {
75        self.chapters.len()
76    }
77
78    /// Total content length in bytes.
79    pub fn total_content_bytes(&self) -> usize {
80        self.chapters.iter().map(|c| c.content_bytes()).sum()
81    }
82}
83
84/// Serialize OPF manifest (stub).
85pub 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
101/// Validate EPUB document.
102pub fn validate_epub(doc: &EpubExport) -> bool {
103    !doc.meta.title.is_empty() && !doc.chapters.is_empty()
104}
105
106/// Serialize EPUB metadata to JSON (stub).
107pub 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        /* chapter count is correct */
136        assert_eq!(sample_doc().chapter_count(), 2);
137    }
138
139    #[test]
140    fn test_total_content_bytes() {
141        /* total content bytes sums chapter content */
142        let d = sample_doc();
143        assert!(d.total_content_bytes() > 0);
144    }
145
146    #[test]
147    fn test_validate_valid() {
148        /* valid document passes */
149        assert!(validate_epub(&sample_doc()));
150    }
151
152    #[test]
153    fn test_validate_empty_title() {
154        /* empty title fails validation */
155        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        /* OPF manifest includes chapter filenames */
167        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        /* metadata JSON contains title */
175        let json = epub_metadata_json(&sample_doc());
176        assert!(json.contains("OxiHuman Guide"));
177    }
178
179    #[test]
180    fn test_chapter_filename() {
181        /* chapter filename is derived from title */
182        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        /* document with no chapters fails validation */
189        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}