diff --git a/Cargo.lock b/Cargo.lock index 100d636..16d810c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -808,6 +808,7 @@ dependencies = [ "clap", "console", "full_moon", + "futures-util", "lazy_static", "lune", "regex", diff --git a/docs/luneTypes.d.luau b/docs/luneTypes.d.luau index 89660a4..ff1dbf6 100644 --- a/docs/luneTypes.d.luau +++ b/docs/luneTypes.d.luau @@ -331,6 +331,7 @@ declare process: { The third argument, `options`, can be passed as a dictionary of options to give to the child process. The available options inside of the `options` dictionary are: + * `cwd` - The current working directory for the process * `env` - Extra environment variables to give to the process * `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 19e53b4..ab53a4f 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -26,6 +26,7 @@ tokio.workspace = true reqwest.workspace = true anyhow = "1.0.68" +futures-util = "0.3.26" regex = "1.7.1" serde_yaml = "0.9.17" diff --git a/packages/cli/src/gen/wiki_dir.rs b/packages/cli/src/gen/wiki_dir.rs index d2619d9..90ea97c 100644 --- a/packages/cli/src/gen/wiki_dir.rs +++ b/packages/cli/src/gen/wiki_dir.rs @@ -1,22 +1,166 @@ -use std::{fmt::Write, path::PathBuf}; +use std::{collections::HashMap, fmt::Write, path::PathBuf}; use anyhow::{Context, Result}; +use futures_util::future::try_join_all; use tokio::fs::{create_dir_all, write}; -use super::definitions::DefinitionsTree; +use super::definitions::{DefinitionsItem, DefinitionsItemKind, DefinitionsTree}; -pub const GENERATED_COMMENT_TAG: &str = "@generated with lune-cli"; +const GENERATED_COMMENT_TAG: &str = ""; +const CATEGORY_NONE: &str = "uncategorized"; pub async fn generate_from_type_definitions(contents: &str) -> Result<()> { let tree = DefinitionsTree::from_type_definitions(contents)?; + let mut dirs_to_write = Vec::new(); + let mut files_to_write = Vec::new(); // Create the wiki dir at the repo root - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + let path_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../") .canonicalize() .unwrap(); - create_dir_all(&root.join("wiki")) - .await - .context("Failed to create wiki dir")?; + let path_wiki_dir = path_root.join("wiki"); + dirs_to_write.push(path_wiki_dir.clone()); + // Sort doc items into subcategories based on globals + let mut api_reference: HashMap<&str, Vec> = HashMap::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 = top_level_item + .get_name() + .context("Missing name for top-level doc item")?; + let category = match api_reference.contains_key(category_name) { + true => api_reference.get_mut(category_name).unwrap(), + false => { + api_reference.insert(category_name, vec![]); + api_reference.get_mut(category_name).unwrap() + } + }; + category.push(top_level_item.clone()); + } + DefinitionsItemKind::Function => { + let category = match api_reference.contains_key(CATEGORY_NONE) { + true => api_reference.get_mut(CATEGORY_NONE).unwrap(), + false => { + api_reference.insert(CATEGORY_NONE, vec![]); + api_reference.get_mut(CATEGORY_NONE).unwrap() + } + }; + category.push(top_level_item.clone()); + } + _ => unimplemented!("Globals other than tables and functions are not yet implemented"), + } + } + // Generate our api reference folder + let path_api_ref = path_wiki_dir.join("api-reference"); + dirs_to_write.push(path_api_ref.clone()); + // Generate files for all subcategories + for (category_name, category_items) in api_reference { + if category_items.len() == 1 { + let item = category_items.first().unwrap(); + let path = path_api_ref.join(category_name).with_extension("md"); + let mut contents = String::new(); + write!(contents, "{GENERATED_COMMENT_TAG}\n\n")?; + generate_markdown_documentation(&mut contents, item)?; + files_to_write.push((path, post_process_docs(contents))); + } else { + let path_subcategory = path_api_ref.join(category_name); + dirs_to_write.push(path_subcategory.clone()); + for item in category_items { + let item_name = item + .get_name() + .context("Missing name for subcategory doc item")?; + let path = path_subcategory.join(item_name).with_extension("md"); + let mut contents = String::new(); + write!(contents, "{GENERATED_COMMENT_TAG}\n\n")?; + generate_markdown_documentation(&mut contents, &item)?; + files_to_write.push((path, post_process_docs(contents))); + } + } + } + // Write all dirs and files only when we know generation was successful + let futs_dirs = dirs_to_write + .drain(..) + .map(create_dir_all) + .collect::>(); + let futs_files = files_to_write + .drain(..) + .map(|(path, contents)| write(path, contents)) + .collect::>(); + try_join_all(futs_dirs).await?; + try_join_all(futs_files).await?; Ok(()) } + +fn generate_markdown_documentation(contents: &mut String, item: &DefinitionsItem) -> Result<()> { + match item.kind() { + DefinitionsItemKind::Table => { + write!( + contents, + "\n# {}\n", + item.get_name().context("Table is missing a name")? + )?; + } + DefinitionsItemKind::Property => { + write!( + contents, + "\n### `{}`\n", + item.get_name().context("Property is missing a name")? + )?; + } + DefinitionsItemKind::Function => { + write!( + contents, + "\n### `{}`\n", + item.get_name().context("Function is missing a name")? + )?; + } + DefinitionsItemKind::Description => { + write!( + contents, + "\n{}\n", + item.get_value().context("Description is missing a value")? + )?; + } + _ => {} + } + let descriptions = item + .children() + .iter() + .filter(|child| child.is_description()) + .collect::>(); + let properties = item + .children() + .iter() + .filter(|child| child.is_property()) + .collect::>(); + let functions = item + .children() + .iter() + .filter(|child| child.is_function()) + .collect::>(); + for description in descriptions { + generate_markdown_documentation(contents, description)?; + } + if !properties.is_empty() { + write!(contents, "\n\n---\n\n## Properties\n\n")?; + } + for property in properties { + generate_markdown_documentation(contents, property)?; + } + if !functions.is_empty() { + write!(contents, "\n\n---\n\n## Functions\n\n")?; + } + for function in functions { + generate_markdown_documentation(contents, function)?; + } + Ok(()) +} + +fn post_process_docs(contents: String) -> String { + contents.replace("\n\n\n", "\n\n") +}