1use std::fs;
4use std::path::PathBuf;
5use std::{
6 fs::{File, ReadDir, create_dir_all, read_dir},
7 io,
8 io::{Read, Write},
9 path::Path,
10};
11
12use dirs::config_dir;
13use log::{error, info};
14
15use crate::config::Config;
16use crate::html_generator::generate_default_css;
17
18pub fn read_input_dir(
27 input_dir: &str,
28 run_recursively: &bool,
29 excluded_entries: &[String],
30) -> Result<Vec<(String, String)>, io::Error> {
31 if *run_recursively {
32 let mut file_contents: Vec<(String, String)> = Vec::new();
34 let input_dir = Path::new(input_dir);
35 visit_dir(
36 Path::new(input_dir),
37 input_dir,
38 &mut file_contents,
39 excluded_entries,
40 )
41 .map_err(|e| {
42 error!(
43 "Failed to read input directory '{}': {}",
44 input_dir.display(),
45 e
46 );
47 e
48 })?;
49
50 Ok(file_contents)
51 } else {
52 let entries: ReadDir = read_dir(input_dir).map_err(|e| {
53 error!("Failed to read input directory '{input_dir}': {e}");
54 e
55 })?;
56
57 let mut file_contents: Vec<(String, String)> = Vec::new();
59 for entry in entries {
60 let entry = entry?;
61
62 let file_path = entry.path();
63 let file_name = file_path
64 .file_name()
65 .and_then(|s| s.to_str())
66 .ok_or_else(|| {
67 io::Error::new(
68 io::ErrorKind::InvalidData,
69 format!(
70 "Failed to extract file name from path '{}'",
71 file_path.display()
72 ),
73 )
74 })?
75 .to_string();
76
77 if excluded_entries.contains(&file_name) {
78 continue;
79 }
80
81 if file_path.extension().and_then(|s| s.to_str()) == Some("md") {
82 let contents = read_file(file_path.to_str().unwrap()).map_err(|e| {
83 io::Error::other(format!(
84 "Failed to read file '{}': {}",
85 file_path.display(),
86 e
87 ))
88 })?;
89 file_contents.push((file_name, contents));
90 }
91 }
92
93 Ok(file_contents)
94 }
95}
96
97fn visit_dir(
99 dir: &Path,
100 base: &Path,
101 file_contents: &mut Vec<(String, String)>,
102 excluded_entries: &[String],
103) -> Result<(), std::io::Error> {
104 for entry in read_dir(dir)? {
105 let entry = entry?;
106 let path = entry.path();
107 let relative_path = path
108 .strip_prefix(base)
109 .map_err(|e| io::Error::other(format!("Failed to strip base path: {e}")))?
110 .to_string_lossy()
111 .to_string();
112
113 if excluded_entries.contains(&relative_path) {
114 continue;
115 }
116
117 if path.is_dir() {
118 visit_dir(&path, base, file_contents, excluded_entries)?;
119 } else if path.extension().and_then(|s| s.to_str()) == Some("md") {
120 let rel_path = path
121 .strip_prefix(base)
122 .map_err(|e| io::Error::other(format!("Failed to strip base path: {e}")))?
123 .to_string_lossy()
124 .to_string();
125 let contents = read_file(path.to_str().unwrap()).map_err(|e| {
126 io::Error::other(format!("Failed to read file '{}': {}", path.display(), e))
127 })?;
128
129 file_contents.push((rel_path, contents));
130 }
131 }
132
133 Ok(())
134}
135
136pub fn read_file(file_path: &str) -> Result<String, std::io::Error> {
145 let mut md_file: File = File::open(file_path)?;
146
147 let mut contents = String::new();
148 md_file.read_to_string(&mut contents)?;
149
150 Ok(contents)
151}
152
153pub fn write_html_to_file(
163 html: &str,
164 output_dir: &str,
165 input_filepath: &str,
166) -> Result<(), io::Error> {
167 info!("Writing output to directory: {}", output_dir);
168 let output_dir = Path::new(output_dir).join(input_filepath);
169
170 if let Some(parent) = output_dir.parent() {
171 create_dir_all(parent)?;
172 }
173
174 let mut output_file = File::create(&output_dir)?;
175
176 output_file.write_all(html.as_bytes())?;
177
178 info!("HTML written to: {}", output_dir.display());
179 Ok(())
180}
181
182pub fn copy_file_to_output_dir(
195 input_file_path: &str,
196 output_dir: &str,
197 subdir: Option<&str>,
198 base_dir: Option<&str>,
199) -> Result<(), io::Error> {
200 use std::path::PathBuf;
201
202 let abs_input_path = if let Some(base) = base_dir {
203 let input_path = Path::new(input_file_path);
204 if input_path.is_absolute() {
205 input_path.to_path_buf()
206 } else {
207 Path::new(base).join(input_file_path)
208 }
209 } else {
210 PathBuf::from(input_file_path)
211 };
212
213 let file_name = abs_input_path.file_name().ok_or_else(|| {
214 io::Error::new(
215 io::ErrorKind::InvalidInput,
216 format!(
217 "Failed to extract file name from path '{}'",
218 abs_input_path.display()
219 ),
220 )
221 })?;
222
223 let mut output_file_path = PathBuf::from(output_dir);
224 if let Some(sub) = subdir {
225 output_file_path.push(sub);
226 create_dir_all(&output_file_path)?;
227 } else {
228 create_dir_all(&output_file_path)?
229 }
230 output_file_path.push(file_name);
231
232 fs::copy(&abs_input_path, &output_file_path)?;
233
234 Ok(())
235}
236
237pub fn copy_favicon_to_output_dir(
239 input_file_path: &str,
240 output_dir: &str,
241) -> Result<(), io::Error> {
242 copy_file_to_output_dir(input_file_path, output_dir, Some("media"), None)
243}
244
245pub fn copy_image_to_output_dir(
247 input_file_path: &str,
248 output_dir: &str,
249 md_dir: &str,
250) -> Result<(), io::Error> {
251 copy_file_to_output_dir(input_file_path, output_dir, Some("media"), Some(md_dir))
252}
253
254pub fn copy_css_to_output_dir(input_file_path: &str, output_dir: &str) -> Result<(), io::Error> {
256 copy_file_to_output_dir(input_file_path, output_dir, None, None)
257}
258
259pub fn write_default_css_file(output_dir: &str) -> Result<(), io::Error> {
261 let css_content = generate_default_css();
262 let css_file_path = format!("{}/styles.css", output_dir);
263
264 let mut file = File::create(&css_file_path)?;
265
266 file.write_all(css_content.as_bytes())?;
267
268 Ok(())
269}
270
271pub fn get_config_path() -> Result<PathBuf, io::Error> {
275 let mut config_path = config_dir().unwrap_or_else(|| PathBuf::from("."));
276
277 config_path.push("markrs");
278 create_dir_all(&config_path)?;
279 config_path.push("config.toml");
280
281 Ok(config_path)
282}
283
284pub fn does_config_exist() -> Result<bool, io::Error> {
286 let config_path = get_config_path()?;
287
288 let config_exists = config_path.exists();
289 Ok(config_exists)
290}
291
292pub fn write_default_config() -> Result<Config, crate::config::Error> {
295 let config_path = get_config_path()?;
296
297 info!(
298 "Config file does not exist, creating default config at: {}",
299 config_path.display()
300 );
301
302 let mut file = File::create(&config_path)?;
303
304 let default_config = Config::default();
305
306 let default_config_content = toml_edit::ser::to_string_pretty(&default_config)
307 .map_err(crate::config::Error::TomlSerialization)?;
308
309 file.write_all(default_config_content.as_bytes())?;
310
311 info!("Default config file created at: {}", config_path.display());
312
313 Ok(default_config)
314}