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 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}