Skip to content

Commit

Permalink
Merge pull request #129 from Janik-Haag/json-output
Browse files Browse the repository at this point in the history
feat: add json_output
  • Loading branch information
infinisil authored Aug 30, 2024
2 parents 203aa64 + 267c357 commit c4ad7f5
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 159 deletions.
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

0 comments on commit c4ad7f5

Please sign in to comment.