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

feat: add json_output #129

Merged
merged 2 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
100 changes: 53 additions & 47 deletions src/commonmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,19 @@
//! This module implements CommonMark output for a struct
//! representing a single entry in the manual.

use std::collections::HashMap;

use std::io::{Result, Write};
use serde::Serialize;

/// Represent a single function argument name and its (optional)
/// doc-string.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize)]
pub struct SingleArg {
pub name: String,
pub doc: Option<String>,
}

/// Represent a function argument, which is either a flat identifier
/// or a pattern set.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize)]
pub enum Argument {
/// Flat function argument (e.g. `n: n * 2`).
Flat(SingleArg),
Expand Down Expand Up @@ -99,16 +97,42 @@ fn handle_indentation(raw: &str) -> String {
}
}

/// Generate the identifier for CommonMark.
/// ident is used as URL Encoded link to the function and has thus stricter rules (i.e. "' " in "lib.map' " is not allowed).
pub(crate) fn get_identifier(prefix: &String, category: &String, name: &String) -> String {
let name_prime = name.replace('\'', "-prime");
vec![prefix, category, &name_prime]
.into_iter()
.filter(|x| !x.is_empty())
.cloned()
.collect::<Vec<String>>()
.join(".")
}

/// Generate the title for CommonMark.
/// the title is the human-readable name of the function.
pub(crate) fn get_title(prefix: &String, category: &String, name: &String) -> String {
vec![prefix, category, name]
.into_iter()
.filter(|x| !x.is_empty())
.cloned()
.collect::<Vec<String>>()
.join(".")
}

