diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f8aeb8..8bf233a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Version 3.1.0 + +Added frontmatter feature: + +- `doc_location` keyword allows to externalize documentation. + +See our [full specification](./doc/frontmatter.md). + +by @hsjobeki; + +in https://github.com/nix-community/nixdoc/pull/114. + ## Version 3.0.2 Avoid displaying arguments when a doc-comment is already in place. diff --git a/Cargo.lock b/Cargo.lock index 344f5a3..0f9b844 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,24 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "gray_matter" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cf2fb99fac0b821a4e61c61abff076324bb0e5c3b4a83815bbc3518a38971ad" +dependencies = [ + "serde", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -138,6 +156,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "indexmap" +version = "2.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "insta" version = "1.36.1" @@ -186,14 +214,17 @@ dependencies = [ [[package]] name = "nixdoc" -version = "3.0.2" +version = "3.1.0" dependencies = [ "clap", + "gray_matter", "insta", + "relative-path", "rnix", "rowan", "serde", "serde_json", + "serde_yaml", "textwrap", ] @@ -215,6 +246,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "relative-path" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" + [[package]] name = "rnix" version = "0.11.0" @@ -280,6 +317,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0623d197252096520c6f2a5e1171ee436e5af99a5d7caa2891e55e61950e6d9" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "similar" version = "2.4.0" @@ -326,6 +376,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -344,6 +403,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 1df6a8b..b62a3a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "nixdoc" -version = "3.0.2" -authors = ["Vincent Ambo ", "asymmetric"] +version = "3.1.0" +authors = ["Vincent Ambo ", "asymmetric", "hsjobeki"] edition = "2021" description = "Generate CommonMark from Nix library functions" @@ -12,6 +12,9 @@ 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" +gray_matter = "0.2.6" +relative-path = "1.9.2" [dev-dependencies] insta = "1.36.1" diff --git a/README.md b/README.md index e24b741..4d6b109 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,20 @@ It is important to start doc-comments with the additional asterisk (`*`) -> `/** The content of the doc-comment should conform to the [Commonmark](https://spec.commonmark.org/0.30/) specification. -### Example +### Examples + +#### Example: simple but complete example The following is an example of markdown documentation for new and current users of nixdoc. -> Sidenote: Indentation is automatically detected and should be consistent across the content. -> +> Sidenote: Indentation is automatically detected and should be consistent across the content. +> > If you are used to multiline-strings (`''`) in nix this should be intuitive to follow. +`simple.nix` ````nix { - /** + /** This function adds two numbers # Example @@ -60,10 +63,39 @@ The following is an example of markdown documentation for new and current users > Note: Within nixpkgs the convention of using [definition-lists](https://www.markdownguide.org/extended-syntax/#definition-lists) for documenting arguments has been established. +#### Example: using frontmatter + +In some cases, it may be desirable to store the documentation in a separate Markdown file. +Nixdoc supports frontmatter, which allows a separate Markdown file to be referenced within the doc comment. + +`fontmatter.nix` +````nix +{ + /** + --- + doc_location: ./docs.md + --- + */ + add = a: b: a + b; +} +```` + +`./docs.md` +````markdown +# Add + +A simple function that adds two numbers. +```` + +> Note: Frontmatter works only with the newer doc-comments `/** */`. + +See our [frontmatter specification](./doc/frontmatter.md) for further information. + +## Legacy support -## Custom nixdoc format (Legacy) +### Custom nixdoc format -You should consider migrating to the newer format described above. +You should consider migrating to the newer format described above. The format described in this section will be removed with the next major release. See [Migration guide](./doc/migration.md). diff --git a/doc/frontmatter.md b/doc/frontmatter.md new file mode 100644 index 0000000..8af9385 --- /dev/null +++ b/doc/frontmatter.md @@ -0,0 +1,114 @@ +# 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. + +Frontmatter is the de-facto standard of adding document specific meta tags to markdown. [1](https://docs.github.com/en/contributing/writing-for-github-docs/using-yaml-frontmatter) [2](https://jekyllrb.com/docs/front-matter/) [3](https://starlight.astro.build/reference/frontmatter/) + +it can be useful for: + +- Enriching documentation generation. +- Maintaining References. +- Metadata inclusion. +- Allow better Handling of documentation edge cases. + +## Detailed design + +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. + +Only `key`s from the list of available keywords can be used. + +The `value` is any valid yaml value. Its type and semantics is specified by each keyword individually. + +Example: + +```nix +{ + /** + --- + key: value + --- + */ + foo = x: x; +} +``` + +See also: [yaml specification](https://yaml.org/spec/1.2.2/) for how to use YAML. + +## Keywords + +### `doc_location` + +Rules: + +1. The `doc_location` keyword can be used to use content from another file and **forbid** any further content to be added to the existing doc-comment. + +2. The value must be given as a path relative to the current file. + +3. The file pointed to by `doc_location` will be used as-is, without any further processing done to it. + +```nix +{ + /** + --- + doc_location: ./path.md + --- + */ + foo = x: x; +} +``` + +`path.md` +```md +some nice docs! +``` + +In this example, the `doc_location` directive fetches content from `./path.md` and treats it as the actual doc-comment. +This allows tracking the reference between the source position and the markdown file, in case of external documentation. + +#### Design decision: Relative vs Absolute paths + +This section explains the decision to only use relative paths and not support absolute paths for the file pointed to by `doc_location`. + +
+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 nix-build, adjustments of which files are included in the derivation source may be needed. + - (-) With only relative paths, it's more similar to NixOS module docs +- (-) We can still allow absolute paths later on if necessary +- (-) Makes it very confusing where absolute paths are relative to (build root, git root, `.nix` location, etc.) + - (+) Could use a syntax like `$GIT_ROOT/foo/bar` + - (-) Relies on a Git repository and git installed + - (-) Not a fan of more custom syntax + +**Decision**: Not supported by now. + +This outcome was discussed in the nix documentation team meeting: https://discourse.nixos.org/t/2024-03-21-documentation-team-meeting-notes-114/41957. + +
+ +## 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. + +## Extensibility + +The initial set of keywords is intentionally minimalistic, focusing on immediate and broadly applicable needs. + +When extending this document we ask our contributors to be tooling agnostic, such that documentation wont't rely on any implementation details. +This approach ensures that the documentation remains independent of specific implementation details. By adhering to this principle, we aim to create a resource that is universally applicable. + +## 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. diff --git a/src/frontmatter.rs b/src/frontmatter.rs new file mode 100644 index 0000000..41f2056 --- /dev/null +++ b/src/frontmatter.rs @@ -0,0 +1,121 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use gray_matter::engine::YAML; +use gray_matter::Matter; +use std::fmt; + +use relative_path::RelativePath; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug)] +pub struct Frontmatter { + pub doc_location: Option, +} + +#[derive(Debug, PartialEq)] +pub enum FrontmatterErrorKind { + InvalidYaml, + DocLocationFileNotFound, + DocLocationConflictWithContent, + DocLocationNotRelativePath, +} + +#[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 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, FrontmatterError> { + let matter = Matter::::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(); + let has_content = !result.content.trim().is_empty(); + match pod.deserialize::() { + Ok(metadata) => { + let abs_import = match metadata.doc_location { + Some(doc_location) => { + if has_content { + return Err(FrontmatterError { + message: format!( + "{:?}: doc_location: if this field is specified no other content is allowed for the doc-comment.", + file_path + ), + kind: FrontmatterErrorKind::DocLocationConflictWithContent, + }); + } + + 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), + } + } + + Err(e) => { + let message = format!( + "{:?}: Failed to parse frontmatter metadata - {} YAML:{}:{}", + file_path, + e, + e.line(), + e.column() + ); + Err(FrontmatterError { + message, + kind: FrontmatterErrorKind::InvalidYaml, + }) + } + } +} diff --git a/src/legacy.rs b/src/legacy.rs index fc11923..3aa873d 100644 --- a/src/legacy.rs +++ b/src/legacy.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use rnix::{ ast::{AstToken, Comment, Expr, Lambda, Param}, SyntaxKind, SyntaxNode, @@ -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 { +pub fn collect_lambda_args_legacy(mut lambda: Lambda, file_path: &Path) -> Vec { let mut args = vec![]; loop { @@ -120,7 +122,7 @@ pub fn collect_lambda_args_legacy(mut lambda: Lambda) -> Vec { .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(), ), diff --git a/src/main.rs b/src/main.rs index 3a93330..d3a8db6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,7 @@ mod comment; mod commonmark; mod format; +mod frontmatter; mod legacy; #[cfg(test)] mod test; @@ -33,13 +34,14 @@ use crate::{format::handle_indentation, legacy::retrieve_legacy_comment}; use self::comment::get_expr_docs; use self::commonmark::*; use format::shift_headings; +use frontmatter::get_imported_content; use legacy::{collect_lambda_args_legacy, LegacyDocItem}; use rnix::{ ast::{Attr, AttrpathValue, Expr, Inherit, LetIn}, SyntaxKind, SyntaxNode, }; use rowan::{ast::AstNode, WalkEvent}; -use std::fs; +use std::{fs, path::Path, process::exit}; use std::collections::HashMap; use std::io; @@ -99,12 +101,33 @@ enum DocItemOrLegacy { } /// Returns a rfc145 doc-comment if one is present -pub fn retrieve_doc_comment(node: &SyntaxNode, shift_headings_by: Option) -> Option { +pub fn retrieve_doc_comment( + node: &SyntaxNode, + shift_headings_by: Option, + file: &Path, +) -> Option { let doc_comment = get_expr_docs(node); - doc_comment.map(|doc_comment| { + doc_comment.map(|inner| { + // Must handle indentation before processing yaml frontmatter + 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(&doc_comment).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) @@ -115,14 +138,14 @@ pub fn retrieve_doc_comment(node: &SyntaxNode, shift_headings_by: Option) /// Transforms an AST node into a `DocItem` if it has a leading /// documentation comment. -fn retrieve_doc_item(node: &AttrpathValue) -> Option { +fn retrieve_doc_item(node: &AttrpathValue, file_path: &Path) -> Option { let ident = node.attrpath().unwrap(); // TODO this should join attrs() with '.' to handle whitespace, dynamic attrs and string // attrs. none of these happen in nixpkgs lib, and the latter two should probably be // rejected entirely. let item_name = ident.to_string(); - let doc_comment = retrieve_doc_comment(node.syntax(), Some(2)); + let doc_comment = retrieve_doc_comment(node.syntax(), Some(2), file_path); match doc_comment { Some(comment) => Some(DocItemOrLegacy::DocItem(DocItem { name: item_name, @@ -192,14 +215,14 @@ fn parse_doc_comment(raw: &str) -> DocComment { /// 2. The attached doc comment on the entry. /// 3. The argument names of any curried functions (pattern functions /// not yet supported). -fn collect_entry_information(entry: AttrpathValue) -> Option { - let doc_item = retrieve_doc_item(&entry)?; +fn collect_entry_information(entry: AttrpathValue, file_path: &Path) -> Option { + let doc_item = retrieve_doc_item(&entry, file_path)?; match doc_item { DocItemOrLegacy::LegacyDocItem(v) => { if let Some(Expr::Lambda(l)) = entry.value() { Some(LegacyDocItem { - args: collect_lambda_args_legacy(l), + args: collect_lambda_args_legacy(l, file_path), ..v }) } else { @@ -223,6 +246,7 @@ fn collect_bindings( prefix: &str, category: &str, scope: HashMap, + file_path: &Path, ) -> Vec { for ev in node.preorder() { match ev { @@ -231,7 +255,7 @@ fn collect_bindings( for child in n.children() { if let Some(apv) = AttrpathValue::cast(child.clone()) { entries.extend( - collect_entry_information(apv) + collect_entry_information(apv, file_path) .map(|di| di.into_entry(prefix, category)), ); } else if let Some(inh) = Inherit::cast(child) { @@ -259,7 +283,12 @@ fn collect_bindings( // Main entrypoint for collection // TODO: document -fn collect_entries(root: rnix::Root, prefix: &str, category: &str) -> Vec { +fn collect_entries( + root: rnix::Root, + prefix: &str, + category: &str, + file_path: &Path, +) -> Vec { // we will look into the top-level let and its body for function docs. // we only need a single level of scope for this. // since only the body can export a function we don't need to implement @@ -273,13 +302,14 @@ fn collect_entries(root: rnix::Root, prefix: &str, category: &str) -> Vec { - return collect_bindings(&n, prefix, category, Default::default()); + return collect_bindings(&n, prefix, category, Default::default(), file_path); } _ => (), } @@ -288,14 +318,19 @@ fn collect_entries(root: rnix::Root, prefix: &str, category: &str) -> Vec String { +fn retrieve_description( + nix: &rnix::Root, + description: &str, + category: &str, + file: &Path, +) -> String { format!( "# {} {{#sec-functions-library-{}}}\n{}\n", description, category, &nix.syntax() .first_child() - .and_then(|node| retrieve_doc_comment(&node, Some(1)) + .and_then(|node| retrieve_doc_comment(&node, Some(1), file) .or(retrieve_legacy_comment(&node, false))) .and_then(|doc_item| handle_indentation(&doc_item)) .unwrap_or_default() @@ -314,12 +349,12 @@ fn main() { .expect("could not read location information"), }; let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); - let description = retrieve_description(&nix, &opts.description, &opts.category); + let description = retrieve_description(&nix, &opts.description, &opts.category, &opts.file); // TODO: move this to commonmark.rs writeln!(output, "{}", description).expect("Failed to write header"); - for entry in collect_entries(nix, &opts.prefix, &opts.category) { + for entry in collect_entries(nix, &opts.prefix, &opts.category, &opts.file) { entry .write_section(&locs, &mut output) .expect("Failed to write section") diff --git a/src/snapshots/nixdoc__test__frontmatter_doc_location_e2e.snap b/src/snapshots/nixdoc__test__frontmatter_doc_location_e2e.snap new file mode 100644 index 0000000..eeaa068 --- /dev/null +++ b/src/snapshots/nixdoc__test__frontmatter_doc_location_e2e.snap @@ -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 diff --git a/src/test.rs b/src/test.rs index cafd651..fe55883 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,14 +1,21 @@ use rnix; use std::fs; +use std::path::PathBuf; use std::io::Write; -use crate::{collect_entries, format::shift_headings, retrieve_description}; +use crate::{ + collect_entries, + format::shift_headings, + frontmatter::{get_imported_content, FrontmatterError, FrontmatterErrorKind}, + retrieve_description, +}; #[test] fn test_main() { let mut output = Vec::new(); - let src = fs::read_to_string("test/strings.nix").unwrap(); + let src_path = PathBuf::from("test/strings.nix"); + let src = fs::read_to_string(&src_path).unwrap(); let locs = serde_json::from_str(&fs::read_to_string("test/strings.json").unwrap()).unwrap(); let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); let desc = "string manipulation functions"; @@ -23,7 +30,7 @@ fn test_main() { ) .expect("Failed to write header"); - for entry in collect_entries(nix, prefix, category) { + for entry in collect_entries(nix, prefix, category, &src_path) { entry .write_section(&locs, &mut output) .expect("Failed to write section") @@ -37,14 +44,15 @@ fn test_main() { #[test] fn test_description_of_lib_debug() { let mut output = Vec::new(); - let src = fs::read_to_string("test/lib-debug.nix").unwrap(); + let src_path = PathBuf::from("test/lib-debug.nix"); + let src = fs::read_to_string(&src_path).unwrap(); let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); let prefix = "lib"; let category = "debug"; - let desc = retrieve_description(&nix, &"Debug", category); + let desc = retrieve_description(&nix, &"Debug", category, &src_path); writeln!(output, "{}", desc).expect("Failed to write header"); - for entry in collect_entries(nix, prefix, category) { + for entry in collect_entries(nix, prefix, category, &src_path) { entry .write_section(&Default::default(), &mut output) .expect("Failed to write section") @@ -58,12 +66,13 @@ fn test_description_of_lib_debug() { #[test] fn test_arg_formatting() { let mut output = Vec::new(); - let src = fs::read_to_string("test/arg-formatting.nix").unwrap(); + let src_path = PathBuf::from("test/arg-formatting.nix"); + let src = fs::read_to_string(&src_path).unwrap(); let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); let prefix = "lib"; let category = "options"; - for entry in collect_entries(nix, prefix, category) { + for entry in collect_entries(nix, prefix, category, &src_path) { entry .write_section(&Default::default(), &mut output) .expect("Failed to write section") @@ -77,12 +86,13 @@ fn test_arg_formatting() { #[test] fn test_inherited_exports() { let mut output = Vec::new(); - let src = fs::read_to_string("test/inherited-exports.nix").unwrap(); + let src_path = PathBuf::from("test/inherited-exports.nix"); + let src = fs::read_to_string(&src_path).unwrap(); let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); let prefix = "lib"; let category = "let"; - for entry in collect_entries(nix, prefix, category) { + for entry in collect_entries(nix, prefix, category, &src_path) { entry .write_section(&Default::default(), &mut output) .expect("Failed to write section") @@ -96,12 +106,13 @@ fn test_inherited_exports() { #[test] fn test_line_comments() { let mut output = Vec::new(); - let src = fs::read_to_string("test/line-comments.nix").unwrap(); + let src_path = PathBuf::from("test/line-comments.nix"); + let src = fs::read_to_string(&src_path).unwrap(); let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); let prefix = "lib"; let category = "let"; - for entry in collect_entries(nix, prefix, category) { + for entry in collect_entries(nix, prefix, category, &src_path) { entry .write_section(&Default::default(), &mut output) .expect("Failed to write section") @@ -115,12 +126,13 @@ fn test_line_comments() { #[test] fn test_multi_line() { let mut output = Vec::new(); - let src = fs::read_to_string("test/multi-line.nix").unwrap(); + let src_path = PathBuf::from("test/multi-line.nix"); + let src = fs::read_to_string(&src_path).unwrap(); let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); let prefix = "lib"; let category = "let"; - for entry in collect_entries(nix, prefix, category) { + for entry in collect_entries(nix, prefix, category, &src_path) { entry .write_section(&Default::default(), &mut output) .expect("Failed to write section") @@ -134,12 +146,13 @@ fn test_multi_line() { #[test] fn test_doc_comment() { let mut output = Vec::new(); - let src = fs::read_to_string("test/doc-comment.nix").unwrap(); + let src_path = PathBuf::from("test/doc-comment.nix"); + let src = fs::read_to_string(&src_path).unwrap(); let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); let prefix = "lib"; let category = "debug"; - for entry in collect_entries(nix, prefix, category) { + for entry in collect_entries(nix, prefix, category, &src_path) { entry .write_section(&Default::default(), &mut output) .expect("Failed to write section") @@ -162,14 +175,15 @@ fn test_headings() { #[test] fn test_doc_comment_section_description() { let mut output = Vec::new(); - let src = fs::read_to_string("test/doc-comment-sec-heading.nix").unwrap(); + let src_path = PathBuf::from("test/doc-comment-sec-heading.nix"); + let src = fs::read_to_string(&src_path).unwrap(); let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); let prefix = "lib"; let category = "debug"; - let desc = retrieve_description(&nix, &"Debug", category); + let desc = retrieve_description(&nix, &"Debug", category, &src_path); writeln!(output, "{}", desc).expect("Failed to write header"); - for entry in collect_entries(nix, prefix, category) { + for entry in collect_entries(nix, prefix, category, &src_path) { entry .write_section(&Default::default(), &mut output) .expect("Failed to write section") @@ -183,14 +197,15 @@ fn test_doc_comment_section_description() { #[test] fn test_doc_comment_no_duplicate_arguments() { let mut output = Vec::new(); - let src = fs::read_to_string("test/doc-comment-arguments.nix").unwrap(); + let src_path = PathBuf::from("test/doc-comment-arguments.nix"); + let src = fs::read_to_string(&src_path).unwrap(); let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); let prefix = "lib"; let category = "debug"; - let desc = retrieve_description(&nix, &"Debug", category); + let desc = retrieve_description(&nix, &"Debug", category, &src_path); writeln!(output, "{}", desc).expect("Failed to write header"); - for entry in collect_entries(nix, prefix, category) { + for entry in collect_entries(nix, prefix, category, &src_path) { entry .write_section(&Default::default(), &mut output) .expect("Failed to write section") @@ -200,3 +215,91 @@ fn test_doc_comment_no_duplicate_arguments() { insta::assert_snapshot!(output); } + +#[test] +fn test_frontmatter_doc_location_e2e() { + let mut output = Vec::new(); + let src_path = PathBuf::from("test/frontmatter-doc-location.nix"); + let src = fs::read_to_string(&src_path).unwrap(); + let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); + let prefix = "lib"; + let category = "debug"; + let desc = retrieve_description(&nix, &"Debug", category, &src_path); + writeln!(output, "{}", desc).expect("Failed to write header"); + + for entry in collect_entries(nix, prefix, category, &src_path) { + entry + .write_section(&Default::default(), &mut output) + .expect("Failed to write section") + } + + let output = String::from_utf8(output).expect("not utf8"); + + insta::assert_snapshot!(output); +} + +const NOT_RELATIVE: &str = r#"--- +doc_location: /tmp/not-relative.md +--- +"#; + +#[test] +fn test_frontmatter_doc_location_relative() { + let base_file = PathBuf::from("test/frontmatter-doc-location.nix"); + + let result = get_imported_content(&base_file, NOT_RELATIVE); + + assert_eq!( + result.unwrap_err().kind, + FrontmatterErrorKind::DocLocationNotRelativePath + ); +} + +const CONFLICT_DOCS: &str = r#"--- +doc_location: docs.md +--- +Other stuff +"#; + +#[test] +fn test_frontmatter_conflict_docs() { + let base_file = PathBuf::from("test/frontmatter-doc-location.nix"); + + let result = get_imported_content(&base_file, CONFLICT_DOCS); + + assert_eq!( + result.unwrap_err().kind, + FrontmatterErrorKind::DocLocationConflictWithContent + ); +} + +const INVALID_TYPE: &str = r#"--- +doc_location: 1 +--- +"#; + +#[test] +fn test_frontmatter_doc_location_type() { + let base_file = PathBuf::from("test/frontmatter-doc-location.nix"); + + let result = get_imported_content(&base_file, INVALID_TYPE); + + assert_eq!(result.unwrap_err().kind, FrontmatterErrorKind::InvalidYaml); +} + +const FILE_NOT_FOUND: &str = r#"--- +doc_location: ./does-not-exist.md +--- +"#; + +#[test] +fn test_frontmatter_doc_location_file_not_found() { + let base_file = PathBuf::from("test/frontmatter-doc-location.nix"); + + let result = get_imported_content(&base_file, FILE_NOT_FOUND); + + assert_eq!( + result.unwrap_err().kind, + FrontmatterErrorKind::DocLocationFileNotFound + ); +} diff --git a/test/docs.md b/test/docs.md new file mode 100644 index 0000000..99a1a67 --- /dev/null +++ b/test/docs.md @@ -0,0 +1,3 @@ +# Imported + +This is be the documentation diff --git a/test/frontmatter-doc-location.nix b/test/frontmatter-doc-location.nix new file mode 100644 index 0000000..a4d4c15 --- /dev/null +++ b/test/frontmatter-doc-location.nix @@ -0,0 +1,18 @@ +/** + --- + doc_location: docs.md + --- +*/ +{}: { + /** + --- + doc_location: docs.md + --- + */ + item = x: x; + + /** + No frontmatter + */ + optional = x: x; +} \ No newline at end of file