fix: TOC navigation and pagination improvements
- Fix recalculate_pages missing heading_top_spacing in page height calculation - Improve build_toc path matching: extract filename first, fall back to substring - Filter out EPUB3 nav.xhtml from content sections - Skip Windows resource compilation when windres is not available - Add unit tests for TOC filename matching and nav filtering
This commit is contained in:
90
src/book.rs
90
src/book.rs
@@ -248,7 +248,19 @@ pub fn load_epub(path: impl AsRef<Path>) -> Result<Book, String> {
|
||||
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 spine: Vec<String> = doc.spine.iter()
|
||||
.filter(|id| {
|
||||
if id.as_str() == "nav" { return false; }
|
||||
if let Some((path, _)) = doc.resources.get(*id) {
|
||||
let path_str = path.to_string_lossy().to_lowercase();
|
||||
if path_str.ends_with("nav.xhtml") || path_str.ends_with("nav.html") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let mut sections = Vec::new();
|
||||
for (i, href) in spine.iter().enumerate() {
|
||||
@@ -290,6 +302,11 @@ fn extract_title(html: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn extract_filename(path: &str) -> &str {
|
||||
let path = path.trim_end_matches('/');
|
||||
path.rsplit('/').next().unwrap_or(path)
|
||||
}
|
||||
|
||||
fn build_toc(
|
||||
entries: &[epub::doc::NavPoint],
|
||||
spine: &[String],
|
||||
@@ -298,10 +315,15 @@ fn build_toc(
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let content_str = e.content.to_string_lossy();
|
||||
let content_file = extract_filename(&content_str);
|
||||
let section = spine
|
||||
.iter()
|
||||
.position(|s| content_str.contains(s.trim_end_matches('/')))
|
||||
// unwrap_or(0) is safe: a real TOC entry should always match a spine item
|
||||
.position(|s| {
|
||||
let spine_file = extract_filename(s);
|
||||
spine_file == content_file
|
||||
|| content_str.contains(s.as_str())
|
||||
|| s.contains(content_str.as_ref())
|
||||
})
|
||||
.unwrap_or(0);
|
||||
TocEntry {
|
||||
label: e.label.clone(),
|
||||
@@ -439,4 +461,66 @@ mod tests {
|
||||
let toc = build_toc(&[], &[]);
|
||||
assert!(toc.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_sample_epub_nav_filtered() {
|
||||
let book = load_epub("sample-short.epub").expect("Failed to load sample epub");
|
||||
// Nav document is filtered out, leaving only chapter_0 as the single section
|
||||
assert_eq!(book.sections.len(), 1);
|
||||
assert_eq!(book.sections[0].title, "Understanding Digital Formats");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toc_section_bounds() {
|
||||
let book = load_epub("sample-short.epub").expect("Failed to load sample epub");
|
||||
// All TOC section indices should be within sections range
|
||||
fn check_bounds(entries: &[TocEntry], max: usize) {
|
||||
for e in entries {
|
||||
assert!(e.section < max, "TOC entry '{}' maps to section {} but only {} sections exist", e.label, e.section, max);
|
||||
check_bounds(&e.children, max);
|
||||
}
|
||||
}
|
||||
check_bounds(&book.toc, book.sections.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_toc_filename_matching() {
|
||||
use epub::doc::NavPoint;
|
||||
let spine = vec![
|
||||
"OEBPS/Text/chapter1.xhtml".to_string(),
|
||||
"OEBPS/Text/chapter2.xhtml".to_string(),
|
||||
];
|
||||
let nav_points = vec![
|
||||
NavPoint {
|
||||
label: "Chapter 2".to_string(),
|
||||
content: std::path::PathBuf::from("Text/chapter2.xhtml"),
|
||||
play_order: 1,
|
||||
children: vec![],
|
||||
},
|
||||
];
|
||||
let toc = build_toc(&nav_points, &spine);
|
||||
assert_eq!(toc.len(), 1);
|
||||
assert_eq!(toc[0].section, 1); // maps to spine index 1
|
||||
assert_eq!(toc[0].label, "Chapter 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_toc_exact_path_match() {
|
||||
use epub::doc::NavPoint;
|
||||
let spine = vec![
|
||||
"chapter1.xhtml".to_string(),
|
||||
"chapter2.xhtml".to_string(),
|
||||
];
|
||||
let nav_points = vec![
|
||||
NavPoint {
|
||||
label: "Chapter 1".to_string(),
|
||||
content: std::path::PathBuf::from("chapter1.xhtml"),
|
||||
play_order: 1,
|
||||
children: vec![],
|
||||
},
|
||||
];
|
||||
let toc = build_toc(&nav_points, &spine);
|
||||
assert_eq!(toc.len(), 1);
|
||||
assert_eq!(toc[0].section, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user