Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

init: frontmatter support #114

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 76 additions & 1 deletion Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ serde = { version = "1.0", features = ["derive"] }
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"

[dev-dependencies]
insta = "1.36.1"
96 changes: 96 additions & 0 deletions doc/frontmatter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Frontmatter

This document is the specification for custom metadata within doc-comments.

It should only apply to doc-comments (`/** */`) and not to regular code comments to ensure a limited scope.

## Why frontmatter is needed (sometimes)

Sometimes it is desireable to extend the native doc-comment functionality. For that user scenario, frontmatter can be optionally used.
hsjobeki marked this conversation as resolved.
Show resolved Hide resolved

i.e.,

- Enriching documentation generation.
- Maintaining References.
- Metadata inclusion.
- Allow better Handling of edge cases.

## Detailed design

Fields (from Keywords list) can be defined in frontmatter.

Frontmatter is defined using key-value pairs, encapsulated within triple-dashed lines (---).

While there is no strict specification for frontmatter formats, YAML is commonly preferred. Although JSON could also be used alternatively.
hsjobeki marked this conversation as resolved.
Show resolved Hide resolved

`{key}` is a placeholder for the list of available keywords listed below.

`{value}` is a placeholder for the set value associated with the directive.
hsjobeki marked this conversation as resolved.
Show resolved Hide resolved

Example:

```nix
{
/**
---
import: ./path.md
---
*/
foo = x: x;
hsjobeki marked this conversation as resolved.
Show resolved Hide resolved
}
```

In this example, the `import` directive fetches content from `./path.md` treating it as if it were directly within the doc-comment.
This allows tracking the reference between the source position and the markdown file, in case of extensive documentation.
hsjobeki marked this conversation as resolved.
Show resolved Hide resolved

## Keywords

### Import
hsjobeki marked this conversation as resolved.
Show resolved Hide resolved

Rules:

1. The `import` keyword can be used to use content from another file INSTEAD of the doc-comments content.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's disallow content entirely if this is specified


2. The value `file` must be given as an absolute path (relative to git root) or relative to the current file.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should absolute paths be allowed?

  • (+) When the docs are entirely elsewhere, e.g. doc/manual/.., a relative path would have to be ../../.., very ugly
    • (-) If only relative paths are allowed, encourages moving docs closer to the source

      Makes changing documentation easier

      • For the Nixpkgs manual build, needs to adjust which files are included in the derivation source
    • (-) With only relative paths, it's more similar to NixOS module docs

  • (-) Makes it very confusing with where absolute paths are relative to (build root, git root, .nix location, etc.)
  • (-) We can still allow absolute paths later on if necessary
  • (-) Relies on a Git repository and git installed
  • (-) It's unclear where absolute paths are rooted
    • (+) Could use a syntax like $GIT_ROOt/foo/bar
      • (-) Not a fan of more custom syntax

Decision: Don't allow for now

This can be added to the design document (I recommend <details> </details>)


3. There can only be one `import` per doc-comment.

```nix
{
/**
---
import: ./path.md
---
*/
foo = x: x;
}
```

`path.md`
```md
some nice docs!
```

Rendering this would behave as if the content where actually placed in the doc-comment itself.
hsjobeki marked this conversation as resolved.
Show resolved Hide resolved

Placing frontmatter inside the imported file will be ignored. (No nested directives)
Since handling recursive or nested imports adds too much complexity for little or no benefit.
hsjobeki marked this conversation as resolved.
Show resolved Hide resolved

> Note: Absolute path imports are relative to the repository root. They only work inside `git` repositories and require having the `git` binary in PATH.
> This is most commonly used in large repositories or when the documentation files are not placed alongside the .nix files.

## Extensibility

The initial set of keywords is intentionally minimalistic,
focusing on immediate and broadly applicable needs.
hsjobeki marked this conversation as resolved.
Show resolved Hide resolved

Community contributions are encouraged to expand this list as new use cases emerge.

## Error handling

Any issues encountered during the processing of frontmatter—be it syntax errors, invalid paths, or unsupported keywords—should result in clear, actionable error messages to the user.

## Future work

This proposal represents a foundational step towards more versatile and extendable reference documentation.
As we move forward, we'll remain open to adapting and expanding this specification to meet emerging needs and leverage community insights.
61 changes: 61 additions & 0 deletions src/frontmatter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
process::Command,
};

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;

let path_str = String::from_utf8(output).ok()?.trim().to_string();
Some(PathBuf::from(path_str))
}

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

/// 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() {
hsjobeki marked this conversation as resolved.
Show resolved Hide resolved
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}")),
]),
}
});

abs_import.map(|path| {
fs::read_to_string(&path)
.expect(format!("Could not read file: {:?}", &path).as_str())
})
}
Err(_) => None,
hsjobeki marked this conversation as resolved.
Show resolved Hide resolved
}
}
6 changes: 4 additions & 2 deletions src/legacy.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::path::Path;

use rnix::{
ast::{AstToken, Comment, Expr, Lambda, Param},
SyntaxKind, SyntaxNode,
Expand Down Expand Up @@ -96,7 +98,7 @@ pub fn retrieve_legacy_comment(node: &SyntaxNode, allow_line_comments: bool) ->

/// Traverse directly chained nix lambdas and collect the identifiers of all lambda arguments
/// until an unexpected AST node is encountered.
pub fn collect_lambda_args_legacy(mut lambda: Lambda) -> Vec<Argument> {
pub fn collect_lambda_args_legacy(mut lambda: Lambda, file_path: &Path) -> Vec<Argument> {
let mut args = vec![];

loop {
Expand All @@ -120,7 +122,7 @@ pub fn collect_lambda_args_legacy(mut lambda: Lambda) -> Vec<Argument> {
.map(|entry| SingleArg {
name: entry.ident().unwrap().to_string(),
doc: handle_indentation(
&retrieve_doc_comment(entry.syntax(), Some(1))
&retrieve_doc_comment(entry.syntax(), Some(1), file_path)
.or(retrieve_legacy_comment(entry.syntax(), true))
.unwrap_or_default(),
),
Expand Down
Loading
Loading