const fs = require("fs"); const path = require("path"); // Extract emmy lua classes from a file // "---@class Bastion" -> "Bastion" /* "---@class Bastion" -> "Bastion" Bastion = { Test = function(self, name, name2) return 5 end, isTest = true, } -> ["Bastion", variables: [{key: 'Test', value: 'function(self, name, name2)'} , {key: 'isTest', value: 'true'}] */ function extractClasses(file) { const classes = []; const lines = file.split("\n"); let className = ""; let variables = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith("---@class ")) { // We have reached the start of a class documentation className = line.replace("---@class ", ""); } else if (line.startsWith("}")) { if (className != "") { // console.log("end of class", className, "variables", variables); classes.push({ name: className, variables, }); className = ""; variables = []; } // if the line contains both { and } then the table is empty and we can just add it to the classes array } else if (className != "" && line.includes("{") && line.includes("}")) { classes.push({ name: className, variables: [], }); className = ""; variables = []; } else if (className != "" && !line.includes("{")) { // if the line contains a = then it is a variable declaration if (line.includes("=")) { const [key, value] = line.split("="); variables.push({ key: key.trim(), value: value.trim(), }); } } } // console.log(classes); 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 = []; let globalClasses = []; // 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( (classData) => { const classFunctions = functions.filter( (func) => func.className === classData.name ); return { ...classData, functions: classFunctions, }; } ); globalClasses = [...globalClasses, ...classes]; // console.log(classes); // write the data to disk as a markdown file const markdown = classes .map((classData) => { const classMarkdown = `# ${classData.name} ${classData.variables ? "## Variables" : ""} ${classData.variables .map((variable) => { return `\`${variable.key} = ${variable.value}\``; }) .join("\n")} ${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, }); } let final = markdown.replace(/^[ \t]+/gm, ""); fs.writeFileSync(p, final); } // 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(_classes, _funcs) { // console.log(_classes); const luaClasses = _classes.map((classData) => { let str = ""; if (classData.description && classData.description !== "") { str += `--- ${classData.description}\n`; } str += `---@class ${classData.name}\n`; if (classData.variables.length > 0) { classData.variables.map((variable) => { str += `---@field ${variable.key} ${variable.value}\n`; }); } return str; }); // console.log(_funcs); const lua = _funcs .map((func) => { let str = ""; if (func.description && 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"); let output = [...luaClasses, lua].join("\n\n"); fs.writeFileSync(path.join(__dirname, "output", "API.lua"), output); } // Wipe the output directory fs.rmdirSync(path.join(__dirname, "output"), { recursive: true }); fs.mkdirSync(path.join(__dirname, "output")); DumpDirectory(path.join(__dirname, "input")); DumpAPIFile(globalClasses, globalFunctions);