diff --git a/CHANGELOG.md b/CHANGELOG.md index a9a6e926..14b5860e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ New features * A new feature, `no_custom_syntax`, is added to remove custom syntax support from Rhai for applications that do not require it (which should be most). * Comment lines beginning with `//!` (requires the `metadata` feature) are now collected as the script file's _module documentation_. +* `AST` and `Module` have methods to access and manipulate documentation. Enhancements ------------ diff --git a/src/ast/ast.rs b/src/ast/ast.rs index 0c201593..0d21651f 100644 --- a/src/ast/ast.rs +++ b/src/ast/ast.rs @@ -195,6 +195,7 @@ impl AST { /// Leading white-spaces are stripped, and each line always starts with `//!`. #[cfg(feature = "metadata")] #[inline(always)] + #[must_use] pub fn doc(&self) -> &str { &self.doc } @@ -211,6 +212,7 @@ impl AST { /// Only available under `metadata`. #[cfg(feature = "metadata")] #[inline(always)] + #[must_use] pub(crate) fn doc_mut(&mut self) -> &mut SmartString { &mut self.doc } diff --git a/src/module/mod.rs b/src/module/mod.rs index 38bb887a..2138995f 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -250,6 +250,9 @@ pub struct Module { /// ID identifying the module. /// No ID if string is empty. id: Identifier, + /// Module documentation. + #[cfg(feature = "metadata")] + doc: crate::SmartString, /// Is this module internal? pub(crate) internal: bool, /// Is this module part of a standard library? @@ -290,31 +293,27 @@ impl fmt::Debug for Module { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut d = f.debug_struct("Module"); - if !self.id.is_empty() { - d.field("id", &self.id); - } - if !self.modules.is_empty() { - d.field( + d.field("id", &self.id) + .field( "modules", &self .modules .keys() .map(|m| m.as_str()) .collect::>(), - ); - } - if !self.variables.is_empty() { - d.field("vars", &self.variables); - } - if !self.functions.is_empty() { - d.field( + ) + .field("vars", &self.variables) + .field( "functions", &self .iter_fn() .map(|f| f.func.to_string()) .collect::>(), ); - } + + #[cfg(feature = "metadata")] + d.field("doc", &self.doc); + d.finish() } } @@ -363,6 +362,8 @@ impl Module { pub fn new() -> Self { Self { id: Identifier::new_const(), + #[cfg(feature = "metadata")] + doc: crate::SmartString::new_const(), internal: false, standard: false, custom_types: CustomTypesCollection::new(), @@ -423,6 +424,7 @@ impl Module { self.id = id.into(); self } + /// Clear the ID of the [`Module`]. /// /// # Example @@ -441,10 +443,68 @@ impl Module { self } + /// Get the documentation of the [`Module`], if any. + /// Exported under the `metadata` feature only. + /// + /// # Example + /// + /// ``` + /// # use rhai::Module; + /// let mut module = Module::new(); + /// module.set_doc("//! This is my special module."); + /// assert_eq!(module.doc(), "//! This is my special module."); + /// ``` + #[cfg(feature = "metadata")] + #[inline] + #[must_use] + pub fn doc(&self) -> &str { + &self.doc + } + + /// Set the documentation of the [`Module`]. + /// Exported under the `metadata` feature only. + /// + /// If the string is empty, it is equivalent to clearing the documentation. + /// + /// # Example + /// + /// ``` + /// # use rhai::Module; + /// let mut module = Module::new(); + /// module.set_doc("//! This is my special module."); + /// assert_eq!(module.doc(), "//! This is my special module."); + /// ``` + #[cfg(feature = "metadata")] + #[inline(always)] + pub fn set_doc(&mut self, doc: impl Into) -> &mut Self { + self.doc = doc.into(); + self + } + + /// Clear the documentation of the [`Module`]. + /// + /// # Example + /// + /// ``` + /// # use rhai::Module; + /// let mut module = Module::new(); + /// module.set_doc("//! This is my special module."); + /// assert_eq!(module.doc(), "//! This is my special module."); + /// module.clear_id(); + /// assert_eq!(module.doc(), ""); + /// ``` + #[cfg(feature = "metadata")] + #[inline(always)] + pub fn clear_doc(&mut self) -> &mut Self { + self.doc.clear(); + self + } + /// Clear the [`Module`]. #[inline(always)] pub fn clear(&mut self) { self.id.clear(); + self.doc.clear(); self.internal = false; self.standard = false; self.custom_types.clear(); @@ -1563,6 +1623,15 @@ impl Module { self.all_type_iterators.clear(); self.indexed = false; self.contains_indexed_global_functions = false; + + #[cfg(feature = "metadata")] + if !other.doc.is_empty() { + if !self.doc.is_empty() { + self.doc.push('\n'); + } + self.doc.push_str(&other.doc); + } + self } @@ -1584,6 +1653,15 @@ impl Module { self.all_type_iterators.clear(); self.indexed = false; self.contains_indexed_global_functions = false; + + #[cfg(feature = "metadata")] + if !other.doc.is_empty() { + if !self.doc.is_empty() { + self.doc.push('\n'); + } + self.doc.push_str(&other.doc); + } + self } @@ -1614,6 +1692,15 @@ impl Module { self.all_type_iterators.clear(); self.indexed = false; self.contains_indexed_global_functions = false; + + #[cfg(feature = "metadata")] + if !other.doc.is_empty() { + if !self.doc.is_empty() { + self.doc.push('\n'); + } + self.doc.push_str(&other.doc); + } + self } @@ -1667,6 +1754,15 @@ impl Module { self.all_type_iterators.clear(); self.indexed = false; self.contains_indexed_global_functions = false; + + #[cfg(feature = "metadata")] + if !other.doc.is_empty() { + if !self.doc.is_empty() { + self.doc.push('\n'); + } + self.doc.push_str(&other.doc); + } + self } @@ -1978,6 +2074,9 @@ impl Module { module.set_id(ast.source_raw().clone()); + #[cfg(feature = "metadata")] + module.set_doc(ast.doc()); + module.build_index(); Ok(module) diff --git a/src/serde/metadata.rs b/src/serde/metadata.rs index f430e484..4c8cc501 100644 --- a/src/serde/metadata.rs +++ b/src/serde/metadata.rs @@ -3,7 +3,7 @@ #![cfg(feature = "metadata")] use crate::module::{calc_native_fn_hash, FuncInfo}; -use crate::{calc_fn_hash, Engine, AST}; +use crate::{calc_fn_hash, Engine, SmartString, AST}; use serde::{Deserialize, Serialize}; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -158,6 +158,9 @@ impl<'a> From<&'a FuncInfo> for FnMetadata<'a> { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] struct ModuleMetadata<'a> { + #[cfg(feature = "metadata")] + #[serde(skip_serializing_if = "SmartString::is_empty")] + pub doc: SmartString, #[serde(skip_serializing_if = "BTreeMap::is_empty")] pub modules: BTreeMap<&'a str, Self>, #[serde(skip_serializing_if = "Vec::is_empty")] @@ -168,6 +171,8 @@ impl ModuleMetadata<'_> { #[inline(always)] pub fn new() -> Self { Self { + #[cfg(feature = "metadata")] + doc: SmartString::new_const(), modules: BTreeMap::new(), functions: Vec::new(), } @@ -180,6 +185,7 @@ impl<'a> From<&'a crate::Module> for ModuleMetadata<'a> { functions.sort(); Self { + doc: module.doc().into(), modules: module .iter_sub_modules() .map(|(name, m)| (name, m.as_ref().into())) @@ -240,6 +246,11 @@ impl Engine { global.functions.sort(); + #[cfg(feature = "metadata")] + { + global.doc = ast.doc().into(); + } + serde_json::to_string_pretty(&global) }