From c86855ca0c47f87f4a8e58f75f9f6ae2ae43833e Mon Sep 17 00:00:00 2001 From: "AsynchronousMatrix (Jack)" Date: Thu, 20 Feb 2025 22:08:33 +0000 Subject: [PATCH] feat: implement MD document generation for project documentation --- .lune/generator.luau | 20 +++ .lune/util/generator/generator.luau | 51 ++++++++ .lune/util/generator/markdown.luau | 191 ++++++++++++++++++++++++++++ .lune/util/generator/moonwave.luau | 85 +++++++++++++ .lune/util/generator/types.luau | 39 ++++++ 5 files changed, 386 insertions(+) create mode 100644 .lune/generator.luau create mode 100644 .lune/util/generator/generator.luau create mode 100644 .lune/util/generator/markdown.luau create mode 100644 .lune/util/generator/moonwave.luau create mode 100644 .lune/util/generator/types.luau diff --git a/.lune/generator.luau b/.lune/generator.luau new file mode 100644 index 0000000..4b01ae9 --- /dev/null +++ b/.lune/generator.luau @@ -0,0 +1,20 @@ +--> Generate documentation for this project + +local Moonwave = require("./util/generator/moonwave") +local Generator = require("./util/generator/generator") +local Markdown = require("./util/generator/markdown") + +Generator.removeLegacyFiles() + +local packageCommentJson = Moonwave.extractCommentsIntoJson() + +for _, classDocumentation in packageCommentJson do + local documentPath = Generator.writeClassName(classDocumentation.name) + local documentContent = Markdown.generateMarkdownDocumentFor(classDocumentation) + + print(`Writing document '{classDocumentation.name}' to '{documentPath}', size: {string.len(documentContent)}`) + + Generator.writeClassContent(documentPath, documentContent) +end + +return {} \ No newline at end of file diff --git a/.lune/util/generator/generator.luau b/.lune/util/generator/generator.luau new file mode 100644 index 0000000..7b3060c --- /dev/null +++ b/.lune/util/generator/generator.luau @@ -0,0 +1,51 @@ +local fs = require("@lune/fs") +local net = require("@lune/net") + +local Generator = {} + +function Generator.removeLegacyFiles() + if fs.isDir("docs/classes") then + fs.removeDir("docs/classes") + end + + fs.writeDir("docs/classes") +end + +function Generator.writeClassName(className: string) + local classPath = string.split(className, ".") + local fullPath = "docs/classes/" + + local fileName = table.remove(classPath, #classPath) :: string + local netSafeFileName = net.urlEncode(fileName, false) + + for index, path in classPath do + if not fs.isDir(fullPath .. path) then + fs.writeDir(fullPath .. path) + end + + fullPath ..= `{path}/` + end + + fs.writeFile(fullPath .. `{netSafeFileName}.md`, ``) + return fullPath .. `{netSafeFileName}.md` +end + +function Generator.writeClassContent(classPath: string, classContent: string) + local pathComponents = string.split(classPath, "/") + local path = "." + + table.remove(pathComponents, #pathComponents) + + for _, component in pathComponents do + warn(component) + if not fs.isDir(`{path}/{component}`) then + fs.writeDir(`{path}/{component}`) + + path ..= `/{component}` + end + end + + fs.writeFile(classPath, classContent) +end + +return Generator \ No newline at end of file diff --git a/.lune/util/generator/markdown.luau b/.lune/util/generator/markdown.luau new file mode 100644 index 0000000..f19dde4 --- /dev/null +++ b/.lune/util/generator/markdown.luau @@ -0,0 +1,191 @@ +local Moonwave = require("moonwave") +local Types = require("types") + +local Markdown = {} + +local function comment(source) + return `[//]: # ({source})\n` +end + +local function newline() + return `\n` +end + +local function h1(source) + return `# {source}\n\n` +end + +local function h2(source) + return `## {source}\n\n` +end + +local function h3(source) + return `### {source}\n\n` +end + +local function input(source) + return `{source}\n` +end + +local function separator() + return `---\n` +end + +local function property(name: string, type: string) + return `` +end + +local function getReadableParamList(proto: Moonwave.FunctionData) + local readableList = " " + + if #proto.params == 0 then + return "" + end + + for index, paramObject in proto.params do + readableList ..= `\`{paramObject.name}\` {Types.parseLuauType(paramObject.lua_type, true)}` .. (index == #proto.params and ` ` or `, `) + end + + return readableList +end + +local function getReadableReturnsList(proto: Moonwave.FunctionData) + local readableList = " " + + if #proto.returns == 0 then + return Types.parseLuauType("nil") + end + + for index, returnObject in proto.returns do + readableList ..= `{Types.parseLuauType(returnObject.lua_type, true)}` .. (index == #proto.returns and ` ` or `, `) + end + + return readableList +end + +local function frontmatter(source: { + name: string, + description: string, + order: number, +}) + return `---\ntitle: {source.name}\ndescription: {source.description}\nsidebar:\n order: {source.order}\n collapsed: true\n---` +end + +function Markdown.generateMarkdownDocumentFor(classDocumentation: Moonwave.DataExportObject) + local markdownFile = `` + + local className = classDocumentation.name:gmatch("%S+%.(%S+)")() or classDocumentation.name + local classDescription = classDocumentation.desc + local classProperties = classDocumentation.properties + + local classMethods = Moonwave.getFunctionsOfFunctionType(classDocumentation.functions, "method") + local classFunctions = Moonwave.getFunctionsOfFunctionType(classDocumentation.functions, "static") + + local sizeOfClassProperties = #classProperties + + local sizeOfClassMethods = #classMethods + local sizeOfClassFunctions = #classFunctions + + local order = #string.split(classDocumentation.name, ".") + + markdownFile ..= frontmatter({ + name = className, + description = `luau-unzip docs for {className}.`, + order = order, + }) + + markdownFile ..= newline() + + markdownFile ..= comment( + `This file was automatically @generated from moonwave comments using a script. Please do not edit by hand.` + ) + markdownFile ..= comment(`To edit this documentation, make changes to the main luau-unzip repo.`) + + markdownFile ..= newline() + + + markdownFile ..= comment(`----- DOCUMENT INTRODUCTION ----- `) + + markdownFile ..= newline() + + markdownFile ..= h1(className) + + markdownFile ..= input(classDescription) + + markdownFile ..= newline() + + markdownFile ..= comment(`----- DOCUMENT PROPERTIES ----- `) + + markdownFile ..= newline() + + markdownFile ..= h2(`Properties`) + + if sizeOfClassProperties > 0 then + for _, prop in classProperties do + markdownFile ..= h3(prop.name) + markdownFile ..= property(`{className}.{prop.name}`, prop.lua_type) + markdownFile ..= newline() + + if prop.desc ~= "" then + markdownFile ..= separator() + markdownFile ..= input(prop.desc) + end + end + else + markdownFile ..= input(`The {className} instance has no set properties!`) + end + + markdownFile ..= newline() + + markdownFile ..= comment(`----- DOCUMENT METHODS ----- `) + + markdownFile ..= newline() + + markdownFile ..= h2(`Methods`) + + if sizeOfClassMethods > 0 then + for _, method in classMethods do + markdownFile ..= h3(method.name) + markdownFile ..= input( + `> {className}:{method.name}({getReadableParamList(method)}) -> {getReadableReturnsList(method)}` + ) + + if method.desc then + markdownFile ..= newline() + markdownFile ..= input(method.desc) + end + end + else + markdownFile ..= input(`The {className} instance has no set methods!`) + end + + markdownFile ..= newline() + + markdownFile ..= comment(`----- DOCUMENT FUNCTIONS ----- `) + + markdownFile ..= newline() + + markdownFile ..= h2(`Functions`) + + if sizeOfClassFunctions > 0 then + for _, func in classFunctions do + markdownFile ..= h3(func.name) + markdownFile ..= input( + `> {className}.{func.name}({getReadableParamList(func)}) -> {getReadableReturnsList(func)}` + ) + + if func.desc then + markdownFile ..= newline() + markdownFile ..= input(func.desc) + end + end + else + markdownFile ..= input(`The {className} instance has no set functions!`) + end + + markdownFile ..= newline() + + return markdownFile +end + +return Markdown \ No newline at end of file diff --git a/.lune/util/generator/moonwave.luau b/.lune/util/generator/moonwave.luau new file mode 100644 index 0000000..31c7021 --- /dev/null +++ b/.lune/util/generator/moonwave.luau @@ -0,0 +1,85 @@ +local process = require("@lune/process") +local serde = require("@lune/serde") + +local Moonwave = {} + +function Moonwave.getFunctionsOfFunctionType(inputArray: { any }, functionType: string) + local resultArray = {} + + for _, functionObject in inputArray do + if functionObject.function_type == functionType then + table.insert(resultArray, functionObject) + end + end + + return resultArray +end + +function Moonwave.extractCommentsIntoJson(): moonwaveDataExportArray + local moonwaveExtractResult = process.spawn("./moonwave-extractor", { + "extract", + "lib", + }) + + if not moonwaveExtractResult.ok then + print(moonwaveExtractResult.stderr) + + return process.exit(1) + else + local moonwaveData = serde.decode("json", moonwaveExtractResult.stdout) + + return moonwaveData + end +end + +export type PropertyData = { + name: string, + desc: string, + lua_type: string, + source: { + line: number, + path: string, + }, +} + +export type FunctionData = { + name: string, + desc: string, + since: string?, + unreleased: boolean?, + source: { + path: string, + line: number, + }, + function_type: "method" | "static", + returns: { + { + desc: string, + lua_type: string, + } + }, + params: { + { + name: string, + desc: string, + lua_type: string, + } + }, +} + +export type DataExportObject = { + name: string, + functions: { FunctionData }, + source: { path: string, line: number }, + properties: { PropertyData }, + inherited: { [string]: { functions: { FunctionData }, properties: { PropertyData } } }?, + desc: string, + types: unknown, + tags: { string }? +} + +export type moonwaveDataExportArray = { + DataExportObject +} + +return Moonwave \ No newline at end of file diff --git a/.lune/util/generator/types.luau b/.lune/util/generator/types.luau new file mode 100644 index 0000000..1af6664 --- /dev/null +++ b/.lune/util/generator/types.luau @@ -0,0 +1,39 @@ +local Types = {} + +local mappedLuauDataTypes = { + -- generic Roblox datatypes + [{ "nil" }] = "https://www.lua.org/pil/2.1.html", + [{ "boolean", "bool" }] = "https://www.lua.org/pil/2.2.html", + [{ "number" }] = "https://www.lua.org/pil/2.3.html", + [{ "string" }] = "https://www.lua.org/pil/2.4.html", + [{ "table" }] = "https://www.lua.org/pil/2.5.html", + [{ "tuple", "..." }] = "https://www.lua.org/pil/5.1.html", + [{ "userdata", "proxy" }] = "https://www.lua.org/pil/28.1.html", +} + +function Types.parseLuauType(luaType: string, escapeBracket: boolean?) + luaType = luaType == "" and "any" or luaType + + if string.sub(luaType, 1, 1) == "!" then + local path = string.split(string.sub(luaType, 2), "/") + local fileName = path[1] + + return `[{fileName}](/Classes/{table.concat(path, "/")})` + end + + local luaTypeCheck = string.gsub(string.lower(luaType), "%W", "") + + if escapeBracket then + luaType = string.gsub(luaType, "{", "\\{") + end + + for queryTable, apiUrl in mappedLuauDataTypes do + if table.find(queryTable, luaTypeCheck) then + return `[{luaType}]({apiUrl})` + end + end + + return luaType +end + +return Types \ No newline at end of file