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