|
1 | 1 | use std::{
|
2 |
| - collections::HashMap, |
3 | 2 | fs,
|
4 | 3 | path::{Path, PathBuf},
|
5 |
| - process::Command, |
6 | 4 | };
|
7 | 5 |
|
| 6 | +use gray_matter::engine::YAML; |
| 7 | +use gray_matter::Matter; |
| 8 | +use std::fmt; |
| 9 | + |
| 10 | +use relative_path::RelativePath; |
8 | 11 | use serde::{Deserialize, Serialize};
|
9 |
| -use yaml_front_matter::YamlFrontMatter; |
10 | 12 |
|
11 |
| -fn find_repo_root() -> Option<PathBuf> { |
12 |
| - let output = Command::new("git") |
13 |
| - .args(["rev-parse", "--show-toplevel"]) |
14 |
| - .output() |
15 |
| - .ok()? |
16 |
| - .stdout; |
| 13 | +#[derive(Deserialize, Serialize, Debug)] |
| 14 | +pub struct Frontmatter { |
| 15 | + pub doc_location: Option<String>, |
| 16 | +} |
17 | 17 |
|
18 |
| - let path_str = String::from_utf8(output).ok()?.trim().to_string(); |
19 |
| - Some(PathBuf::from(path_str)) |
| 18 | +#[derive(Debug, PartialEq)] |
| 19 | +pub enum FrontmatterErrorKind { |
| 20 | + InvalidYaml, |
| 21 | + DocLocationFileNotFound, |
| 22 | + DocLocationNotRelativePath, |
20 | 23 | }
|
21 | 24 |
|
22 |
| -#[derive(Deserialize, Serialize, Debug)] |
23 |
| -pub struct Matter { |
24 |
| - #[serde(flatten)] |
25 |
| - pub content: HashMap<String, serde_yaml::Value>, |
| 25 | +#[derive(Debug, PartialEq)] |
| 26 | +pub struct FrontmatterError { |
| 27 | + pub message: String, |
| 28 | + pub kind: FrontmatterErrorKind, |
| 29 | +} |
| 30 | + |
| 31 | +impl fmt::Display for FrontmatterError { |
| 32 | + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 33 | + // Write the error message to the formatter |
| 34 | + write!(f, "FrontmatterError: {}", self.message) |
| 35 | + } |
26 | 36 | }
|
27 | 37 |
|
28 |
| -/// Returns the actual content of a markdown file, if the frontmatter has an import field. |
29 |
| -pub fn get_imported_content(file_path: &Path, markdown: Option<&String>) -> Option<String> { |
30 |
| - markdown?; |
31 |
| - |
32 |
| - match YamlFrontMatter::parse::<Matter>(markdown.unwrap()) { |
33 |
| - Ok(document) => { |
34 |
| - let metadata = document.metadata.content; |
35 |
| - |
36 |
| - let abs_import = metadata.get("import").map(|field| { |
37 |
| - let import_val = field |
38 |
| - .as_str() |
39 |
| - .expect("Frontmatter: import field must be a string"); |
40 |
| - match PathBuf::from(import_val).is_relative() { |
41 |
| - true => PathBuf::from_iter(vec![ |
42 |
| - // Cannot fail because every file has a parent directory |
43 |
| - file_path.parent().unwrap().to_path_buf(), |
44 |
| - PathBuf::from(import_val), |
45 |
| - ]), |
46 |
| - false => PathBuf::from_iter(vec![ |
47 |
| - find_repo_root() |
48 |
| - .expect("Could not find root directory of repository. Make sure you have git installed and are in a git repository"), |
49 |
| - PathBuf::from(format!(".{import_val}")), |
50 |
| - ]), |
| 38 | +/// Returns the actual content of a markdown file, if the frontmatter has a doc_location field. |
| 39 | +/// It returns None if the frontmatter is not present. |
| 40 | +/// It returns an error if the frontmatter is present but invalid. This includes: |
| 41 | +/// - Invalid yaml frontmatter |
| 42 | +/// - Invalid doc_location type |
| 43 | +/// - doc_location file is not readable or not found |
| 44 | +/// - doc_location field is not a relative path |
| 45 | +/// - doc_location file is not utf8 |
| 46 | +pub fn get_imported_content( |
| 47 | + file_path: &Path, |
| 48 | + markdown: &str, |
| 49 | +) -> Result<Option<String>, FrontmatterError> { |
| 50 | + let matter = Matter::<YAML>::new(); |
| 51 | + |
| 52 | + let result = matter.parse(markdown); |
| 53 | + |
| 54 | + // If the frontmatter is not present, we return None |
| 55 | + if result.data.is_none() { |
| 56 | + return Ok(None); |
| 57 | + } |
| 58 | + |
| 59 | + let pod = result.data.unwrap(); |
| 60 | + match pod.deserialize::<Frontmatter>() { |
| 61 | + Ok(metadata) => { |
| 62 | + let abs_import = match metadata.doc_location { |
| 63 | + Some(doc_location) => { |
| 64 | + let import_path: PathBuf = PathBuf::from(&doc_location); |
| 65 | + let relative_path = RelativePath::from_path(&import_path); |
| 66 | + |
| 67 | + match relative_path { |
| 68 | + Ok(rel) => Ok(Some(rel.to_path(file_path.parent().unwrap()))), |
| 69 | + Err(e) => Err(FrontmatterError { |
| 70 | + message: format!("{:?}: doc_location: field must be a path relative to the current file. Error: {} - {}", file_path, doc_location, e), |
| 71 | + kind: FrontmatterErrorKind::DocLocationNotRelativePath, |
| 72 | + }), |
| 73 | + } |
51 | 74 | }
|
52 |
| - }); |
| 75 | + // doc_location: field doesn't exist. Since it is optional, we return None |
| 76 | + None => Ok(None), |
| 77 | + }; |
| 78 | + |
| 79 | + match abs_import { |
| 80 | + Ok(Some(path)) => match fs::read_to_string(&path) { |
| 81 | + Ok(content) => Ok(Some(content)), |
| 82 | + Err(e) => Err(FrontmatterError { |
| 83 | + message: format!( |
| 84 | + "{:?}: Failed to read doc_location file: {:?} {}", |
| 85 | + file_path, path, e |
| 86 | + ), |
| 87 | + kind: FrontmatterErrorKind::DocLocationFileNotFound, |
| 88 | + }), |
| 89 | + }, |
| 90 | + Ok(None) => Ok(None), |
| 91 | + Err(e) => Err(e), |
| 92 | + } |
| 93 | + } |
53 | 94 |
|
54 |
| - abs_import.map(|path| { |
55 |
| - fs::read_to_string(&path) |
56 |
| - .expect(format!("Could not read file: {:?}", &path).as_str()) |
| 95 | + Err(e) => { |
| 96 | + let message = format!( |
| 97 | + "{:?}: Failed to parse frontmatter metadata - {} YAML:{}:{}", |
| 98 | + file_path, |
| 99 | + e, |
| 100 | + e.line(), |
| 101 | + e.column() |
| 102 | + ); |
| 103 | + Err(FrontmatterError { |
| 104 | + message, |
| 105 | + kind: FrontmatterErrorKind::InvalidYaml, |
57 | 106 | })
|
58 | 107 | }
|
59 |
| - Err(_) => None, |
60 | 108 | }
|
61 | 109 | }
|
0 commit comments