Skip to content

Commit

Permalink
unit tests: add frontmatter unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
hsjobeki committed Mar 22, 2024
1 parent c48fb33 commit e10882a
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 89 deletions.
70 changes: 30 additions & 40 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ serde_json = "1.0"
textwrap = "0.16"
clap = { version = "4.4.4", features = ["derive"] }
serde_yaml = "0.9.33"
yaml-front-matter = "0.1.0"
gray_matter = "0.2.6"
relative-path = "1.9.2"

[dev-dependencies]
insta = "1.36.1"
134 changes: 91 additions & 43 deletions src/frontmatter.rs
Original file line number Diff line number Diff line change
@@ -1,61 +1,109 @@
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
process::Command,
};

use gray_matter::engine::YAML;
use gray_matter::Matter;
use std::fmt;

use relative_path::RelativePath;
use serde::{Deserialize, Serialize};
use yaml_front_matter::YamlFrontMatter;

fn find_repo_root() -> Option<PathBuf> {
let output = Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
.ok()?
.stdout;
#[derive(Deserialize, Serialize, Debug)]
pub struct Frontmatter {
pub doc_location: Option<String>,
}

let path_str = String::from_utf8(output).ok()?.trim().to_string();
Some(PathBuf::from(path_str))
#[derive(Debug, PartialEq)]
pub enum FrontmatterErrorKind {
InvalidYaml,
DocLocationFileNotFound,
DocLocationNotRelativePath,
}

#[derive(Deserialize, Serialize, Debug)]
pub struct Matter {
#[serde(flatten)]
pub content: HashMap<String, serde_yaml::Value>,
#[derive(Debug, PartialEq)]
pub struct FrontmatterError {
pub message: String,
pub kind: FrontmatterErrorKind,
}

impl fmt::Display for FrontmatterError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Write the error message to the formatter
write!(f, "FrontmatterError: {}", self.message)
}
}

/// Returns the actual content of a markdown file, if the frontmatter has an import field.
pub fn get_imported_content(file_path: &Path, markdown: Option<&String>) -> Option<String> {
markdown?;

match YamlFrontMatter::parse::<Matter>(markdown.unwrap()) {
Ok(document) => {
let metadata = document.metadata.content;

let abs_import = metadata.get("import").map(|field| {
let import_val = field
.as_str()
.expect("Frontmatter: import field must be a string");
match PathBuf::from(import_val).is_relative() {
true => PathBuf::from_iter(vec![
// Cannot fail because every file has a parent directory
file_path.parent().unwrap().to_path_buf(),
PathBuf::from(import_val),
]),
false => PathBuf::from_iter(vec![
find_repo_root()
.expect("Could not find root directory of repository. Make sure you have git installed and are in a git repository"),
PathBuf::from(format!(".{import_val}")),
]),
/// Returns the actual content of a markdown file, if the frontmatter has a doc_location field.
/// It returns None if the frontmatter is not present.
/// It returns an error if the frontmatter is present but invalid. This includes:
/// - Invalid yaml frontmatter
/// - Invalid doc_location type
/// - doc_location file is not readable or not found
/// - doc_location field is not a relative path
/// - doc_location file is not utf8
pub fn get_imported_content(
file_path: &Path,
markdown: &str,
) -> Result<Option<String>, FrontmatterError> {
let matter = Matter::<YAML>::new();

let result = matter.parse(markdown);

// If the frontmatter is not present, we return None
if result.data.is_none() {
return Ok(None);
}

let pod = result.data.unwrap();
match pod.deserialize::<Frontmatter>() {
Ok(metadata) => {
let abs_import = match metadata.doc_location {
Some(doc_location) => {
let import_path: PathBuf = PathBuf::from(&doc_location);
let relative_path = RelativePath::from_path(&import_path);

match relative_path {
Ok(rel) => Ok(Some(rel.to_path(file_path.parent().unwrap()))),
Err(e) => Err(FrontmatterError {
message: format!("{:?}: doc_location: field must be a path relative to the current file. Error: {} - {}", file_path, doc_location, e),
kind: FrontmatterErrorKind::DocLocationNotRelativePath,
}),
}
}
});
// doc_location: field doesn't exist. Since it is optional, we return None
None => Ok(None),
};

match abs_import {
Ok(Some(path)) => match fs::read_to_string(&path) {
Ok(content) => Ok(Some(content)),
Err(e) => Err(FrontmatterError {
message: format!(
"{:?}: Failed to read doc_location file: {:?} {}",
file_path, path, e
),
kind: FrontmatterErrorKind::DocLocationFileNotFound,
}),
},
Ok(None) => Ok(None),
Err(e) => Err(e),
}
}

abs_import.map(|path| {
fs::read_to_string(&path)
.expect(format!("Could not read file: {:?}", &path).as_str())
Err(e) => {
let message = format!(
"{:?}: Failed to parse frontmatter metadata - {} YAML:{}:{}",
file_path,
e,
e.line(),
e.column()
);
Err(FrontmatterError {
message,
kind: FrontmatterErrorKind::InvalidYaml,
})
}
Err(_) => None,
}
}
23 changes: 19 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ use rnix::{
SyntaxKind, SyntaxNode,
};
use rowan::{ast::AstNode, WalkEvent};
use std::{fs, path::Path};
use std::{fs, path::Path, process::exit};

use std::collections::HashMap;
use std::io;
Expand Down Expand Up @@ -109,11 +109,26 @@ pub fn retrieve_doc_comment(
let doc_comment = get_expr_docs(node);

// Doc comments can import external file via "import" in frontmatter
let content = get_imported_content(file, doc_comment.as_ref()).or(doc_comment);

content.map(|inner| {
doc_comment.map(|inner| {
let content = handle_indentation(&inner).unwrap_or_default();

let final_content = match get_imported_content(file, &content) {
// Use the imported content instead of the original content
Ok(Some(imported_content)) => imported_content,

// Use the original content
Ok(None) => content,

// Abort if we failed to read the frontmatter
Err(e) => {
eprintln!("{}", e);
exit(1);
}
};

shift_headings(
&handle_indentation(&inner).unwrap_or(String::new()),
&handle_indentation(&final_content).unwrap_or(String::new()),
// H1 to H4 can be used in the doc-comment with the current rendering.
// They will be shifted to H3, H6
// H1 and H2 are currently used by the outer rendering. (category and function name)
Expand Down
18 changes: 18 additions & 0 deletions src/snapshots/nixdoc__test__frontmatter_doc_location_e2e.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
source: src/test.rs
expression: output
---
# Debug {#sec-functions-library-debug}
## Imported

This is be the documentation

## `lib.debug.item` {#function-library-lib.debug.item}

### Imported

This is be the documentation

## `lib.debug.optional` {#function-library-lib.debug.optional}

No frontmatter
Loading

0 comments on commit e10882a

Please sign in to comment.