From 5f66478c267bacf989fbf3adffd03e42c4231f3a Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 13 May 2026 23:09:01 +0800 Subject: [PATCH] Implement EpubLoader with load_epub, extract_title, and build_toc functions --- src/book.rs | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/book.rs b/src/book.rs index 69c27a7..117783d 100644 --- a/src/book.rs +++ b/src/book.rs @@ -55,10 +55,88 @@ pub struct Book { pub toc: Vec, } +use std::path::Path; + +pub fn load_epub(path: impl AsRef) -> Result { + let path = path.as_ref(); + let mut doc = epub::doc::EpubDoc::new(path) + .map_err(|e| format!("无法打开文件: {}", e))?; + + let title = doc.mdata("title").unwrap_or_else(|| "未知标题".to_string()); + let author = doc.mdata("creator").unwrap_or_else(|| "未知作者".to_string()); + let cover = doc.get_cover().ok(); + let spine = doc.spine.clone(); + let raw_toc = std::mem::take(&mut doc.toc); + + let mut sections = Vec::new(); + for (i, href) in spine.iter().enumerate() { + let raw_html = doc.get_resource_str(href) + .map_err(|e| format!("读取章节失败: {}", e))?; + let text = strip_html(&raw_html); + let title = extract_title(&raw_html) + .unwrap_or_else(|| format!("第{}章", i + 1)); + sections.push(Section { + title, + content: text, + pages: Vec::new(), + }); + } + + let toc = build_toc(&raw_toc, &spine); + + Ok(Book { title, author, cover, sections, toc }) +} + +fn extract_title(html: &str) -> Option { + if let Some(start) = html.find("") { + let rest = &html[start + 7..]; + if let Some(end) = rest.find("") { + return Some(strip_html(&rest[..end]).trim().to_string()); + } + } + if let Some(start) = html.find("') { + let inner = &rest[content_start + 1..]; + if let Some(end) = inner.find("") { + return Some(strip_html(&inner[..end]).trim().to_string()); + } + } + } + None +} + +fn build_toc( + entries: &[epub::doc::NavPoint], + spine: &[String], +) -> Vec { + entries + .iter() + .map(|e| { + let content_str = e.content.to_string_lossy(); + let section = spine + .iter() + .position(|s| content_str.contains(s.trim_end_matches('/'))) + .unwrap_or(0); + TocEntry { + label: e.label.clone(), + section, + children: build_toc(&e.children, spine), + } + }) + .collect() +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn test_epub_loader_nonexistent_file() { + let result = load_epub("nonexistent.epub"); + assert!(result.is_err()); + } + #[test] fn test_strip_html_plain_text() { assert_eq!(strip_html("Hello World"), "Hello World");