fix title extraction and epub errors

This commit is contained in:
2026-05-25 17:48:52 +02:00
parent d8c59dacc1
commit 2b66132d0d
4 changed files with 205 additions and 44 deletions
+52 -9
View File
@@ -3,6 +3,7 @@ use std::fs::File;
use std::io::Write;
use std::path::Path;
use chrono::Utc;
use quick_xml::escape::escape;
use zip::CompressionMethod;
use zip::write::{SimpleFileOptions, ZipWriter};
@@ -181,11 +182,16 @@ fn build_ncx(manifest: &crate::manifest::Manifest, built: &[BuiltEntry]) -> Stri
let section_play_order = play_order;
play_order += 1;
let section_target = section_entries[0]
.section_anchor
.as_ref()
.map(|anchor| format!("text/{}.xhtml#{}", section_entries[0].id, anchor))
.unwrap_or_else(|| format!("text/{}.xhtml", section_entries[0].id));
let mut child_points = String::new();
for entry in &section_entries {
child_points.push_str(&format!(
"<navPoint id=\"nav-{}\" playOrder=\"{}\"><navLabel><text>{}</text></navLabel><content src=\"text/{}.xhtml\"/></navPoint>",
escape(&entry.id),
escape(&xml_id("nav", &entry.id)),
play_order,
escape(&entry.chapter.nav_title),
escape(&entry.id)
@@ -194,11 +200,11 @@ fn build_ncx(manifest: &crate::manifest::Manifest, built: &[BuiltEntry]) -> Stri
}
nav_points.push_str(&format!(
"<navPoint id=\"section-{}\" playOrder=\"{}\"><navLabel><text>{}</text></navLabel><content src=\"text/{}.xhtml\"/>{}</navPoint>",
escape(&section.id),
"<navPoint id=\"{}\" playOrder=\"{}\"><navLabel><text>{}</text></navLabel><content src=\"{}\"/>{}</navPoint>",
escape(&xml_id("section", &section.id)),
section_play_order,
escape(&section.title),
escape(&section_entries[0].id),
escape(&section_target),
child_points
));
}
@@ -233,14 +239,17 @@ fn build_opf(
for entry in built {
manifest_items.push_str(&format!(
"<item id=\"{}\" href=\"text/{}.xhtml\" media-type=\"application/xhtml+xml\"/>",
escape(&entry.id),
escape(&xml_id("entry", &entry.id)),
escape(&entry.id)
));
spine_items.push_str(&format!("<itemref idref=\"{}\"/>", escape(&entry.id)));
spine_items.push_str(&format!(
"<itemref idref=\"{}\"/>",
escape(&xml_id("entry", &entry.id))
));
for asset in &entry.assets {
manifest_items.push_str(&format!(
"<item id=\"{}\" href=\"{}\" media-type=\"{}\"/>",
escape(&asset.id),
escape(&xml_id("asset", &asset.id)),
escape(&asset.href),
escape(&asset.media_type)
));
@@ -249,8 +258,9 @@ fn build_opf(
if let Some(cover_href) = cover_href {
manifest_items.push_str(&format!(
"<item id=\"cover\" href=\"{}\" media-type=\"image/jpeg\" properties=\"cover-image\"/>",
escape(cover_href)
"<item id=\"cover\" href=\"{}\" media-type=\"{}\" properties=\"cover-image\"/>",
escape(cover_href),
escape(&media_type_from_href(cover_href))
));
}
@@ -260,6 +270,7 @@ fn build_opf(
.clone()
.unwrap_or_else(|| "Unknown".to_string());
let description = manifest.book.description.clone().unwrap_or_default();
let modified = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<package version="3.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="bookid">
@@ -269,6 +280,7 @@ fn build_opf(
<dc:creator>{}</dc:creator>
<dc:language>{}</dc:language>
<dc:description>{}</dc:description>
<meta property="dcterms:modified">{}</meta>
</metadata>
<manifest>{}</manifest>
<spine toc="ncx">{}</spine>
@@ -278,11 +290,42 @@ fn build_opf(
escape(&author),
escape(&manifest.book.language),
escape(&description),
escape(&modified),
manifest_items,
spine_items
)
}
fn xml_id(prefix: &str, value: &str) -> String {
let mut id = String::with_capacity(prefix.len() + value.len() + 1);
id.push_str(prefix);
id.push('-');
for ch in value.chars() {
if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.') {
id.push(ch);
} else {
id.push('-');
}
}
id
}
fn media_type_from_href(href: &str) -> String {
match href.rsplit('.').next().map(|ext| ext.to_ascii_lowercase()) {
Some(extension) => match extension.as_str() {
"jpg" | "jpeg" => "image/jpeg",
"png" => "image/png",
"gif" => "image/gif",
"svg" => "image/svg+xml",
"webp" => "image/webp",
"avif" => "image/avif",
_ => "application/octet-stream",
}
.to_string(),
None => "application/octet-stream".to_string(),
}
}
const CONTAINER_XML: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>