novel_cli/cmd/
build.rs

1use std::env;
2use std::path::PathBuf;
3use std::process::Command;
4
5use clap::Args;
6use color_eyre::eyre::{self, Result};
7use fluent_templates::Loader;
8use fs_extra::dir::CopyOptions;
9use mdbook_driver::MDBook;
10use walkdir::WalkDir;
11
12use crate::utils::{self, CurrentDir};
13use crate::{LANG_ID, LOCALES};
14
15#[must_use]
16#[derive(Args)]
17#[command(arg_required_else_help = true,
18    about = LOCALES.lookup(&LANG_ID, "build_command"))]
19pub struct Build {
20    #[arg(help = LOCALES.lookup(&LANG_ID, "build_path"))]
21    pub build_path: PathBuf,
22
23    #[arg(short, long, default_value_t = false,
24        help = LOCALES.lookup(&LANG_ID, "delete"))]
25    pub delete: bool,
26
27    #[arg(short, long, default_value_t = false,
28        help = LOCALES.lookup(&LANG_ID, "open"))]
29    pub open: bool,
30}
31
32pub fn execute(config: Build) -> Result<()> {
33    if utils::is_mdbook_dir(&config.build_path)? {
34        execute_mdbook(config)?;
35    } else {
36        execute_pandoc(config)?;
37    }
38    println!("{}", utils::locales("build_complete_msg", "👌"));
39
40    Ok(())
41}
42
43fn execute_mdbook(config: Build) -> Result<()> {
44    println!("{}", utils::locales_with_arg("build_msg", "📚", "mdBook"));
45
46    let input_mdbook_dir_path = dunce::canonicalize(&config.build_path)?;
47    tracing::info!(
48        "Input mdBook directory path: `{}`",
49        input_mdbook_dir_path.display()
50    );
51
52    let book_path = input_mdbook_dir_path.join("book");
53
54    if book_path.try_exists()? {
55        tracing::warn!("The mdBook output directory already exists and will be deleted");
56        utils::remove_file_or_dir(&book_path)?;
57    }
58
59    match MDBook::load(&input_mdbook_dir_path) {
60        Ok(mdbook) => {
61            if let Err(error) = mdbook.build() {
62                eyre::bail!("mdBook failed to build: {error}");
63            }
64        }
65        Err(error) => {
66            eyre::bail!("mdBook failed to load: {error}");
67        }
68    }
69
70    if config.delete {
71        for entry in WalkDir::new(&input_mdbook_dir_path).max_depth(1) {
72            let path = entry?.path().to_path_buf();
73
74            if path != input_mdbook_dir_path && path != book_path {
75                utils::remove_file_or_dir(path)?;
76            }
77        }
78
79        let mut options = CopyOptions::new();
80        options.copy_inside = true;
81        options.content_only = true;
82        fs_extra::dir::move_dir(&book_path, &input_mdbook_dir_path, &options)?;
83    }
84
85    if config.open {
86        let index_html_path = if config.delete {
87            input_mdbook_dir_path.join("index.html")
88        } else {
89            book_path.join("index.html")
90        };
91
92        open::that(index_html_path)?;
93    }
94
95    Ok(())
96}
97
98fn execute_pandoc(config: Build) -> Result<()> {
99    utils::ensure_executable_exists("pandoc")?;
100
101    let input_file_path;
102    let input_file_parent_path;
103    let mut in_directory = false;
104
105    if utils::is_markdown_or_txt_file(&config.build_path)? {
106        input_file_path = dunce::canonicalize(&config.build_path)?;
107        input_file_parent_path = input_file_path.parent().unwrap().to_path_buf();
108    } else if let Ok(Some(path)) =
109        utils::try_get_markdown_or_txt_file_name_in_dir(&config.build_path)
110    {
111        in_directory = true;
112
113        input_file_path = path;
114        input_file_parent_path = dunce::canonicalize(&config.build_path)?;
115    } else {
116        eyre::bail!("Invalid input path: `{}`", config.build_path.display());
117    }
118    tracing::info!("Input file path: `{}`", input_file_path.display());
119    println!("{}", utils::locales_with_arg("build_msg", "📚", "Pandoc"));
120
121    let output_epub_file_path =
122        env::current_dir()?.join(utils::read_markdown_to_epub_file_name(&input_file_path)?);
123    tracing::info!(
124        "Output epub file path: `{}`",
125        output_epub_file_path.display()
126    );
127
128    if output_epub_file_path.try_exists()? {
129        tracing::warn!("The epub output file already exists and will be deleted");
130        utils::remove_file_or_dir(&output_epub_file_path)?;
131    }
132
133    let current_dir = CurrentDir::new(&input_file_parent_path)?;
134    let output = Command::new("pandoc")
135        .arg("--from=commonmark+yaml_metadata_block")
136        .arg("--to=epub3")
137        .arg("--split-level=2")
138        .arg("--epub-title-page=false")
139        .args(["-o", output_epub_file_path.to_str().unwrap()])
140        .arg(&input_file_path)
141        .output()?;
142
143    tracing::info!("{}", simdutf8::basic::from_utf8(&output.stdout)?);
144
145    if !output.status.success() {
146        tracing::error!("{}", simdutf8::basic::from_utf8(&output.stderr)?);
147        eyre::bail!("`pandoc` failed to execute");
148    }
149
150    if config.delete {
151        if in_directory {
152            // On Windows, the current working directory will be occupied and cannot be deleted
153            current_dir.restore()?;
154            utils::remove_file_or_dir(input_file_parent_path)?;
155        } else {
156            let images = utils::read_markdown_to_images(&input_file_path)?;
157            utils::remove_file_or_dir_all(&images)?;
158
159            utils::remove_file_or_dir(input_file_path)?;
160
161            current_dir.restore()?;
162        }
163    }
164
165    if config.open {
166        open::that(output_epub_file_path)?;
167    }
168
169    Ok(())
170}