Added global types to documentation site

This commit is contained in:
Filip Tibell 2023-05-01 12:18:15 +02:00
parent 1559e69de6
commit d4d9108947
No known key found for this signature in database
9 changed files with 204 additions and 60 deletions

View file

@ -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/), 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). 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 ## `0.6.6` - April 30th, 2023
### Added ### Added

View file

@ -123,6 +123,9 @@ impl Cli {
|| self.generate_docs_file || self.generate_docs_file
|| self.generate_gitbook_dir; || self.generate_gitbook_dir;
if generate_file_requested { 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); let definitions = generate_typedefs_file_from_dir(&TYPEDEFS_DIR);
if self.generate_luau_types { if self.generate_luau_types {
generate_and_save_file(FILE_NAME_LUAU_TYPES, "Luau type definitions", || { generate_and_save_file(FILE_NAME_LUAU_TYPES, "Luau type definitions", || {
@ -142,9 +145,6 @@ impl Cli {
}) })
.await?; .await?;
} }
if self.generate_gitbook_dir {
generate_gitbook_dir_from_definitions(&definitions).await?;
}
} }
if self.script_path.is_none() { if self.script_path.is_none() {
// Only generating typedefs without running a script is completely // Only generating typedefs without running a script is completely

View file

@ -9,6 +9,7 @@ use super::{
pub struct DefinitionsItemBuilder { pub struct DefinitionsItemBuilder {
exported: bool, exported: bool,
kind: Option<DefinitionsItemKind>, kind: Option<DefinitionsItemKind>,
typ: Option<String>,
name: Option<String>, name: Option<String>,
meta: Option<String>, meta: Option<String>,
value: Option<String>, value: Option<String>,
@ -41,6 +42,11 @@ impl DefinitionsItemBuilder {
self self
} }
pub fn with_type(mut self, typ: String) -> Self {
self.typ = Some(typ);
self
}
pub fn with_meta<S: AsRef<str>>(mut self, meta: S) -> Self { pub fn with_meta<S: AsRef<str>>(mut self, meta: S) -> Self {
self.meta = Some(meta.as_ref().to_string()); self.meta = Some(meta.as_ref().to_string());
self self
@ -88,10 +94,11 @@ impl DefinitionsItemBuilder {
pub fn build(self) -> Result<DefinitionsItem> { pub fn build(self) -> Result<DefinitionsItem> {
if let Some(kind) = self.kind { if let Some(kind) = self.kind {
let mut children = self.children; let mut children = self.children;
children.sort(); children.sort_by(|left, right| left.name.cmp(&right.name));
Ok(DefinitionsItem { Ok(DefinitionsItem {
exported: self.exported, exported: self.exported,
kind, kind,
typ: self.typ,
name: self.name, name: self.name,
meta: self.meta, meta: self.meta,
value: self.value, 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(),
}
}
}

View file

@ -45,12 +45,13 @@ impl DefinitionsItemFunctionRet {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DefinitionsItem { pub struct DefinitionsItem {
#[serde(skip_serializing_if = "skip_serialize_is_false")] #[serde(skip_serializing_if = "skip_serialize_is_false")]
pub(super) exported: bool, pub(super) exported: bool,
pub(super) kind: DefinitionsItemKind, pub(super) kind: DefinitionsItemKind,
pub(super) typ: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub(super) name: Option<String>, pub(super) name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[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)] #[allow(dead_code)]
impl DefinitionsItem { impl DefinitionsItem {
pub fn is_exported(&self) -> bool { pub fn is_exported(&self) -> bool {
@ -111,6 +106,10 @@ impl DefinitionsItem {
self.kind.is_root() self.kind.is_root()
} }
pub fn is_type(&self) -> bool {
self.kind.is_type()
}
pub fn is_table(&self) -> bool { pub fn is_table(&self) -> bool {
self.kind.is_table() self.kind.is_table()
} }
@ -139,6 +138,10 @@ impl DefinitionsItem {
self.name.as_deref() self.name.as_deref()
} }
pub fn get_type(&self) -> Option<String> {
self.typ.clone()
}
pub fn get_meta(&self) -> Option<&str> { pub fn get_meta(&self) -> Option<&str> {
self.meta.as_deref() self.meta.as_deref()
} }

View file

@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub enum DefinitionsItemKind { pub enum DefinitionsItemKind {
Root, Root,
Type,
Table, Table,
Property, Property,
Function, Function,
@ -19,6 +20,10 @@ impl DefinitionsItemKind {
self == DefinitionsItemKind::Root self == DefinitionsItemKind::Root
} }
pub fn is_type(self) -> bool {
self == DefinitionsItemKind::Type
}
pub fn is_table(self) -> bool { pub fn is_table(self) -> bool {
self == DefinitionsItemKind::Table self == DefinitionsItemKind::Table
} }
@ -47,6 +52,7 @@ impl fmt::Display for DefinitionsItemKind {
"{}", "{}",
match self { match self {
Self::Root => "Root", Self::Root => "Root",
Self::Type => "Type",
Self::Table => "Table", Self::Table => "Table",
Self::Property => "Property", Self::Property => "Property",
Self::Function => "Function", Self::Function => "Function",

View file

@ -12,7 +12,7 @@ use regex::Regex;
use super::{ use super::{
builder::DefinitionsItemBuilder, item::DefinitionsItem, moonwave::parse_moonwave_style_comment, builder::DefinitionsItemBuilder, item::DefinitionsItem, moonwave::parse_moonwave_style_comment,
type_info_ext::TypeInfoExt, type_info_ext::TypeInfoExt, DefinitionsItemKind,
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -26,6 +26,7 @@ struct DefinitionsParserItem {
pub struct DefinitionsParser { pub struct DefinitionsParser {
found_top_level_items: BTreeMap<String, DefinitionsParserItem>, found_top_level_items: BTreeMap<String, DefinitionsParserItem>,
found_top_level_types: HashMap<String, TypeInfo>, found_top_level_types: HashMap<String, TypeInfo>,
found_top_level_comments: HashMap<String, Option<String>>,
found_top_level_declares: Vec<String>, found_top_level_declares: Vec<String>,
} }
@ -34,6 +35,7 @@ impl DefinitionsParser {
Self { Self {
found_top_level_items: BTreeMap::new(), found_top_level_items: BTreeMap::new(),
found_top_level_types: HashMap::new(), found_top_level_types: HashMap::new(),
found_top_level_comments: HashMap::new(),
found_top_level_declares: Vec::new(), found_top_level_declares: Vec::new(),
} }
} }
@ -69,6 +71,7 @@ impl DefinitionsParser {
// Parse contents into top-level parser items for later use // Parse contents into top-level parser items for later use
let mut found_top_level_items = BTreeMap::new(); let mut found_top_level_items = BTreeMap::new();
let mut found_top_level_types = HashMap::new(); let mut found_top_level_types = HashMap::new();
let mut found_top_level_comments = HashMap::new();
let ast = let ast =
full_moon::parse(&resulting_contents).context("Failed to parse type definitions")?; full_moon::parse(&resulting_contents).context("Failed to parse type definitions")?;
for stmt in ast.nodes().stmts() { for stmt in ast.nodes().stmts() {
@ -80,28 +83,36 @@ impl DefinitionsParser {
_ => None, _ => None,
} { } {
let name = declaration.type_name().token().to_string(); let name = declaration.type_name().token().to_string();
let comment = find_token_moonwave_comment(token_reference);
found_top_level_items.insert( found_top_level_items.insert(
name.clone(), name.clone(),
DefinitionsParserItem { DefinitionsParserItem {
name: name.clone(), name: name.clone(),
comment: find_token_moonwave_comment(token_reference), comment: comment.clone(),
type_info: declaration.type_definition().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 // Store results
self.found_top_level_items = found_top_level_items; self.found_top_level_items = found_top_level_items;
self.found_top_level_types = found_top_level_types; 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; self.found_top_level_declares = found_declares;
Ok(()) Ok(())
} }
fn convert_parser_item_into_doc_item(&self, item: DefinitionsParserItem) -> DefinitionsItem { fn convert_parser_item_into_doc_item(
&self,
item: DefinitionsParserItem,
kind: Option<DefinitionsItemKind>,
) -> DefinitionsItem {
let mut builder = DefinitionsItemBuilder::new() let mut builder = DefinitionsItemBuilder::new()
.with_kind(item.type_info.parse_definitions_kind()) .with_kind(kind.unwrap_or_else(|| item.type_info.parse_definitions_kind()))
.with_name(&item.name); .with_name(&item.name)
.with_type(item.type_info.to_string());
if self.found_top_level_declares.contains(&item.name) { if self.found_top_level_declares.contains(&item.name) {
builder = builder.as_exported(); builder = builder.as_exported();
} }
@ -123,6 +134,7 @@ impl DefinitionsParser {
comment: find_token_moonwave_comment(name), comment: find_token_moonwave_comment(name),
type_info: field.value().clone(), type_info: field.value().clone(),
}, },
None,
)); ));
} }
} }
@ -137,14 +149,16 @@ impl DefinitionsParser {
*/ */
#[allow(clippy::unnecessary_wraps)] #[allow(clippy::unnecessary_wraps)]
pub fn drain(&mut self) -> Result<Vec<DefinitionsItem>> { pub fn drain(&mut self) -> Result<Vec<DefinitionsItem>> {
let mut results = Vec::new(); let mut resulting_items = Vec::new();
for top_level_item in self.found_top_level_items.values() { 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_items = BTreeMap::new();
self.found_top_level_types = HashMap::new(); self.found_top_level_types = HashMap::new();
self.found_top_level_comments = HashMap::new();
self.found_top_level_declares = Vec::new(); self.found_top_level_declares = Vec::new();
Ok(results) Ok(resulting_items)
} }
} }

View file

@ -8,7 +8,7 @@ use super::{
parser::DefinitionsParser, parser::DefinitionsParser,
}; };
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DefinitionsTree(DefinitionsItem); pub struct DefinitionsTree(DefinitionsItem);
#[allow(dead_code)] #[allow(dead_code)]

View file

@ -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. These are to be used directly without indexing a global table first.
"; ";
pub async fn generate_from_type_definitions(contents: &str) -> Result<()> { #[allow(clippy::too_many_lines)]
let tree = DefinitionsTree::from_type_definitions(contents)?; pub async fn generate_from_type_definitions(contents: HashMap<String, String>) -> Result<()> {
let mut dirs_to_write = Vec::new(); let mut dirs_to_write = Vec::new();
let mut files_to_write = Vec::new(); let mut files_to_write = Vec::new();
// Create the gitbook dir at the repo root // 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()); dirs_to_write.push(path_gitbook_api_dir.clone());
// Sort doc items into subcategories based on globals // Sort doc items into subcategories based on globals
let mut api_reference = HashMap::new(); let mut api_reference = HashMap::new();
let mut no_category = Vec::new(); let mut without_main_item = Vec::new();
for top_level_item in tree 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() .children()
.iter() .iter()
.filter(|top_level| top_level.is_exported()) .filter_map(|child| {
{ if child == main {
match top_level_item.kind() { None
DefinitionsItemKind::Table => { } else {
let category_name = Some(
get_name(top_level_item).context("Missing name for top-level doc item")?; DefinitionsItemBuilder::from(child)
api_reference.insert(category_name, top_level_item.clone()); .with_kind(DefinitionsItemKind::Type)
.build()
.unwrap(),
)
} }
DefinitionsItemKind::Function => { })
no_category.push(top_level_item.clone()); .collect::<Vec<_>>();
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());
} }
_ => unimplemented!("Globals other than tables and functions are not yet implemented"),
} }
} }
// Insert globals with no category into a new "Uncategorized" global // 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() DefinitionsItemBuilder::new()
.with_kind(DefinitionsItemKind::Table) .with_kind(DefinitionsItemKind::Table)
.with_name("Uncategorized") .with_name("Uncategorized")
.with_children(&no_category) .with_children(&without_main_item)
.with_child( .with_child(
DefinitionsItemBuilder::new() DefinitionsItemBuilder::new()
.with_kind(DefinitionsItemKind::Description) .with_kind(DefinitionsItemKind::Description)
@ -78,7 +98,7 @@ pub async fn generate_from_type_definitions(contents: &str) -> Result<()> {
.with_extension("md"); .with_extension("md");
let mut contents = String::new(); let mut contents = String::new();
write!(contents, "{GENERATED_COMMENT_TAG}\n\n")?; 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))); files_to_write.push((path, post_process_docs(contents)));
} }
// Write all dirs and files only when we know generation was successful // Write all dirs and files only when we know generation was successful
@ -113,13 +133,16 @@ fn get_name(item: &DefinitionsItem) -> Result<String> {
.context("Definitions item is missing a name") .context("Definitions item is missing a name")
} }
#[allow(clippy::too_many_lines)]
fn generate_markdown_documentation( fn generate_markdown_documentation(
contents: &mut String, contents: &mut String,
item: &DefinitionsItem, item: &DefinitionsItem,
parent: Option<&DefinitionsItem>,
depth: usize, depth: usize,
) -> Result<()> { ) -> Result<()> {
match item.kind() { match item.kind() {
DefinitionsItemKind::Table DefinitionsItemKind::Type
| DefinitionsItemKind::Table
| DefinitionsItemKind::Property | DefinitionsItemKind::Property
| DefinitionsItemKind::Function => { | DefinitionsItemKind::Function => {
write!( 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 let args = item
.args() .args()
.iter() .iter()
.map(|arg| format!("{}: {}", arg.name, arg.typedef)) .map(|arg| format!("{}: {}", arg.name.trim(), arg.typedef.trim()))
.collect::<Vec<_>>(); .collect::<Vec<_>>()
.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!( write!(
contents, 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("_"), item.get_name().unwrap_or("_"),
args.join(", ") item.get_type().unwrap_or_else(|| "{}".to_string()).trim()
)?; )?;
} }
let descriptions = item let descriptions = item
@ -174,24 +211,50 @@ fn generate_markdown_documentation(
.iter() .iter()
.filter(|child| child.is_function()) .filter(|child| child.is_function())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let types = item
.children()
.iter()
.filter(|child| child.is_type())
.collect::<Vec<_>>();
for description in descriptions { for description in descriptions {
generate_markdown_documentation(contents, description, depth + 1)?; generate_markdown_documentation(contents, description, Some(item), depth + 1)?;
} }
if !item.is_type() {
if !properties.is_empty() { if !properties.is_empty() {
write!(contents, "\n\n---\n\n## Properties\n\n")?; write!(contents, "\n\n---\n\n## Properties\n\n")?;
} }
for property in properties { for property in properties {
generate_markdown_documentation(contents, property, depth + 1)?; generate_markdown_documentation(contents, property, Some(item), depth + 1)?;
} }
if !functions.is_empty() { if !functions.is_empty() {
write!(contents, "\n\n---\n\n## Functions\n\n")?; write!(contents, "\n\n---\n\n## Functions\n\n")?;
} }
for function in functions { for function in functions {
generate_markdown_documentation(contents, function, depth + 1)?; 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(()) Ok(())
} }
fn post_process_docs(contents: String) -> String { 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::<Vec<_>>()
.join("\n");
no_empty_lines
.replace("\n\n\n", "\n\n")
.replace("\n\n\n", "\n\n")
} }

View file

@ -1,3 +1,6 @@
use std::collections::HashMap;
use anyhow::Result;
use include_dir::Dir; use include_dir::Dir;
use regex::Regex; use regex::Regex;
@ -9,10 +12,36 @@ mod selene_defs;
pub mod definitions; pub mod definitions;
pub use docs_file::generate_from_type_definitions as generate_docs_json_from_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 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 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 { pub fn generate_typedefs_file_from_dir(dir: &Dir<'_>) -> String {
let mut result = String::new(); let mut result = String::new();