diff --git a/CHANGELOG.md b/CHANGELOG.md index db94129..c7acb95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +## Added + +- Added global types to documentation site + ## `0.6.6` - April 30th, 2023 ### Added diff --git a/packages/cli/src/cli.rs b/packages/cli/src/cli.rs index 29d4100..d95590e 100644 --- a/packages/cli/src/cli.rs +++ b/packages/cli/src/cli.rs @@ -123,6 +123,9 @@ impl Cli { || self.generate_docs_file || self.generate_gitbook_dir; if generate_file_requested { + if self.generate_gitbook_dir { + generate_gitbook_dir_from_definitions(&TYPEDEFS_DIR).await?; + } let definitions = generate_typedefs_file_from_dir(&TYPEDEFS_DIR); if self.generate_luau_types { generate_and_save_file(FILE_NAME_LUAU_TYPES, "Luau type definitions", || { @@ -142,9 +145,6 @@ impl Cli { }) .await?; } - if self.generate_gitbook_dir { - generate_gitbook_dir_from_definitions(&definitions).await?; - } } if self.script_path.is_none() { // Only generating typedefs without running a script is completely diff --git a/packages/cli/src/gen/definitions/builder.rs b/packages/cli/src/gen/definitions/builder.rs index 7daac60..960a47b 100644 --- a/packages/cli/src/gen/definitions/builder.rs +++ b/packages/cli/src/gen/definitions/builder.rs @@ -9,6 +9,7 @@ use super::{ pub struct DefinitionsItemBuilder { exported: bool, kind: Option, + typ: Option, name: Option, meta: Option, value: Option, @@ -41,6 +42,11 @@ impl DefinitionsItemBuilder { self } + pub fn with_type(mut self, typ: String) -> Self { + self.typ = Some(typ); + self + } + pub fn with_meta>(mut self, meta: S) -> Self { self.meta = Some(meta.as_ref().to_string()); self @@ -88,10 +94,11 @@ impl DefinitionsItemBuilder { pub fn build(self) -> Result { if let Some(kind) = self.kind { let mut children = self.children; - children.sort(); + children.sort_by(|left, right| left.name.cmp(&right.name)); Ok(DefinitionsItem { exported: self.exported, kind, + typ: self.typ, name: self.name, meta: self.meta, value: self.value, @@ -104,3 +111,19 @@ impl DefinitionsItemBuilder { } } } + +impl From<&DefinitionsItem> for DefinitionsItemBuilder { + fn from(value: &DefinitionsItem) -> Self { + Self { + exported: value.exported, + kind: Some(value.kind), + typ: value.typ.clone(), + name: value.name.clone(), + meta: value.meta.clone(), + value: value.value.clone(), + children: value.children.clone(), + args: value.args.clone(), + rets: value.rets.clone(), + } + } +} diff --git a/packages/cli/src/gen/definitions/item.rs b/packages/cli/src/gen/definitions/item.rs index d611ff7..b8f819b 100644 --- a/packages/cli/src/gen/definitions/item.rs +++ b/packages/cli/src/gen/definitions/item.rs @@ -45,12 +45,13 @@ impl DefinitionsItemFunctionRet { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DefinitionsItem { #[serde(skip_serializing_if = "skip_serialize_is_false")] pub(super) exported: bool, pub(super) kind: DefinitionsItemKind, + pub(super) typ: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(super) name: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -95,12 +96,6 @@ impl PartialOrd for DefinitionsItem { } } -impl Ord for DefinitionsItem { - fn cmp(&self, other: &Self) -> Ordering { - self.partial_cmp(other).unwrap() - } -} - #[allow(dead_code)] impl DefinitionsItem { pub fn is_exported(&self) -> bool { @@ -111,6 +106,10 @@ impl DefinitionsItem { self.kind.is_root() } + pub fn is_type(&self) -> bool { + self.kind.is_type() + } + pub fn is_table(&self) -> bool { self.kind.is_table() } @@ -139,6 +138,10 @@ impl DefinitionsItem { self.name.as_deref() } + pub fn get_type(&self) -> Option { + self.typ.clone() + } + pub fn get_meta(&self) -> Option<&str> { self.meta.as_deref() } diff --git a/packages/cli/src/gen/definitions/kind.rs b/packages/cli/src/gen/definitions/kind.rs index 3bc2015..08e4457 100644 --- a/packages/cli/src/gen/definitions/kind.rs +++ b/packages/cli/src/gen/definitions/kind.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "PascalCase")] pub enum DefinitionsItemKind { Root, + Type, Table, Property, Function, @@ -19,6 +20,10 @@ impl DefinitionsItemKind { self == DefinitionsItemKind::Root } + pub fn is_type(self) -> bool { + self == DefinitionsItemKind::Type + } + pub fn is_table(self) -> bool { self == DefinitionsItemKind::Table } @@ -47,6 +52,7 @@ impl fmt::Display for DefinitionsItemKind { "{}", match self { Self::Root => "Root", + Self::Type => "Type", Self::Table => "Table", Self::Property => "Property", Self::Function => "Function", diff --git a/packages/cli/src/gen/definitions/parser.rs b/packages/cli/src/gen/definitions/parser.rs index 93724c2..4c463ab 100644 --- a/packages/cli/src/gen/definitions/parser.rs +++ b/packages/cli/src/gen/definitions/parser.rs @@ -12,7 +12,7 @@ use regex::Regex; use super::{ builder::DefinitionsItemBuilder, item::DefinitionsItem, moonwave::parse_moonwave_style_comment, - type_info_ext::TypeInfoExt, + type_info_ext::TypeInfoExt, DefinitionsItemKind, }; #[derive(Debug, Clone)] @@ -26,6 +26,7 @@ struct DefinitionsParserItem { pub struct DefinitionsParser { found_top_level_items: BTreeMap, found_top_level_types: HashMap, + found_top_level_comments: HashMap>, found_top_level_declares: Vec, } @@ -34,6 +35,7 @@ impl DefinitionsParser { Self { found_top_level_items: BTreeMap::new(), found_top_level_types: HashMap::new(), + found_top_level_comments: HashMap::new(), found_top_level_declares: Vec::new(), } } @@ -69,6 +71,7 @@ impl DefinitionsParser { // Parse contents into top-level parser items for later use let mut found_top_level_items = BTreeMap::new(); let mut found_top_level_types = HashMap::new(); + let mut found_top_level_comments = HashMap::new(); let ast = full_moon::parse(&resulting_contents).context("Failed to parse type definitions")?; for stmt in ast.nodes().stmts() { @@ -80,28 +83,36 @@ impl DefinitionsParser { _ => None, } { let name = declaration.type_name().token().to_string(); + let comment = find_token_moonwave_comment(token_reference); found_top_level_items.insert( name.clone(), DefinitionsParserItem { name: name.clone(), - comment: find_token_moonwave_comment(token_reference), + comment: comment.clone(), type_info: declaration.type_definition().clone(), }, ); - found_top_level_types.insert(name, declaration.type_definition().clone()); + found_top_level_types.insert(name.clone(), declaration.type_definition().clone()); + found_top_level_comments.insert(name, comment); } } // Store results self.found_top_level_items = found_top_level_items; self.found_top_level_types = found_top_level_types; + self.found_top_level_comments = found_top_level_comments; self.found_top_level_declares = found_declares; Ok(()) } - fn convert_parser_item_into_doc_item(&self, item: DefinitionsParserItem) -> DefinitionsItem { + fn convert_parser_item_into_doc_item( + &self, + item: DefinitionsParserItem, + kind: Option, + ) -> DefinitionsItem { let mut builder = DefinitionsItemBuilder::new() - .with_kind(item.type_info.parse_definitions_kind()) - .with_name(&item.name); + .with_kind(kind.unwrap_or_else(|| item.type_info.parse_definitions_kind())) + .with_name(&item.name) + .with_type(item.type_info.to_string()); if self.found_top_level_declares.contains(&item.name) { builder = builder.as_exported(); } @@ -123,6 +134,7 @@ impl DefinitionsParser { comment: find_token_moonwave_comment(name), type_info: field.value().clone(), }, + None, )); } } @@ -137,14 +149,16 @@ impl DefinitionsParser { */ #[allow(clippy::unnecessary_wraps)] pub fn drain(&mut self) -> Result> { - let mut results = Vec::new(); + let mut resulting_items = Vec::new(); for top_level_item in self.found_top_level_items.values() { - results.push(self.convert_parser_item_into_doc_item(top_level_item.clone())); + resulting_items + .push(self.convert_parser_item_into_doc_item(top_level_item.clone(), None)); } self.found_top_level_items = BTreeMap::new(); self.found_top_level_types = HashMap::new(); + self.found_top_level_comments = HashMap::new(); self.found_top_level_declares = Vec::new(); - Ok(results) + Ok(resulting_items) } } diff --git a/packages/cli/src/gen/definitions/tree.rs b/packages/cli/src/gen/definitions/tree.rs index c55693b..6df44f5 100644 --- a/packages/cli/src/gen/definitions/tree.rs +++ b/packages/cli/src/gen/definitions/tree.rs @@ -8,7 +8,7 @@ use super::{ parser::DefinitionsParser, }; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DefinitionsTree(DefinitionsItem); #[allow(dead_code)] diff --git a/packages/cli/src/gen/gitbook_dir.rs b/packages/cli/src/gen/gitbook_dir.rs index 4e4dc48..2304d1a 100644 --- a/packages/cli/src/gen/gitbook_dir.rs +++ b/packages/cli/src/gen/gitbook_dir.rs @@ -18,8 +18,8 @@ All globals that are not available under a specific scope. These are to be used directly without indexing a global table first. "; -pub async fn generate_from_type_definitions(contents: &str) -> Result<()> { - let tree = DefinitionsTree::from_type_definitions(contents)?; +#[allow(clippy::too_many_lines)] +pub async fn generate_from_type_definitions(contents: HashMap) -> Result<()> { let mut dirs_to_write = Vec::new(); let mut files_to_write = Vec::new(); // Create the gitbook dir at the repo root @@ -37,22 +37,42 @@ pub async fn generate_from_type_definitions(contents: &str) -> Result<()> { dirs_to_write.push(path_gitbook_api_dir.clone()); // Sort doc items into subcategories based on globals let mut api_reference = HashMap::new(); - let mut no_category = Vec::new(); - for top_level_item in tree - .children() - .iter() - .filter(|top_level| top_level.is_exported()) - { - match top_level_item.kind() { - DefinitionsItemKind::Table => { - let category_name = - get_name(top_level_item).context("Missing name for top-level doc item")?; - api_reference.insert(category_name, top_level_item.clone()); + let mut without_main_item = Vec::new(); + for (typedef_name, typedef_contents) in contents { + let tree = DefinitionsTree::from_type_definitions(typedef_contents)?; + let main = tree.children().iter().find( + |c| matches!(c.get_name(), Some(s) if s.to_lowercase() == typedef_name.to_lowercase()), + ); + if let Some(main) = main { + let children = tree + .children() + .iter() + .filter_map(|child| { + if child == main { + None + } else { + Some( + DefinitionsItemBuilder::from(child) + .with_kind(DefinitionsItemKind::Type) + .build() + .unwrap(), + ) + } + }) + .collect::>(); + let root = DefinitionsItemBuilder::new() + .with_kind(main.kind()) + .with_name(main.get_name().unwrap()) + .with_children(main.children()) + .with_children(&children); + api_reference.insert( + typedef_name.clone(), + root.build().expect("Failed to build root definitions item"), + ); + } else { + for top_level_item in tree.children() { + without_main_item.push(top_level_item.clone()); } - DefinitionsItemKind::Function => { - no_category.push(top_level_item.clone()); - } - _ => unimplemented!("Globals other than tables and functions are not yet implemented"), } } // Insert globals with no category into a new "Uncategorized" global @@ -61,7 +81,7 @@ pub async fn generate_from_type_definitions(contents: &str) -> Result<()> { DefinitionsItemBuilder::new() .with_kind(DefinitionsItemKind::Table) .with_name("Uncategorized") - .with_children(&no_category) + .with_children(&without_main_item) .with_child( DefinitionsItemBuilder::new() .with_kind(DefinitionsItemKind::Description) @@ -78,7 +98,7 @@ pub async fn generate_from_type_definitions(contents: &str) -> Result<()> { .with_extension("md"); let mut contents = String::new(); write!(contents, "{GENERATED_COMMENT_TAG}\n\n")?; - generate_markdown_documentation(&mut contents, &category_item, 0)?; + generate_markdown_documentation(&mut contents, &category_item, None, 0)?; files_to_write.push((path, post_process_docs(contents))); } // Write all dirs and files only when we know generation was successful @@ -113,13 +133,16 @@ fn get_name(item: &DefinitionsItem) -> Result { .context("Definitions item is missing a name") } +#[allow(clippy::too_many_lines)] fn generate_markdown_documentation( contents: &mut String, item: &DefinitionsItem, + parent: Option<&DefinitionsItem>, depth: usize, ) -> Result<()> { match item.kind() { - DefinitionsItemKind::Table + DefinitionsItemKind::Type + | DefinitionsItemKind::Table | DefinitionsItemKind::Property | DefinitionsItemKind::Function => { write!( @@ -146,17 +169,31 @@ fn generate_markdown_documentation( } _ => {} } - if item.kind().is_function() && !item.args().is_empty() { + if item.is_function() && !item.args().is_empty() { let args = item .args() .iter() - .map(|arg| format!("{}: {}", arg.name, arg.typedef)) - .collect::>(); + .map(|arg| format!("{}: {}", arg.name.trim(), arg.typedef.trim())) + .collect::>() + .join(", ") + .replace("_: T...", "T..."); + let func_name = item.get_name().unwrap_or("_"); + let parent_name = parent.unwrap().get_name().unwrap_or("_"); + let parent_pre = if parent_name.to_lowercase() == "uncategorized" { + String::new() + } else { + format!("{parent_name}.") + }; write!( contents, - "\n```lua\nfunction {}({})\n```\n", + "\n```lua\nfunction {parent_pre}{func_name}({args})\n```\n", + )?; + } else if item.is_type() { + write!( + contents, + "\n```lua\ntype {} = {}\n```\n", item.get_name().unwrap_or("_"), - args.join(", ") + item.get_type().unwrap_or_else(|| "{}".to_string()).trim() )?; } let descriptions = item @@ -174,24 +211,50 @@ fn generate_markdown_documentation( .iter() .filter(|child| child.is_function()) .collect::>(); + let types = item + .children() + .iter() + .filter(|child| child.is_type()) + .collect::>(); for description in descriptions { - generate_markdown_documentation(contents, description, depth + 1)?; + generate_markdown_documentation(contents, description, Some(item), depth + 1)?; } - if !properties.is_empty() { - write!(contents, "\n\n---\n\n## Properties\n\n")?; - } - for property in properties { - generate_markdown_documentation(contents, property, depth + 1)?; - } - if !functions.is_empty() { - write!(contents, "\n\n---\n\n## Functions\n\n")?; - } - for function in functions { - generate_markdown_documentation(contents, function, depth + 1)?; + if !item.is_type() { + if !properties.is_empty() { + write!(contents, "\n\n---\n\n## Properties\n\n")?; + } + for property in properties { + generate_markdown_documentation(contents, property, Some(item), depth + 1)?; + } + if !functions.is_empty() { + write!(contents, "\n\n---\n\n## Functions\n\n")?; + } + for function in functions { + generate_markdown_documentation(contents, function, Some(item), depth + 1)?; + } + if !types.is_empty() { + write!(contents, "\n\n---\n\n## Types\n\n")?; + } + for typ in types { + generate_markdown_documentation(contents, typ, Some(item), depth + 1)?; + } } Ok(()) } fn post_process_docs(contents: String) -> String { - contents.replace("\n\n\n", "\n\n") + let no_empty_lines = contents + .lines() + .map(|line| { + if line.chars().all(char::is_whitespace) { + "" + } else { + line + } + }) + .collect::>() + .join("\n"); + no_empty_lines + .replace("\n\n\n", "\n\n") + .replace("\n\n\n", "\n\n") } diff --git a/packages/cli/src/gen/mod.rs b/packages/cli/src/gen/mod.rs index e3cf9a9..48cc7e5 100644 --- a/packages/cli/src/gen/mod.rs +++ b/packages/cli/src/gen/mod.rs @@ -1,3 +1,6 @@ +use std::collections::HashMap; + +use anyhow::Result; use include_dir::Dir; use regex::Regex; @@ -9,10 +12,36 @@ mod selene_defs; pub mod definitions; pub use docs_file::generate_from_type_definitions as generate_docs_json_from_definitions; -pub use gitbook_dir::generate_from_type_definitions as generate_gitbook_dir_from_definitions; pub use luau_defs::generate_from_type_definitions as generate_luau_defs_from_definitions; pub use selene_defs::generate_from_type_definitions as generate_selene_defs_from_definitions; +pub async fn generate_gitbook_dir_from_definitions(dir: &Dir<'_>) -> Result<()> { + let mut result = HashMap::new(); + + for entry in dir.find("*.luau").unwrap() { + let entry_file = entry.as_file().unwrap(); + let entry_name = entry_file.path().file_name().unwrap().to_string_lossy(); + + let typedef_name = entry_name.trim_end_matches(".luau"); + let typedef_contents = entry_file + .contents_utf8() + .unwrap() + .to_string() + .replace( + &format!("export type {typedef_name} = "), + &format!("declare {}: ", typedef_name.to_ascii_lowercase()), + ) + .replace("export type ", "type "); + + result.insert(typedef_name.to_string(), typedef_contents); + } + + match gitbook_dir::generate_from_type_definitions(result).await { + Ok(_) => Ok(()), + Err(e) => Err(e), + } +} + pub fn generate_typedefs_file_from_dir(dir: &Dir<'_>) -> String { let mut result = String::new();