1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! A `mdbook` backend for generating a book in the `EPUB` format.

use ::epub_builder;
use ::thiserror::Error;
use ::handlebars;
#[macro_use]
extern crate log;
use ::mdbook;
use ::semver;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;

use mdbook::config::Config as MdConfig;
use mdbook::renderer::RenderContext;
use semver::{Version, VersionReq};
use std::fs::{create_dir_all, File};
use std::path::{Path, PathBuf};

mod config;
mod generator;
mod resources;

pub use crate::config::Config;
pub use crate::generator::Generator;

/// The default stylesheet used to make the rendered document pretty.
pub const DEFAULT_CSS: &str = include_str!("master.css");

#[derive(Error, Debug)]
pub enum Error {
    #[error("Incompatible mdbook version got {0} expected {1}")]
    IncompatibleVersion(String, String),

    #[error("{0}")]
    EpubDocCreate(String),

    #[error("Could not parse the template")]
    TemplateParse,

    #[error("Content file was not found: \'{0}\'")]
    ContentFileNotFound(String),

    #[error("{0}")]
    AssetFileNotFound(String),

    #[error("Asset was not a file {0}")]
    AssetFile(PathBuf),

    #[error("Could not open css file {0}")]
    CssOpen(PathBuf),

    #[error("Unable to open template {0}")]
    OpenTemplate(PathBuf),

    #[error("Unable to parse render context")]
    RenderContext,

    #[error("Unable to open asset")]
    AssetOpen,

    #[error("Error reading stylesheet")]
    StylesheetRead,

    #[error("Epub check failed, ensure the epubcheck program is installed")]
    EpubCheck,

    #[error(transparent)]
    Io(#[from] std::io::Error),

    #[error(transparent)]
    Book(#[from] mdbook::errors::Error),
    #[error(transparent)]
    Semver(#[from] semver::SemVerError),
    #[error(transparent)]
    SemverReqParse(#[from] semver::ReqParseError),
    #[error(transparent)]
    EpubBuilder(#[from] epub_builder::Error),
    #[error(transparent)]
    Render(#[from] handlebars::RenderError),
    #[error(transparent)]
    TomlDeser(#[from] toml::de::Error),
}

/// The exact version of `mdbook` this crate is compiled against.
pub const MDBOOK_VERSION: &str = mdbook::MDBOOK_VERSION;

/// Check that the version of `mdbook` we're called by is compatible with this
/// backend.
fn version_check(ctx: &RenderContext) -> Result<(), Error> {
    let provided_version = Version::parse(&ctx.version)?;
    let required_version = VersionReq::parse(&format!("~{}", MDBOOK_VERSION))?;

    if !required_version.matches(&provided_version) {
        Err(Error::IncompatibleVersion(
            MDBOOK_VERSION.to_string(), ctx.version.clone()))
    } else {
        Ok(())
    }
}

/// Generate an `EPUB` version of the provided book.
pub fn generate(ctx: &RenderContext) -> Result<(), Error> {
    info!("Starting the EPUB generator");
    version_check(ctx)?;

    let outfile = output_filename(&ctx.destination, &ctx.config);
    trace!("Output File: {}", outfile.display());

    if !ctx.destination.exists() {
        debug!(
            "Creating destination directory ({})",
            ctx.destination.display()
        );
        create_dir_all(&ctx.destination)?;
    }

    let f = File::create(&outfile)?;
    Generator::new(ctx)?.generate(f)?;

    Ok(())
}

/// Calculate the output filename using the `mdbook` config.
pub fn output_filename(dest: &Path, config: &MdConfig) -> PathBuf {
    match config.book.title {
        Some(ref title) => dest.join(title).with_extension("epub"),
        None => dest.join("book.epub"),
    }
}