/// Represents a single manual section describing a library function.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize)]
pub struct ManualEntry {
/// Prefix for the category (e.g. 'lib' or 'utils').
pub prefix: String,

/// Name of the function category (e.g. 'strings', 'trivial', 'attrsets')
/// Name of the function category (e.g. 'strings', 'trivial', 'attrsets').
pub category: String,

/// Name of the section (used as the title)
/// Location of the function.
pub location: Option<String>,

/// Name of the section (used as the title).
pub name: String,

/// Type signature (if provided). This is not actually a checked
Expand All @@ -122,61 +146,44 @@ pub struct ManualEntry {
/// Usage example for the entry.
pub example: Option<String>,

/// Arguments of the function
/// Arguments of the function.
pub args: Vec<Argument>,
}

impl ManualEntry {
/// Generate the identifier and title for CommonMark.
/// title is the human-readable name of the function.
/// ident is used as URL Encoded link to the function and has thus stricter rules (i.e. "' " in "lib.map' " is not allowed).
pub(crate) fn get_ident_title(&self) -> (String, String) {
let name_prime = self.name.replace('\'', "-prime");

let ident = vec![&self.prefix, &self.category, &name_prime]
.into_iter()
.filter(|x| !x.is_empty())
.cloned()
.collect::<Vec<String>>()
.join(".");

let title = vec![&self.prefix, &self.category, &self.name]
.into_iter()
.filter(|x| !x.is_empty())
.cloned()
.collect::<Vec<String>>()
.join(".");

let ident = get_identifier(&self.prefix, &self.category, &self.name);
let title = get_title(&self.prefix, &self.category, &self.name);
(ident, title)
}

/// Write a single CommonMark entry for a documented Nix function.
pub fn write_section<W: Write>(
self,
locs: &HashMap<String, String>,
writer: &mut W,
) -> Result<()> {
pub fn write_section(self, output: &mut String) -> String {
let (ident, title) = self.get_ident_title();
writeln!(writer, "## `{}` {{#function-library-{}}}\n", title, ident)?;
output.push_str(&format!(
"## `{}` {{#function-library-{}}}\n\n",
title, ident
));

// <subtitle> (type signature)
if let Some(t) = &self.fn_type {
if t.lines().count() > 1 {
writeln!(writer, "**Type**:\n```\n{}\n```\n", t)?;
output.push_str(&format!("**Type**:\n```\n{}\n```\n\n", t));
} else {
writeln!(writer, "**Type**: `{}`\n", t)?;
output.push_str(&format!("**Type**: `{}`\n\n", t));
}
}

// Primary doc string
// TODO: Split paragraphs?
for paragraph in &self.description {
writeln!(writer, "{}\n", paragraph)?;
output.push_str(&format!("{}\n\n", paragraph));
}

// Function argument names
if !self.args.is_empty() {
for arg in self.args {
writeln!(writer, "{}", arg.format_argument())?;
output.push_str(&format!("{}\n", arg.format_argument()));
}
}

Expand All @@ -185,19 +192,18 @@ impl ManualEntry {
// TODO: In grhmc's version there are multiple (named)
// examples, how can this be achieved automatically?
if let Some(example) = &self.example {
writeln!(
writer,
"::: {{.example #function-library-example-{}}}",
output.push_str(&format!(
"::: {{.example #function-library-example-{}}}\n",
ident
)?;
writeln!(writer, "# `{}` usage example\n", title)?;
writeln!(writer, "```nix\n{}\n```\n:::\n", example.trim())?;
));
output.push_str(&format!("# `{}` usage example\n\n", title));
output.push_str(&format!("```nix\n{}\n```\n:::\n\n", example.trim()));
}

if let Some(loc) = locs.get(&ident) {
writeln!(writer, "Located at {loc}.\n")?;
if let Some(loc) = self.location {
output.push_str(&String::from(format!("Located at {loc}.\n\n")));
}

Ok(())
output.to_string()
}
}
17 changes: 15 additions & 2 deletions src/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ use rnix::{
SyntaxKind, SyntaxNode,
};
use rowan::ast::AstNode;
use std::collections::HashMap;

use crate::{
commonmark::{Argument, ManualEntry, SingleArg},
format::handle_indentation,
retrieve_doc_comment, DocComment,
get_identifier, retrieve_doc_comment, DocComment,
};

#[derive(Debug)]
Expand All @@ -18,10 +19,22 @@ pub struct LegacyDocItem {
}

impl LegacyDocItem {
pub fn into_entry(self, prefix: &str, category: &str) -> ManualEntry {
pub fn into_entry(
self,
prefix: &str,
category: &str,
locs: &HashMap<String, String>,
) -> ManualEntry {
let ident = get_identifier(
&prefix.to_string(),
&category.to_string(),
&self.name.to_string(),
);

ManualEntry {
prefix: prefix.to_string(),
category: category.to_string(),
location: locs.get(&ident).cloned(),
name: self.name,
description: self
.comment
Expand Down
64 changes: 48 additions & 16 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,8 @@ use rnix::{
use rowan::{ast::AstNode, WalkEvent};
use std::fs;

use serde::Serialize;
use std::collections::HashMap;
use std::io;
use std::io::Write;

use clap::Parser;
use std::path::PathBuf;
Expand All @@ -56,6 +55,10 @@ struct Options {
#[arg(short, long, default_value_t = String::from("lib"))]
prefix: String,

/// Whether to output JSON.
#[arg(short, long, default_value_t = false)]
json_output: bool,

/// Name of the function category (e.g. 'strings', 'attrsets').
#[arg(short, long)]
category: String,
Expand Down Expand Up @@ -93,6 +96,12 @@ struct DocItem {
comment: DocComment,
}

#[derive(Debug, Serialize)]
struct JsonFormat {
version: u32,
entries: Vec<ManualEntry>,
}

enum DocItemOrLegacy {
LegacyDocItem(LegacyDocItem),
DocItem(DocItem),
Expand Down Expand Up @@ -222,6 +231,7 @@ fn collect_bindings(
node: &SyntaxNode,
prefix: &str,
category: &str,
locs: &HashMap<String, String>,
scope: HashMap<String, ManualEntry>,
) -> Vec<ManualEntry> {
for ev in node.preorder() {
Expand All @@ -232,7 +242,7 @@ fn collect_bindings(
if let Some(apv) = AttrpathValue::cast(child.clone()) {
entries.extend(
collect_entry_information(apv)
.map(|di| di.into_entry(prefix, category)),
.map(|di| di.into_entry(prefix, category, locs)),
);
} else if let Some(inh) = Inherit::cast(child) {
// `inherit (x) ...` needs much more handling than we can
Expand All @@ -259,7 +269,12 @@ fn collect_bindings(

// Main entrypoint for collection
// TODO: document
fn collect_entries(root: rnix::Root, prefix: &str, category: &str) -> Vec<ManualEntry> {
fn collect_entries(
root: rnix::Root,
prefix: &str,
category: &str,
locs: &HashMap<String, String>,
) -> Vec<ManualEntry> {
// 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
Expand All @@ -276,15 +291,16 @@ fn collect_entries(root: rnix::Root, prefix: &str, category: &str) -> Vec<Manual
LetIn::cast(n.clone()).unwrap().body().unwrap().syntax(),
prefix,
category,
locs,
n.children()
.filter_map(AttrpathValue::cast)
.filter_map(collect_entry_information)
.map(|di| (di.name.to_string(), di.into_entry(prefix, category)))
.map(|di| (di.name.to_string(), di.into_entry(prefix, category, locs)))
.collect(),
);
}
WalkEvent::Enter(n) if n.kind() == SyntaxKind::NODE_ATTR_SET => {
return collect_bindings(&n, prefix, category, Default::default());
return collect_bindings(&n, prefix, category, locs, Default::default());
}
_ => (),
}
Expand All @@ -307,9 +323,7 @@ fn retrieve_description(nix: &rnix::Root, description: &str, category: &str) ->
)
}

fn main() {
let mut output = io::stdout();
let opts = Options::parse();
fn main_with_options(opts: Options) -> String {
let src = fs::read_to_string(&opts.file).unwrap();
let locs = match opts.locs {
None => Default::default(),
Expand All @@ -321,12 +335,30 @@ fn main() {
let nix = rnix::Root::parse(&src).ok().expect("failed to parse input");
let description = retrieve_description(&nix, &opts.description, &opts.category);

// TODO: move this to commonmark.rs
writeln!(output, "{}", description).expect("Failed to write header");

for entry in collect_entries(nix, &opts.prefix, &opts.category) {
entry
.write_section(&locs, &mut output)
.expect("Failed to write section")
let entries = collect_entries(nix, &opts.prefix, &opts.category, &locs);

if opts.json_output {
let json_string = match serde_json::to_string(&JsonFormat {
version: 1,
entries,
}) {
Ok(json) => json,
Err(error) => panic!("Problem converting entries to JSON: {error:?}"),
};
json_string
} else {
// TODO: move this to commonmark.rs
let mut output = description + "\n";

for entry in entries {
entry.write_section(&mut output);
}
output
}
}

fn main() {
let opts = Options::parse();
let output = main_with_options(opts);
println!("{}", output)
}
6 changes: 6 additions & 0 deletions src/snapshots/nixdoc__test__json_output.snap

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/snapshots/nixdoc__test__main.snap
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
---
source: src/main.rs
source: src/test.rs
assertion_line: 23
expression: output
---
# string manipulation functions {#sec-functions-library-strings}
String manipulation functions.

## `lib.strings.concatStrings` {#function-library-lib.strings.concatStrings}

Expand Down Expand Up @@ -1693,5 +1695,3 @@ levenshteinAtMost 3 "This is a sentence" "this is a sentense."
:::

Located at [lib/strings.nix:1183](https://github.com/NixOS/nixpkgs/blob/580dd2124db98c13c3798af23c2ecf6277ec7d9e/lib/strings.nix#L1183) in `<nixpkgs>`.


Loading