deps: update

This commit is contained in:
Maverick Liu 2026-01-06 19:05:13 +08:00
parent 2bf293c4d7
commit 6fbd50bb1b
4 changed files with 278 additions and 1912 deletions

2098
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "epub2mdbook" name = "epub2mdbook"
version = "0.15.0" version = "0.16.0"
edition = "2024" edition = "2024"
description = "A tool to convert EPUB files to MDBook format" description = "A tool to convert EPUB files to MDBook format"
authors = ["Maverick Liu <maverick.liu42@gmail.com>"] authors = ["Maverick Liu <maverick.liu42@gmail.com>"]
@ -10,10 +10,10 @@ keywords = ["epub", "mdbook", "converter", "ebook"]
categories = ["command-line-utilities", "text-processing"] categories = ["command-line-utilities", "text-processing"]
[dependencies] [dependencies]
clap = { version = "4.5.30", features = ["derive"] } clap = { version = "4.5.54", features = ["derive"] }
epub = "2.1.1" epub = "2.1.5"
htmd = "0.1.6" htmd = "0.5.0"
mdbook = "0.4.45" mdbook-core = "0.5.2"
regex = "1.11.1" regex = "1.12.2"
thiserror = "2.0.11" thiserror = "2.0.17"
toml = "0.8.20" toml = "0.9.10"

View file

