Skip to main content

kindling/
lib.rs

1//! kindling: Kindle MOBI/AZW3 builder library.
2//!
3//! This crate provides EPUB-to-MOBI/AZW3 conversion, a comic pipeline,
4//! KDP pre-flight validation, and post-build MOBI readback checks.
5//! The same functionality is exposed through the `kindling-cli` binary,
6//! which is a thin wrapper around these modules.
7
8// Public API
9pub mod comic;
10pub mod epub;
11pub mod extracted;
12pub mod kdp_rules;
13pub mod mobi;
14pub mod mobi_check;
15pub mod mobi_dump;
16pub mod mobi_rewrite;
17pub mod opf;
18pub mod profile;
19pub mod repair;
20pub mod validate;
21
22// Internal implementation modules, visible inside the crate only.
23pub(crate) mod cbr;
24pub(crate) mod checks;
25pub(crate) mod cncx;
26pub(crate) mod exth;
27pub(crate) mod html_check;
28pub(crate) mod indx;
29pub(crate) mod kf8;
30pub(crate) mod moire;
31pub(crate) mod palmdoc;
32pub(crate) mod vwi;
33
34#[cfg(test)]
35mod tests;
36
37use std::path::Path;
38
39use crate::extracted::ExtractedEpub;
40
41/// Fallback author written to EXTH 100 when the input OPF or ComicInfo.xml
42/// does not supply a creator. Used anywhere kindling needs a non-empty author
43/// string: dictionary EXTH, book EXTH, and the comic pipeline. Kept as a
44/// single crate-wide constant so the displayed author on a Kindle home screen
45/// is consistent regardless of which entry point produced the file.
46pub const DEFAULT_AUTHOR: &str = "kindling";
47
48/// Pre-flight KDP validation used by `do_build`. Returns `Err(error_count)`.
49///
50/// Path-based entry point: parses the OPF into an [`ExtractedEpub`] and
51/// then delegates to [`run_preflight_validation_on_extracted`]. Callers
52/// that already have an `ExtractedEpub` (e.g. the CLI build flow that
53/// then hands the same instance to `mobi::build_mobi_from_extracted`)
54/// should use the `_on_extracted` variant directly so the OPF is not
55/// parsed twice.
56pub fn run_preflight_validation(opf_path: &Path, no_validate: bool) -> Result<(), usize> {
57    if no_validate {
58        println!("Skipping KDP validation (--no-validate)");
59        return Ok(());
60    }
61
62    let epub = match ExtractedEpub::from_opf_path(opf_path) {
63        Ok(e) => e,
64        Err(e) => {
65            // Match the original "couldn't parse" warning path exactly:
66            // print the path-aware warning and treat as a soft pass so the
67            // build can proceed and surface a more useful error itself.
68            println!(
69                "Validating {} against Kindle Publishing Guidelines v{}",
70                opf_path.display(),
71                kdp_rules::KPG_VERSION
72            );
73            eprintln!(
74                "Warning: could not parse OPF for pre-flight validation ({}): {}",
75                opf_path.display(),
76                e
77            );
78            return Ok(());
79        }
80    };
81
82    run_preflight_validation_on_extracted(&epub, no_validate)
83}
84
85/// Pre-flight KDP validation against an already-parsed [`ExtractedEpub`].
86///
87/// Identical behavior to [`run_preflight_validation`] (including the
88/// header line, per-finding output, summary line, error/warning return
89/// semantics, and `--no-validate` short-circuit), but does not re-parse
90/// the OPF. Use this whenever the caller has already constructed an
91/// `ExtractedEpub` it intends to reuse for the subsequent build.
92pub fn run_preflight_validation_on_extracted(
93    epub: &ExtractedEpub,
94    no_validate: bool,
95) -> Result<(), usize> {
96    if no_validate {
97        println!("Skipping KDP validation (--no-validate)");
98        return Ok(());
99    }
100
101    println!(
102        "Validating {} against Kindle Publishing Guidelines v{}",
103        epub.opf_path.display(),
104        kdp_rules::KPG_VERSION
105    );
106
107    let report = validate::validate(epub);
108
109    for finding in &report.findings {
110        println!("{}", finding);
111    }
112
113    let errors = report.error_count();
114    let warnings = report.warning_count();
115    let infos = report.info_count();
116    println!("{} errors, {} warnings, {} info", errors, warnings, infos);
117
118    if errors > 0 {
119        return Err(errors);
120    }
121    if warnings > 0 {
122        println!("Validation passed with {} warnings", warnings);
123    }
124    Ok(())
125}