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/),
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

View file

@ -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

View file

@ -9,6 +9,7 @@ use super::{
pub struct DefinitionsItemBuilder {
exported: bool,
kind: Option<DefinitionsItemKind>,
typ: Option<String>,
name: Option<String>,
meta: Option<String>,
value: Option<String>,
@ -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<S: AsRef<str>>(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<DefinitionsItem> {
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(),
}
}
}

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")]
pub struct DefinitionsItem {
#[serde(skip_serializing_if = "skip_serialize_is_false")]
pub(super) exported: bool,
pub(super) kind: DefinitionsItemKind,
pub(super) typ: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(super) name: Option<String>,
#[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<String> {
self.typ.clone()
}
pub fn get_meta(&self) -> Option<&str> {
self.meta.as_deref()
}

View file

@ -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",

View file

@ -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<String, DefinitionsParserItem>,
found_top_level_types: HashMap<String, TypeInfo>,
found_top_level_comments: HashMap<String, Option<String>>,
found_top_level_declares: Vec<String>,
}
@ -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<DefinitionsItemKind>,
) -> 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<Vec<DefinitionsItem>> {
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)
}
}

View file

@ -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)]

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.
";
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<String, String>) -> 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
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(|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());
.filter_map(|child| {
if child == main {
None
} else {
Some(
DefinitionsItemBuilder::from(child)
.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
@ -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<String> {
.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::<Vec<_>>();
.map(|arg| format!("{}: {}", arg.name.trim(), arg.typedef.trim()))
.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!(
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::<Vec<_>>();
let types = item
.children()
.iter()
.filter(|child| child.is_type())
.collect::<Vec<_>>();
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() {
write!(contents, "\n\n---\n\n## Properties\n\n")?;
}
for property in properties {
generate_markdown_documentation(contents, property, depth + 1)?;
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, 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(())
}
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 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();