pub mod error;
pub mod options;
use serde::{Deserialize, Serialize};
use crate::function::FunctionMetadata;
use crate::{fmt_doc_comments, remove_test_code};
use self::error::AutodocsError;
use self::options::Options;
pub use self::options::options;
#[derive(Debug)]
pub struct ModuleDocumentation {
pub namespace: String,
pub name: String,
pub sub_modules: Vec<ModuleDocumentation>,
pub documentation: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub(crate) struct ModuleMetadata {
pub doc: Option<String>,
pub functions: Option<Vec<FunctionMetadata>>,
pub modules: Option<serde_json::Map<String, serde_json::Value>>,
}
impl ModuleMetadata {
pub fn fmt_doc_comments(&self) -> Option<String> {
self.doc
.clone()
.map(|dc| remove_test_code(&fmt_doc_comments(dc)))
}
}
pub fn generate_module_documentation(
engine: &rhai::Engine,
options: &Options,
) -> Result<ModuleDocumentation, AutodocsError> {
let json_fns = engine
.gen_fn_metadata_to_json(options.include_standard_packages)
.map_err(|error| AutodocsError::Metadata(error.to_string()))?;
let metadata = serde_json::from_str::<ModuleMetadata>(&json_fns)
.map_err(|error| AutodocsError::Metadata(error.to_string()))?;
generate_module_documentation_inner(options, None, "global", &metadata)
}
fn generate_module_documentation_inner(
options: &Options,
namespace: Option<String>,
name: impl Into<String>,
metadata: &ModuleMetadata,
) -> Result<ModuleDocumentation, AutodocsError> {
let name = name.into();
let namespace = namespace.map_or(name.clone(), |namespace| namespace);
let documentation = match options.markdown_processor {
options::MarkdownProcessor::MdBook => {
format!(
r#"# {}
```Namespace: {}```
{}"#,
&name,
&namespace,
metadata
.fmt_doc_comments()
.map_or_else(String::default, |doc| format!("{doc}\n\n"))
)
}
options::MarkdownProcessor::Docusaurus => {
format!(
r#"---
title: {}
slug: /{}
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
```Namespace: {}```
{}"#,
&name,
&namespace,
&namespace,
metadata
.fmt_doc_comments()
.map_or_else(String::default, |doc| format!("{doc}\n\n"))
)
}
};
let mut md = ModuleDocumentation {
namespace: namespace.clone(),
name,
sub_modules: vec![],
documentation,
};
if let Some(functions) = &metadata.functions {
let fn_groups = group_functions(options, &namespace, functions)?;
for (name, polymorphisms) in fn_groups {
if let Some(fn_doc) = generate_function_documentation(
options,
&name.replace("get$", "").replace("set$", ""),
&polymorphisms[..],
) {
md.documentation += &fn_doc;
}
}
}
if let Some(sub_modules) = &metadata.modules {
for (sub_module, value) in sub_modules {
md.sub_modules.push(generate_module_documentation_inner(
options,
Some(format!("{}/{}", namespace, sub_module)),
sub_module,
&serde_json::from_value::<ModuleMetadata>(value.clone())
.map_err(|error| AutodocsError::Metadata(error.to_string()))?,
)?);
}
}
Ok(md)
}
pub(crate) fn group_functions<'meta>(
options: &Options,
namespace: &str,
functions: &'meta [FunctionMetadata],
) -> Result<Vec<(String, Vec<&'meta FunctionMetadata>)>, AutodocsError> {
let mut function_groups =
std::collections::HashMap::<String, Vec<&FunctionMetadata>>::default();
functions.iter().for_each(|metadata| {
match function_groups.get_mut(&metadata.name) {
Some(polymorphisms) => polymorphisms.push(metadata),
None => {
function_groups.insert(metadata.name.clone(), vec![metadata]);
}
};
});
let function_groups = function_groups
.into_iter()
.map(|(name, polymorphisms)| (name, polymorphisms))
.collect::<Vec<_>>();
let fn_groups = options
.functions_order
.order_function_groups(namespace, function_groups)?;
Ok(fn_groups)
}
fn generate_function_documentation(
options: &Options,
name: &str,
polymorphisms: &[&FunctionMetadata],
) -> Option<String> {
let metadata = polymorphisms
.iter()
.find(|metadata| metadata.doc_comments.is_some())?;
let root_definition = metadata.generate_function_definition();
if !name.starts_with("anon$") {
match options.markdown_processor {
options::MarkdownProcessor::MdBook => {
Some(format!(
r#"
<div markdown="span" style='box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); padding: 15px; border-radius: 5px;'>
<h2 class="func-name"> <code>{}</code> {} </h2>
```rust,ignore
{}
```
{}
</div>
</br>
"#,
root_definition.type_to_str(),
root_definition.name(),
polymorphisms
.iter()
.map(|metadata| metadata.generate_function_definition().display())
.collect::<Vec<_>>()
.join("\n"),
&metadata
.fmt_doc_comments(&options.sections_format, &options.markdown_processor)
.unwrap_or_default()
))
}
options::MarkdownProcessor::Docusaurus => {
Some(format!(
r#"## <code>{}</code> {}
```js
{}
```
{}
"#,
root_definition.type_to_str(),
root_definition.name(),
polymorphisms
.iter()
.map(|metadata| metadata.generate_function_definition().display())
.collect::<Vec<_>>()
.join("\n"),
&metadata
.fmt_doc_comments(&options.sections_format, &options.markdown_processor)
.unwrap_or_default()
))
}
}
} else {
None
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::module::options::FunctionOrder;
use rhai::plugin::*;
#[export_module]
mod my_module {
#[rhai_fn(global)]
pub fn hello_world() {
println!("Hello, World!");
}
#[rhai_fn(global)]
pub fn add(a: rhai::INT, b: rhai::INT) -> rhai::INT {
a + b
}
}
#[test]
fn test_order_by_index() {
let mut engine = rhai::Engine::new();
engine.register_static_module("my_module", rhai::exported_module!(my_module).into());
let docs = options::options()
.include_standard_packages(false)
.order_functions_with(FunctionOrder::ByIndex)
.for_markdown_processor(options::MarkdownProcessor::MdBook)
.generate(&engine)
.expect("failed to generate documentation");
assert_eq!(docs.name, "global");
assert_eq!(
docs.documentation,
"# global\n\n```Namespace: global```\n\n"
);
let my_module = &docs.sub_modules[0];
assert_eq!(my_module.name, "my_module");
pretty_assertions::assert_eq!(
my_module.documentation,
r#"# my_module
```Namespace: global/my_module```
My own module.
<div markdown="span" style='box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); padding: 15px; border-radius: 5px;'>
<h2 class="func-name"> <code>fn</code> hello_world </h2>
```rust,ignore
fn hello_world()
```
<details>
<summary markdown="span"> details </summary>
A function that prints to stdout.
</details>
</div>
</br>
<div markdown="span" style='box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); padding: 15px; border-radius: 5px;'>
<h2 class="func-name"> <code>fn</code> add </h2>
```rust,ignore
fn add(a: int, b: int) -> int
```
<details>
<summary markdown="span"> details </summary>
A function that adds two integers together.
</details>
</div>
</br>
"#
);
}
}