Skip to content

Commit 6217f2e

Browse files
committed
unit tests: add frontmatter unit tests
1 parent c48fb33 commit 6217f2e

File tree

8 files changed

+260
-90
lines changed

8 files changed

+260
-90
lines changed

Cargo.lock

Lines changed: 30 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ serde_json = "1.0"
1313
textwrap = "0.16"
1414
clap = { version = "4.4.4", features = ["derive"] }
1515
serde_yaml = "0.9.33"
16-
yaml-front-matter = "0.1.0"
16+
gray_matter = "0.2.6"
17+
relative-path = "1.9.2"
1718

1819
[dev-dependencies]
1920
insta = "1.36.1"

src/frontmatter.rs

Lines changed: 91 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,109 @@
11
use std::{
2-
collections::HashMap,
32
fs,
43
path::{Path, PathBuf},
5-
process::Command,
64
};
75

6+
use gray_matter::engine::YAML;
7+
use gray_matter::Matter;
8+
use std::fmt;
9+
10+
use relative_path::RelativePath;
811
use serde::{Deserialize, Serialize};
9-
use yaml_front_matter::YamlFrontMatter;
1012

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

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,
2023
}
2124

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+
}
2636
}
2737

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+
}
5174
}
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+
}
5394

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,
57106
})
58107
}
59-
Err(_) => None,
60108
}
61109
}

src/main.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use rnix::{
4141
SyntaxKind, SyntaxNode,
4242
};
4343
use rowan::{ast::AstNode, WalkEvent};
44-
use std::{fs, path::Path};
44+
use std::{fs, path::Path, process::exit};
4545

4646
use std::collections::HashMap;
4747
use std::io;
@@ -108,12 +108,26 @@ pub fn retrieve_doc_comment(
108108
) -> Option<String> {
109109
let doc_comment = get_expr_docs(node);
110110

111-
// Doc comments can import external file via "import" in frontmatter
112-
let content = get_imported_content(file, doc_comment.as_ref()).or(doc_comment);
111+
doc_comment.map(|inner| {
112+
// Must handle indentation before processing yaml frontmatter
113+
let content = handle_indentation(&inner).unwrap_or_default();
114+
115+
let final_content = match get_imported_content(file, &content) {
116+
// Use the imported content instead of the original content
117+
Ok(Some(imported_content)) => imported_content,
118+
119+
// Use the original content
120+
Ok(None) => content,
121+
122+
// Abort if we failed to read the frontmatter
123+
Err(e) => {
124+
eprintln!("{}", e);
125+
exit(1);
126+
}
127+
};
113128

114-
content.map(|inner| {
115129
shift_headings(
116-
&handle_indentation(&inner).unwrap_or(String::new()),
130+
&handle_indentation(&final_content).unwrap_or(String::new()),
117131
// H1 to H4 can be used in the doc-comment with the current rendering.
118132
// They will be shifted to H3, H6
119133
// H1 and H2 are currently used by the outer rendering. (category and function name)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
source: src/test.rs
3+
expression: output
4+
---
5+
# Debug {#sec-functions-library-debug}
6+
## Imported
7+
8+
This is be the documentation
9+
10+
## `lib.debug.item` {#function-library-lib.debug.item}
11+
12+
### Imported
13+
14+
This is be the documentation
15+
16+
## `lib.debug.optional` {#function-library-lib.debug.optional}
17+
18+
No frontmatter

0 commit comments

Comments
 (0)