rhai_autodocs/
generate.rs

1use serde_json::json;
2
3use crate::{item::Item, module::Documentation};
4
5/// Glossary of all function for a module and it's submodules.
6#[derive(Debug)]
7pub struct Glossary {
8    /// Formatted function signatures by submodules.
9    pub content: String,
10}
11
12pub const GLOSSARY_COLOR_FN: &str = "#C6cacb";
13pub const GLOSSARY_COLOR_OP: &str = "#16c6f3";
14pub const GLOSSARY_COLOR_GETSET: &str = "#25c2a0";
15pub const GLOSSARY_COLOR_INDEX: &str = "#25c2a0";
16
17#[derive(Default)]
18pub struct DocusaurusOptions {
19    slug: Option<String>,
20    module_name: Option<String>,
21}
22
23impl DocusaurusOptions {
24    /// Format the slug in the metadata section of the generated MDX document by concatenating the `slug` parameter with the module name.
25    ///
26    /// For example, if the documentation for a module called `my_module` is generated with
27    /// the slug `/docs/api/`, the slug set in the document will be `/docs/api/my_module`.
28    ///
29    /// By default the root `/` path is used.
30    #[must_use]
31    pub fn with_slug(mut self, slug: &str) -> Self {
32        self.slug = Some(slug.to_string());
33
34        self
35    }
36
37    /// When registering stuff into your engine, some items will be exported in the "global" module, a module
38    /// that is accessible without the need to specify it's name. For documentation sake, you can use this method
39    /// to rename the global module so that you can split multiple items groups into multiple global modules without
40    /// having the "global" slug everywhere.
41    ///
42    /// For example, if the documentation exports items under the global namespace with
43    /// the slug `/docs/api/` and the module renamed as `my_module`, the slug set in the document will be
44    /// `/docs/api/my_module` instead of `/docs/api/global`.
45    ///
46    /// By default the root `global` module name is used.
47    #[must_use]
48    pub fn rename_root_module(mut self, name: &str) -> Self {
49        self.module_name = Some(name.to_string());
50
51        self
52    }
53
54    /// Build MDX documentation for docusaurus from the given module documentation struct.
55    ///
56    /// # Return
57    ///
58    /// A hashmap with the name of the module as the key and its raw documentation as the value.
59    //
60    /// # Errors
61    ///
62    /// Handlebar failed to render the variables in the module documentation.
63    #[allow(clippy::missing_panics_doc)]
64    pub fn generate(
65        self,
66        module: &Documentation,
67    ) -> Result<std::collections::HashMap<String, String>, handlebars::RenderError> {
68        let mut hbs_registry = handlebars::Handlebars::new();
69        let mut module = module.clone();
70
71        if let Some(module_name) = self.module_name {
72            module.name = module_name;
73        }
74
75        hbs_registry
76            .register_template_string(
77                "docusaurus-module",
78                include_str!("handlebars/docusaurus/module.hbs"),
79            )
80            .expect("template is valid");
81
82        // A partial used to keep indentation for mdx to render correctly.
83        hbs_registry
84            .register_partial("ContentPartial", "{{{content}}}")
85            .expect("partial is valid");
86
87        generate(
88            &module,
89            "docusaurus-module",
90            self.slug.as_deref(),
91            &hbs_registry,
92        )
93    }
94}
95
96/// Create a new builder to generate documentation for docusaurus from a [`super::module::Documentation`] object.
97#[must_use]
98pub fn docusaurus() -> DocusaurusOptions {
99    DocusaurusOptions::default()
100}
101
102#[derive(Default)]
103pub struct DocusaurusGlossaryOptions {
104    slug: Option<String>,
105}
106
107impl DocusaurusGlossaryOptions {
108    /// Format the slug in the metadata section of the generated MDX document.
109    ///
110    /// By default the root `/glossary` path is used.
111    #[must_use]
112    pub fn with_slug(mut self, slug: &str) -> Self {
113        self.slug = Some(slug.to_string());
114
115        self
116    }
117
118    /// Build MDX documentation for docusaurus from the given module documentation struct, with
119    /// a glossary that group all functions from all submodules.
120    ///
121    /// # Return
122    ///
123    /// A glossary and a hashmap with the name of the module as the key and its raw documentation as the value.
124    ///
125    /// # Errors
126    ///
127    /// Handlebar failed to render the variables in the module documentation.
128    #[allow(clippy::missing_panics_doc)]
129    pub fn generate(self, module: &Documentation) -> Result<String, handlebars::RenderError> {
130        let mut hbs = handlebars::Handlebars::new();
131
132        hbs.register_template_string(
133            "docusaurus-glossary",
134            include_str!("handlebars/docusaurus/glossary.hbs"),
135        )
136        .expect("template is valid");
137
138        self.generate_inner(&hbs, true, module)
139    }
140
141    fn generate_inner(
142        &self,
143        hbs: &handlebars::Handlebars<'_>,
144        is_root: bool,
145        module: &Documentation,
146    ) -> Result<String, handlebars::RenderError> {
147        let mut flatten_items = Vec::default();
148
149        for item in &module.items {
150            match item {
151                Item::Function { metadata, .. } => {
152                    for m in metadata {
153                        let definition = m.generate_function_definition();
154                        let serialized = definition.display();
155                        let ty = definition.type_to_str();
156                        let color = match ty {
157                            "op" => GLOSSARY_COLOR_OP,
158                            "get/set" => GLOSSARY_COLOR_GETSET,
159                            "index get/set" => GLOSSARY_COLOR_INDEX,
160                            _ => GLOSSARY_COLOR_FN,
161                        };
162
163                        flatten_items.push(json!({
164                            "color": color,
165                            "type": ty,
166                            "definition": serialized.trim_start_matches(ty).trim(),
167                            "heading_id": item.heading_id(),
168                        }));
169                    }
170                }
171                Item::CustomType { metadata, .. } => {
172                    flatten_items.push(json!({
173                        "color": GLOSSARY_COLOR_FN,
174                        "type": "type",
175                        "definition": metadata.display_name,
176                        "heading_id": item.heading_id(),
177                    }));
178                }
179            }
180        }
181
182        let data = json!({
183            "title": module.name,
184            "root": is_root,
185            "slug": self.slug.clone().unwrap_or_default(),
186            "items": flatten_items,
187        });
188
189        let mut glossary = hbs.render("docusaurus-glossary", &data)?;
190
191        for module in &module.sub_modules {
192            glossary += self.generate_inner(hbs, false, module)?.as_str();
193        }
194
195        Ok(glossary)
196    }
197}
198
199/// Create a new builder to generate a function glossary for docusaurus from a [`super::module::Documentation`] object.
200#[must_use]
201pub fn docusaurus_glossary() -> DocusaurusGlossaryOptions {
202    DocusaurusGlossaryOptions::default()
203}
204
205#[derive(Default)]
206pub struct MDBookOptions;
207
208impl MDBookOptions {
209    /// Build html documentation for mdbook from the given module documentation struct.
210    ///
211    /// Returns a hashmap with the name of the module as the key and its raw documentation as the value.
212    ///
213    /// # Errors
214    ///
215    /// Handlebar failed to render the variables in the module documentation.
216    #[allow(clippy::missing_panics_doc)]
217    pub fn generate(
218        self,
219        module: &Documentation,
220    ) -> Result<std::collections::HashMap<String, String>, handlebars::RenderError> {
221        let mut hbs_registry = handlebars::Handlebars::new();
222
223        hbs_registry
224            .register_template_string(
225                "mdbook-module",
226                include_str!("handlebars/mdbook/module.hbs"),
227            )
228            .expect("template is valid");
229
230        // A partial used to keep indentation for md to render correctly.
231        hbs_registry
232            .register_partial("ContentPartial", "{{{content}}}")
233            .expect("partial is valid");
234
235        generate(module, "mdbook-module", None, &hbs_registry)
236    }
237}
238
239/// Create a new builder to generate documentation for mdbook from a [`super::module::Documentation`] object.
240#[allow(clippy::missing_const_for_fn)]
241#[must_use]
242pub fn mdbook() -> MDBookOptions {
243    MDBookOptions
244}
245
246fn generate(
247    module: &Documentation,
248    template: &str,
249    slug: Option<&str>,
250    hbs_registry: &handlebars::Handlebars<'_>,
251) -> Result<std::collections::HashMap<String, String>, handlebars::RenderError> {
252    let mut documentation = std::collections::HashMap::default();
253
254    if !module.items.is_empty() {
255        let data = json!({
256            "title": module.name,
257            "slug": slug.map_or_else(|| format!("/{}", module.name), |slug| format!("{}/{}", slug, module.name)),
258            "description": module.documentation,
259            "namespace": module.namespace,
260            "items": module.items,
261        });
262
263        documentation.insert(
264            module.name.to_string(),
265            hbs_registry.render(template, &data)?,
266        );
267    }
268
269    for sub in &module.sub_modules {
270        documentation.extend(generate(sub, template, slug, hbs_registry)?);
271    }
272
273    Ok(documentation)
274}