1use crate::error::{FsError, ParseError};
8use crate::parse::{parse_html, parse_markdown};
9use crate::plugins::Manager;
10use crate::{Config, Error, TracebackError};
11
12pub use crate::parse::ParsedContents;
13
14use humphrey_json::prelude::*;
15use humphrey_json::Value;
16
17use std::fmt::Debug;
18use std::fs::{create_dir, metadata, read, read_dir, remove_dir_all, write};
19use std::io::ErrorKind;
20use std::path::{Component, Path, PathBuf};
21use std::rc::Rc;
22
23#[derive(Clone)]
25pub enum Node {
26 File {
28 name: String,
30 contents: Vec<u8>,
32 parsed_contents: ParsedContents,
34 metadata: Option<Value>,
36 source: PathBuf,
38 },
39 Directory {
41 name: String,
43 children: Vec<Node>,
45 source: PathBuf,
47 },
48}
49
50impl Node {
51 pub fn new(root: impl AsRef<Path>, parse: bool) -> Result<Self, Error> {
53 let root = root.as_ref().to_path_buf().canonicalize().map_err(|_| {
54 Error::Fs(FsError::NotFound(
55 root.as_ref().to_string_lossy().to_string(),
56 ))
57 })?;
58
59 Self::create_from_dir(root, parse, None)
60 }
61
62 pub fn new_with_plugins(
64 root: impl AsRef<Path>,
65 parse: bool,
66 plugins: &dyn Manager,
67 ) -> Result<Self, Error> {
68 let root = root.as_ref().to_path_buf().canonicalize().map_err(|_| {
69 Error::Fs(FsError::NotFound(
70 root.as_ref().to_string_lossy().to_string(),
71 ))
72 })?;
73
74 Self::create_from_dir(root, parse, Some(plugins))
75 }
76
77 pub fn is_dir(&self) -> bool {
79 matches!(self, Node::Directory { .. })
80 }
81
82 pub fn is_file(&self) -> bool {
84 matches!(self, Node::File { .. })
85 }
86
87 pub fn name(&self) -> &str {
89 match self {
90 Node::File { name, .. } => name,
91 Node::Directory { name, .. } => name,
92 }
93 }
94
95 pub fn children(&self) -> Option<&[Node]> {
97 match self {
98 Node::Directory { children, .. } => Some(children),
99 Node::File { .. } => None,
100 }
101 }
102
103 pub fn contents(&self) -> Option<&[u8]> {
105 match self {
106 Node::File { contents, .. } => Some(contents),
107 Node::Directory { .. } => None,
108 }
109 }
110
111 pub fn parsed_contents(&self) -> &ParsedContents {
113 match self {
114 Node::File {
115 parsed_contents, ..
116 } => parsed_contents,
117 Node::Directory { .. } => &ParsedContents::None,
118 }
119 }
120
121 pub fn parsed_contents_mut(&mut self) -> &mut ParsedContents {
124 match self {
125 Node::File {
126 parsed_contents, ..
127 } => parsed_contents,
128 Node::Directory { .. } => {
129 panic!("`Node::parsed_contents_mut` should only be used on files")
130 }
131 }
132 }
133
134 pub fn source(&self) -> &Path {
136 match self {
137 Node::File { source, .. } => source,
138 Node::Directory { source, .. } => source,
139 }
140 }
141
142 pub fn get_at_path(&self, path: &Path) -> Option<&Self> {
144 let mut working_path = vec![self];
145
146 for part in path.components() {
147 match part {
148 Component::Normal(name) => {
149 working_path.push(
150 working_path
151 .last()
152 .and_then(|n| n.children())
153 .and_then(|children| children.iter().find(|n| n.name() == name))?,
154 );
155 }
156 Component::CurDir => (),
157 _ => return None,
158 }
159 }
160
161 working_path.last().copied()
162 }
163
164 pub(crate) fn create_from_dir(
166 dir: impl AsRef<Path>,
167 parse: bool,
168 plugins: Option<&dyn Manager>,
169 ) -> Result<Self, Error> {
170 let dir = dir.as_ref();
171 let content = read_dir(dir)
172 .map_err(|_| Error::Fs(FsError::NotFound(dir.to_string_lossy().to_string())))?;
173
174 let children = content
175 .flatten()
176 .map(|path| {
177 let path = path.path();
178
179 match metadata(&path).map(|m| m.file_type()) {
180 Ok(t) if t.is_dir() => Self::create_from_dir(&path, parse, plugins),
181 Ok(t) if t.is_file() => Self::create_from_file(&path, parse, plugins),
182 _ => Err(Error::Fs(FsError::Read)),
183 }
184 })
185 .collect::<Result<_, _>>()?;
186
187 Ok(Node::Directory {
188 name: dir.file_name().unwrap().to_string_lossy().to_string(),
189 children,
190 source: dir.to_path_buf(),
191 })
192 }
193
194 pub(crate) fn create_from_file(
196 file: impl AsRef<Path>,
197 parse: bool,
198 plugins: Option<&dyn Manager>,
199 ) -> Result<Self, Error> {
200 let file = file.as_ref();
201 let name = file.file_name().unwrap().to_string_lossy().to_string();
202 let contents = read(file).map_err(|_| Error::Fs(FsError::Read))?;
203
204 let parsed_contents = if parse {
205 let extension = file.extension().map(|e| e.to_string_lossy().to_string());
206 let contents_string =
207 std::str::from_utf8(&contents).map_err(|_| Error::Fs(FsError::Read));
208
209 match extension.as_deref() {
210 Some("html") => ParsedContents::Html(
211 parse_html(contents_string?, file, plugins).map_err(Error::Parse)?,
212 ),
213 Some("md") => ParsedContents::Markdown(
214 parse_markdown(contents_string?.to_string(), file, plugins)
215 .map_err(Error::Parse)?,
216 ),
217 Some("json") => ParsedContents::Json(
218 humphrey_json::from_str(contents_string?).map_err(|_| {
219 Error::Parse(TracebackError {
220 path: file.to_path_buf(),
221 kind: ParseError::InvalidJson,
222 column: 0,
223 line: 0,
224 })
225 })?,
226 ),
227 Some(extension) => {
228 let mut result = ParsedContents::None;
229
230 if let Some(plugins) = plugins {
231 'outer: for plugin in plugins.plugins() {
232 for parser in &plugin.parsers {
233 if parser.extensions().contains(&extension) {
234 result = ParsedContents::Custom(Rc::new(
235 parser.parse(&contents, file).map_err(Error::Plugin)?,
236 ));
237 break 'outer;
238 }
239 }
240 }
241 }
242
243 result
244 }
245 None => ParsedContents::None,
246 }
247 } else {
248 ParsedContents::Ignored
249 };
250
251 Ok(Node::File {
252 name,
253 contents,
254 parsed_contents,
255 metadata: None,
256 source: file.to_path_buf(),
257 })
258 }
259
260 pub fn save(&self, path: impl AsRef<Path>, config: &Config) -> Result<(), Error> {
262 let path = path.as_ref().to_path_buf();
263
264 if path.exists() && path.is_dir() {
265 remove_dir_all(&path).map_err(|_| Error::Fs(FsError::Write))?;
266 }
267
268 match self {
269 Self::Directory { children, .. } => {
270 create_dir(&path).map_err(|_| Error::Fs(FsError::Write))?;
271
272 for child in children {
273 child.save_recur(&path, config)?;
274 }
275 }
276 _ => panic!("`Node::save` should only be used on the root directory"),
277 }
278
279 Ok(())
280 }
281
282 pub fn save_metadata(&self, mut base: Value, path: impl AsRef<Path>) -> Result<(), Error> {
285 base["data"] = self.save_metadata_recur(true);
286
287 write(path, base.serialize()).map_err(|_| Error::Fs(FsError::Write))?;
288
289 Ok(())
290 }
291
292 pub fn merge(&mut self, other: Node) -> Result<(), Error> {
295 match (self, other) {
296 (
297 Self::Directory { children, .. },
298 Self::Directory {
299 children: other_children,
300 ..
301 },
302 ) => {
303 for other_child in other_children {
304 if let Some(child) = children
305 .iter_mut()
306 .find(|child| child.name() == other_child.name())
307 {
308 if matches!(child, Self::Directory { .. })
311 && matches!(other_child, Self::Directory { .. })
312 {
313 child.merge(other_child)?;
314 } else {
315 return Err(Error::Fs(FsError::Conflict(
316 child.source().to_path_buf(),
317 other_child.source().to_path_buf(),
318 )));
319 }
320 } else {
321 children.push(other_child);
322 }
323 }
324
325 Ok(())
326 }
327 _ => panic!("`Node::merge` should only be used on directories"),
328 }
329 }
330
331 fn save_recur(&self, path: impl AsRef<Path>, config: &Config) -> Result<(), Error> {
333 let path = path.as_ref().to_path_buf();
334
335 match self {
336 Self::Directory { name, children, .. } => {
337 let dir = path.join(name);
338
339 match create_dir(&dir) {
341 Ok(_) => (),
342 Err(e) if e.kind() == ErrorKind::AlreadyExists => (),
343 Err(_) => return Err(Error::Fs(FsError::Write)),
344 };
345
346 for child in children {
347 child.save_recur(&dir, config)?;
348 }
349 }
350 Self::File {
351 name,
352 contents,
353 parsed_contents,
354 ..
355 } => {
356 if name != "root.html"
357 && name != "md.html"
358 && (config.save_data_files || !name.ends_with(".json"))
359 {
360 if config.strip_extensions
361 && name.ends_with(".html")
362 && name != "index.html"
363 && !parsed_contents.is_ignored()
364 {
365 let directory_name = name.strip_suffix(".html").unwrap().to_string();
366 let dir = path.join(directory_name);
367
368 match create_dir(&dir) {
369 Ok(_) => (),
370 Err(e) if e.kind() == ErrorKind::AlreadyExists => (),
371 Err(_) => return Err(Error::Fs(FsError::Write)),
372 };
373
374 write(dir.join("index.html"), contents)
375 .map_err(|_| Error::Fs(FsError::Write))?;
376 } else {
377 write(path.join(name), contents).map_err(|_| Error::Fs(FsError::Write))?;
378 }
379 }
380 }
381 }
382
383 Ok(())
384 }
385
386 fn save_metadata_recur(&self, is_first: bool) -> Value {
388 match self {
389 Self::Directory { name, children, .. } => {
390 let children = children
391 .iter()
392 .map(|c| c.save_metadata_recur(false))
393 .collect();
394
395 if is_first {
396 Value::Array(children)
397 } else {
398 json!({
399 "type": "directory",
400 "name": name,
401 "children": (Value::Array(children))
402 })
403 }
404 }
405 Self::File {
406 name,
407 metadata: json,
408 ..
409 } => {
410 let mut metadata = json!({ "name": name });
411
412 if let Some(json) = json {
413 for (key, value) in json.as_object().unwrap() {
414 metadata[key.as_str()] = value.clone();
415 }
416 } else {
417 metadata["type"] = json!("file");
418 }
419
420 metadata
421 }
422 }
423 }
424}
425
426impl Debug for Node {
427 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428 match self {
429 Self::File {
430 name,
431 contents,
432 parsed_contents,
433 metadata,
434 source,
435 } => f
436 .debug_struct("File")
437 .field("name", name)
438 .field("contents", &format!("{} bytes", contents.len()))
439 .field("parsed_contents", parsed_contents)
440 .field("metadata", metadata)
441 .field("source", source)
442 .finish(),
443 Self::Directory {
444 name,
445 children,
446 source,
447 } => f
448 .debug_struct("Directory")
449 .field("name", name)
450 .field("children", children)
451 .field("source", source)
452 .finish(),
453 }
454 }
455}