fix: resolve spine manifest IDs to file paths for TOC section matching

This commit is contained in:
Developer
2026-05-23 08:25:05 +08:00
parent 528d70fc33
commit 2e6ac83759

View File

@@ -310,6 +310,12 @@ pub fn load_epub(path: impl AsRef<Path>) -> Result<Book, String> {
.cloned() .cloned()
.collect(); .collect();
let spine_paths: Vec<String> = spine.iter().map(|id| {
doc.resources.get(id)
.map(|(path, _)| path.to_string_lossy().to_string())
.unwrap_or_else(|| id.clone())
}).collect();
let mut sections = Vec::new(); let mut sections = Vec::new();
for (i, href) in spine.iter().enumerate() { for (i, href) in spine.iter().enumerate() {
let raw_html = doc.get_resource_str(href) let raw_html = doc.get_resource_str(href)
@@ -326,7 +332,7 @@ pub fn load_epub(path: impl AsRef<Path>) -> Result<Book, String> {
} }
let raw_toc = std::mem::take(&mut doc.toc); let raw_toc = std::mem::take(&mut doc.toc);
let toc = build_toc(&raw_toc, &spine); let toc = build_toc(&raw_toc, &spine, &spine_paths);
Ok(Book { title, author, cover, layout, sections, toc }) Ok(Book { title, author, cover, layout, sections, toc })
} }
@@ -351,8 +357,8 @@ fn extract_title(html: &str) -> Option<String> {
} }
fn extract_filename(path: &str) -> &str { fn extract_filename(path: &str) -> &str {
let path = path.trim_end_matches('/'); let path = path.trim_end_matches('/').trim_end_matches('\\');
path.rsplit('/').next().unwrap_or(path) path.rsplit(&['/', '\\'][..]).next().unwrap_or(path)
} }
fn extract_fragment(path: &str) -> Option<String> { fn extract_fragment(path: &str) -> Option<String> {
@@ -371,6 +377,7 @@ fn extract_fragment(path: &str) -> Option<String> {
fn build_toc( fn build_toc(
entries: &[epub::doc::NavPoint], entries: &[epub::doc::NavPoint],
spine: &[String], spine: &[String],
spine_paths: &[String],
) -> Vec<TocEntry> { ) -> Vec<TocEntry> {
entries entries
.iter() .iter()
@@ -378,20 +385,29 @@ fn build_toc(
let content_str = e.content.to_string_lossy(); let content_str = e.content.to_string_lossy();
let anchor = extract_fragment(&content_str); let anchor = extract_fragment(&content_str);
let content_file = extract_filename(&content_str); let content_file = extract_filename(&content_str);
let section = spine let section = spine_paths
.iter() .iter()
.position(|s| { .position(|s| {
let spine_file = extract_filename(s); let spine_file = extract_filename(s);
spine_file == content_file if spine_file == content_file {
|| content_str.contains(s.as_str()) return true;
|| s.contains(content_str.as_ref()) }
content_str.contains(s.as_str()) || s.contains(content_str.as_ref())
})
.or_else(|| {
spine.iter().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); .unwrap_or(0);
TocEntry { TocEntry {
label: e.label.clone(), label: e.label.clone(),
section, section,
anchor, anchor,
children: build_toc(&e.children, spine), children: build_toc(&e.children, spine, spine_paths),
} }
}) })
.collect() .collect()
@@ -521,7 +537,7 @@ mod tests {
#[test] #[test]
fn test_build_toc_empty() { fn test_build_toc_empty() {
let toc = build_toc(&[], &[]); let toc = build_toc(&[], &[], &[]);
assert!(toc.is_empty()); assert!(toc.is_empty());
} }
@@ -561,7 +577,7 @@ mod tests {
children: vec![], children: vec![],
}, },
]; ];
let toc = build_toc(&nav_points, &spine); let toc = build_toc(&nav_points, &spine, &spine);
assert_eq!(toc.len(), 1); assert_eq!(toc.len(), 1);
assert_eq!(toc[0].section, 1); // maps to spine index 1 assert_eq!(toc[0].section, 1); // maps to spine index 1
assert_eq!(toc[0].label, "Chapter 2"); assert_eq!(toc[0].label, "Chapter 2");
@@ -582,7 +598,7 @@ mod tests {
children: vec![], children: vec![],
}, },
]; ];
let toc = build_toc(&nav_points, &spine); let toc = build_toc(&nav_points, &spine, &spine);
assert_eq!(toc.len(), 1); assert_eq!(toc.len(), 1);
assert_eq!(toc[0].section, 0); assert_eq!(toc[0].section, 0);
} }