@ -2,7 +2,7 @@ pub mod error;
use epub::doc::{EpubDoc, NavPoint}; use epub::doc::{EpubDoc, NavPoint};
use error::Error; use error::Error;
use mdbook::config::BookConfig; use mdbook_core::config::BookConfig;
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::OsStr; use std::ffi::OsStr;
@ -15,20 +15,21 @@ use std::{fs, io};
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `epub_path` - The path to the EPUB file /// * `epub_path` - Path to the input EPUB file
/// * `output_dir` - The path to the output directory /// * `output_dir` - Path to the output directory
/// * `with_file_name` - Whether to use the file name as the output directory /// * `create_subdir` - If `true`, creates a subdirectory named after the EPUB file
/// (e.g., `output_dir/book_name/`). If `false`, outputs directly to `output_dir`.
pub fn convert_epub_to_mdbook( pub fn convert_epub_to_mdbook(
epub_path: impl AsRef<Path>, epub_path: impl AsRef<Path>,
output_dir: impl AsRef<Path>, output_dir: impl AsRef<Path>,
with_file_name: bool, create_subdir: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
let epub_path = epub_path.as_ref(); let epub_path = epub_path.as_ref();
if !epub_path.is_file() { if !epub_path.is_file() {
return Err(Error::NotAFile(epub_path.display().to_string())); return Err(Error::NotAFile(epub_path.display().to_string()));
} }
let mut output_dir = output_dir.as_ref().to_owned(); let mut output_dir = output_dir.as_ref().to_owned();
if with_file_name { if create_subdir {
let book_name = epub_path let book_name = epub_path
.with_extension("") .with_extension("")
.file_name() .file_name()
@ -80,7 +81,7 @@ fn epub_nav_to_md(
pub fn generate_summary_md<R: Read + Seek>( pub fn generate_summary_md<R: Read + Seek>(
epub_doc: &EpubDoc<R>, epub_doc: &EpubDoc<R>,
) -> (String, HashMap<PathBuf, PathBuf>) { ) -> (String, HashMap<PathBuf, PathBuf>) {
let title = epub_doc.metadata.get("title").and_then(|v| v.first()); let title = epub_doc.get_title();
let mut summary_md = if let Some(title) = title { let mut summary_md = if let Some(title) = title {
format!("# {}\n\n", title) format!("# {}\n\n", title)
} else { } else {
@ -89,8 +90,10 @@ pub fn generate_summary_md<R: Read + Seek>(
let html_to_md = epub_doc let html_to_md = epub_doc
.resources .resources
.iter() .iter()
.filter(|(_, (_, mime))| ["application/xhtml+xml", "text/html"].contains(&&**mime)) .filter(|(_, resource)| {
.map(|(_, (path, _))| (path.clone(), path.with_extension("md"))) ["application/xhtml+xml", "text/html"].contains(&resource.mime.as_str())
})
.map(|(_, resource)| (resource.path.clone(), resource.path.with_extension("md")))
.collect::<HashMap<PathBuf, PathBuf>>(); .collect::<HashMap<PathBuf, PathBuf>>();
for nav in &epub_doc.toc { for nav in &epub_doc.toc {
if let Some(md) = epub_nav_to_md(nav, 0, &html_to_md) { if let Some(md) = epub_nav_to_md(nav, 0, &html_to_md) {
@ -110,12 +113,13 @@ fn extract_chapters_and_resources<R: Read + Seek>(
.filter_map(|(k, v)| Some((k.file_name()?, v.file_name()?))) .filter_map(|(k, v)| Some((k.file_name()?, v.file_name()?)))
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
let src_dir = output_dir.as_ref().join("src"); let src_dir = output_dir.as_ref().join("src");
for (_, (path, _)) in epub_doc.resources.clone().into_iter() { for (_, resource) in epub_doc.resources.clone() {
let mut content = match epub_doc.get_resource_by_path(&path) { let path = &resource.path;
let mut content = match epub_doc.get_resource_by_path(path) {
Some(content) => content, Some(content) => content,
None => continue, // unreachable None => continue, // unreachable
}; };
let target_path = if let Some(md_path) = html_to_md.get(&path) { let target_path = if let Some(md_path) = html_to_md.get(path) {
// html file, convert to md // html file, convert to md
let html = String::from_utf8(content.clone())?; let html = String::from_utf8(content.clone())?;
let markdown = htmd::convert(&html)?; let markdown = htmd::convert(&html)?;
@ -127,7 +131,7 @@ fn extract_chapters_and_resources<R: Read + Seek>(
} }
} else { } else {
// other file, just copy // other file, just copy
src_dir.join(&path) src_dir.join(path)
}; };
// write to target path // write to target path
if let Some(parent) = target_path.parent() { if let Some(parent) = target_path.parent() {
@ -139,7 +143,7 @@ fn extract_chapters_and_resources<R: Read + Seek>(
} }
/// Capture the `{link}` without `#`, eg: /// Capture the `{link}` without `#`, eg:
/// ``` /// ```text
/// [ABC]({abc.html}#xxx) /// [ABC]({abc.html}#xxx)
/// [ABC]({abc.html}) /// [ABC]({abc.html})
/// ``` /// ```
@ -147,7 +151,7 @@ static LINK: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"\[[^\]]+\]\((?P<link>[^#)]+)(#[^)]+)?\)"#).expect("unreachable") Regex::new(r#"\[[^\]]+\]\((?P<link>[^#)]+)(#[^)]+)?\)"#).expect("unreachable")
}); });
/// Match the URL link, eg: /// Match the URL link, eg:
/// ``` /// ```text
/// https://www.example.com\ /// https://www.example.com\
/// ``` /// ```
static URL_LINK: LazyLock<Regex> = static URL_LINK: LazyLock<Regex> =
@ -183,29 +187,26 @@ fn write_book_toml<R: Read + Seek>(
output_dir: impl AsRef<Path>, output_dir: impl AsRef<Path>,
) -> io::Result<()> { ) -> io::Result<()> {
let output_dir = output_dir.as_ref(); let output_dir = output_dir.as_ref();
let title = epub_doc let title = epub_doc.get_title();
let authors = epub_doc
.metadata .metadata
.get("title") .iter()
.and_then(|v| v.first().cloned()); .filter(|m| m.property == "creator")
let authors = epub_doc.metadata.get("creator").cloned().unwrap_or(vec![]); .map(|m| m.value.clone())
.collect::<Vec<_>>();
let description = epub_doc let description = epub_doc
.metadata .mdata("description")
.get("description") .map(|m| htmd::convert(&m.value).expect("unreachable"));
.and_then(|v| v.first().cloned())
.map(|s| htmd::convert(&s).expect("unreachable"));
let lang = epub_doc let lang = epub_doc
.metadata .mdata("language")
.get("lang") .or_else(|| epub_doc.mdata("lang"))
.and_then(|v| v.first().cloned()); .map(|m| m.value.clone());
let config = BookConfig { let mut config = BookConfig::default();
title, config.title = title;
authors, config.authors = authors;
description, config.description = description;
src: PathBuf::from("src"), config.src = PathBuf::from("src");
multilingual: false, config.language = lang;
language: lang,
text_direction: None,
};
let toml_content = format!("[book]\n{}", toml::to_string(&config).expect("unreachable")); let toml_content = format!("[book]\n{}", toml::to_string(&config).expect("unreachable"));
fs::write(output_dir.join("book.toml"), toml_content)?; fs::write(output_dir.join("book.toml"), toml_content)?;
Ok(()) Ok(())

View file

@ -10,11 +10,14 @@ struct Args {
/// The path to the output directory /// The path to the output directory
#[clap(short, long, default_value = ".")] #[clap(short, long, default_value = ".")]
output_dir: PathBuf, output_dir: PathBuf,
/// Output directly to the output directory without creating a subdirectory named after the book
#[clap(short, long)]
flat: bool,
} }
fn main() -> Result<(), Error> { fn main() -> Result<(), Error> {
let args = Args::parse(); let args = Args::parse();
convert_epub_to_mdbook(args.input_epub, args.output_dir, true)?; convert_epub_to_mdbook(args.input_epub, args.output_dir, !args.flat)?;
println!("Conversion completed successfully!"); println!("Conversion completed successfully!");
Ok(()) Ok(())
} }