const fs = require("fs"); const path = require("path"); // Extract emmy lua classes from a file // "---@class Bastion" -> "Bastion" function extractClasses(file) { const classes = []; const lines = file.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith("---@class ")) { const className = line.replace("---@class ", ""); classes.push(className); } } return classes; } // console.log( // extractClasses(fs.readFileSync(path.join(__dirname, "Bastion.lua"), "utf8")) // ); // Extract emmy lua functions from a file /* "---@param name string" -> "name", "string" "---@param name2 string|nil" -> "name2", "string|nil" "---@return string" -> "string" "function Bastion:Test(name, name2)" -> "Bastion", "Test" Every function documentation starts with a ---@param line or a ---@return line and ends with a function line, we should ignore any other lines in between these lines return { name: "Bastion", function: "Test", params: [ { name: "name", type: "string" }, { name: "name2", type: "string|nil" }, ], returns: [ { type: "string" } ] } */ function extractFunctions(file) { const functions = []; const lines = file.split("\n"); let params = []; let returns = []; let description = ""; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith("---@param ")) { const paramLine = line.replace("---@param ", ""); // get the name (everything before the first space) and the types (everything after the first space allowing for spaces in the types) const name = paramLine.split(" ")[0]; const type = paramLine.replace(name + " ", ""); params.push({ name, type, }); } else if (line.startsWith("---@return ")) { const returnLine = line.replace("---@return ", ""); const type = returnLine; returns.push({ type, }); } else if (line.startsWith("---") && !line.startsWith("---@")) { // This is a description line description += line.replace("---", "") + "\n"; } else if (line.startsWith("function ")) { // We have reached the end of the function documentation, now we can extract the function name and class name if (params.length === 0 && returns.length === 0) { // There is no documentation for this function continue; } const functionLine = line.replace("function ", ""); const [className, functionName] = functionLine.split(":"); functions.push({ className: className, function: functionName, params, returns, description: description, }); // Reset the params and returns array params = []; returns = []; description = ""; } } return functions; } let globalFunctions = []; // dump a file to markdown function Dump(filePath) { const fileName = path.basename(filePath); const functions = extractFunctions(fs.readFileSync(filePath, "utf8")); globalFunctions = [...globalFunctions, ...functions]; const classes = extractClasses(fs.readFileSync(filePath, "utf8")).map( (className) => { return { name: className, functions: functions.filter((func) => func.className === className), }; } ); // console.log(classes); // write the data to disk as a markdown file const markdown = classes .map((classData) => { const classMarkdown = `# ${classData.name} ${classData.functions .map((func) => { return `## ${func.className}:${func.function} ${func.description} ${func.params.length > 0 ? "### Parameters" : ""} ${func.params .map((param) => { return `\`${param.name} (${param.type})\``; }) .join("\n")} ${func.returns.length > 0 ? "### Returns" : ""} ${func.returns .map((ret) => { return `\`-> ${ret.type}\``; }) .join("\n")}`; }) .join("\n")}`; return classMarkdown; }) .join("\n"); const p = path.join(filePath.replace("input", "output")) + ".md"; // create the output directory if it doesn't exist if (!fs.existsSync(path.dirname(p))) { fs.mkdirSync(path.dirname(p), { recursive: true, }); } fs.writeFileSync(p, markdown); } // Dump(path.join(__dirname, "Bastion.lua")); function DumpDirectory(directory) { // get all the files in the directory const files = fs.readdirSync(directory); // filter out the files that are not lua files const luaFiles = files.filter((file) => file.endsWith(".lua")); // dump each file luaFiles.forEach((file) => { Dump(path.join(directory, file)); }); // dump the files in the subdirectories const subDirectories = files.filter((file) => fs.lstatSync(path.join(directory, file)).isDirectory() ); subDirectories.forEach((subDirectory) => { DumpDirectory(path.join(directory, subDirectory)); }); } function DumpAPIFile(_funcs) { // console.log(_funcs); const lua = _funcs .map((func) => { let str = ""; if (func.description !== "") { str += `--- ${func.description}\n`; } func.params.map((param) => { str += `---@param ${param.name} ${param.type}\n`; }); func.returns.map((ret) => { str += `---@return ${ret.type}\n`; }); str += `function ${func.className}:${func.function} end`; return str; }) .join("\n\n"); fs.writeFileSync(path.join(__dirname, "output", "API.lua"), lua); } // Wipe the output directory fs.rmdirSync(path.join(__dirname, "output"), { recursive: true }); fs.mkdirSync(path.join(__dirname, "output")); DumpDirectory(path.join(__dirname, "input")); DumpAPIFile(globalFunctions);