From 2ec3e2f133ad08ce69ebd8e7419c8d0962b3d852 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Tue, 30 Dec 2025 00:19:59 -0300 Subject: [PATCH 01/19] Added LuaLS generator Decided to create a LuaLS annotation generator since the original one was only generating LuaCATS which is not compatible or used that much anymore. This should be a great improvement for those using the LuaLS LSP, very common amongst most IDEs and code editors :) --- api/generators/luals.lua | 443 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 443 insertions(+) create mode 100644 api/generators/luals.lua diff --git a/api/generators/luals.lua b/api/generators/luals.lua new file mode 100644 index 00000000..447e5730 --- /dev/null +++ b/api/generators/luals.lua @@ -0,0 +1,443 @@ +local DOCS_URL = "https://lovr.org/docs/" + +local KEYWORD_LOOKUP = { + ["and"] = true, + ["break"] = true, + ["do"] = true, + ["else"] = true, + ["elseif"] = true, + ["end"] = true, + ["false"] = true, + ["for"] = true, + ["function"] = true, + ["if"] = true, + ["in"] = true, + ["local"] = true, + ["nil"] = true, + ["not"] = true, + ["or"] = true, + ["repeat"] = true, + ["return"] = true, + ["then"] = true, + ["true"] = true, + ["until"] = true, + ["while"] = true, +} + +local OPERATOR_LOOKUP = { + ["add"] = "add", + ["sub"] = "sub", + ["div"] = "div", + ["mul"] = "mul", + --["equals"] = "eq", + --["length"] = "len", +} + +-- this link usually assumes that anything with a proper API definition has to have +-- a documentation page. (it usually is the case.) +local function writeLink(key, f) + f:write("---\n") + f:write("--- [Open in browser](") + f:write(DOCS_URL) + f:write(key) + f:write(")\n") +end + +-- Breaks a multi-line string (such as a description) into a bunch of commented +-- likes. +-- CONSIDER: Maybe break them even further at periods? Not sure. +local function writeComment(cmt, f) + for line in cmt:gmatch('[^\r\n]+') do + f:write("--- ") + f:write(line) + f:write("\n") + end +end + +--- Turns a multiline string into a single line string. +local function writeSingleLine(cmt, f) + f:write((cmt:gsub("\n", ""))) +end + +local function writeEnum(enum, f) + writeComment(enum.description, f) + + --# ---@alias MyEnum + f:write("---@alias ") + f:write(enum.name) + f:write("\n") + + --# --| 'Cool' # Denotes a particular coolness. + for _, value in ipairs(enum.values) do + f:write("---| '\"") + f:write(value.name) + f:write("\"' # ") + writeSingleLine(value.description, f) + f:write("\n") + end + + f:write("\n") +end + +-- Utility for translating from the pseudo-syntax used for types in Lövr's API +-- to the syntax used for LuaLS annotations. +local function handleType(t) + if t == "*" then -- * -> any + return "any" + end + + local a = t:match("{(.*)}") + if a then + local things = {} + for item in a:gmatch("[^|]+") do + -- trim whitespace around each item + item = item:match("^%s*(.-)%s*$") + table.insert(things, handleType(item)) + end + + -- {type} -> type[] + if #things == 1 then + return things[1] .. "[]" + end + + -- {A | B} -> (A | B)[] + return "(" .. table.concat(things, "|") .. ")[]" + end + + -- A -> A + return t +end + +-- Assumes that an object with :sub, also has :__sub, for example +local function writeOperator(func, f) + local name = OPERATOR_LOOKUP[func.name] + if not name then + return + end + + for i = 1, #func.variants do + local var = func.variants[i] + + if (#var.arguments > 1) then + goto continue + end + + local params = {} + for _, arg in ipairs(var.arguments) do + table.insert(params, handleType(arg.type)) + end + + if (#var.returns > 1) then + goto continue + end + + local returns = {} + for _, ret in ipairs(var.returns) do + table.insert(returns, handleType(ret.type)) + end + + --# ---@operator add(Vec2): Vec2 + f:write("---@operator ") + f:write(name) + f:write("(") + f:write(table.concat(params, ", ")) + f:write(")") + if #returns > 0 then + f:write(": ") + f:write(table.concat(returns, ", ")) + end + f:write("\n") + + ::continue:: + end +end + +local function handleParam(name) + if name:sub(1, 3) == "..." then -- ...something -> ... + return "..." + elseif KEYWORD_LOOKUP[name] then -- return -> return_ + return name .. "_" + end + return name -- a -> a +end + +local function writeFunction(func, namespace, is_method, f) + local key = func.key + local name = func.name + + writeComment(func.description, f) + writeLink(key, f) + + if func.related then + --# ---@see Robot.destroy + for _, rel in ipairs(func.related) do + f:write("---@see ") + f:write((rel:gsub(":", "."))) + f:write("\n") + end + end + + local params = {} + local first = func.variants[1] + do + for _, arg in ipairs(first.arguments) do + local param = handleParam(arg.name) + local type = handleType(arg.type) .. (arg.default and "?" or "") + + f:write("---@param ") + f:write(param) + f:write(" ") + f:write(type) + f:write(" # ") + writeSingleLine(arg.description, f) + if arg.default then + f:write(" (default: ") + f:write(arg.default) + f:write(")") + end + f:write("\n") + table.insert(params, param) + end + + for _, ret in ipairs(first.returns) do + f:write("---@return ") + f:write(handleType(ret.type)) + if ret.description then + f:write(" # ") + writeSingleLine(ret.description, f) + end + f:write("\n") + end + end + + for i = 2, #func.variants do + local var = func.variants[i] + + local p = {} + if is_method then + table.insert(p, "self: " .. namespace) + end + + for _, arg in ipairs(var.arguments) do + local param = handleParam(arg.name) + if arg.default then + param = param .. "?" + end + table.insert(p, param .. ": " .. handleType(arg.type)) + end + + local returns = {} + for _, ret in ipairs(var.returns) do + table.insert(returns, handleType(ret.type)) + end + + f:write("---@overload fun(") + f:write(table.concat(p, ", ")) + f:write(")") + if #returns > 0 then + f:write(": ") + f:write(table.concat(returns, ", ")) + end + f:write("\n") + end + + f:write("function ") + f:write(namespace) + f:write(is_method and ":" or ".") + f:write(name) + f:write("(") + f:write(table.concat(params, ", ")) + f:write(") end") + + f:write("\n\n") +end + +local function writeCallback(call, f) + local name = call.name + local var = call.variants[1] + + local p = {} + for _, arg in ipairs(var.arguments) do + local param = handleParam(arg.name) + if arg.default then + param = param .. "?" + end + table.insert(p, param .. ": " .. handleType(arg.type)) + end + + local returns = {} + for _, ret in ipairs(var.returns) do + table.insert(returns, handleType(ret.type)) + end + + writeComment(call.description, f) + writeLink("lovr." .. name, f) + + f:write("---@field ") + f:write(name) + f:write(" fun(") + f:write(table.concat(p, ", ")) + f:write(")") + if #returns > 0 then + f:write(": ") + f:write(table.concat(returns, ", ")) + end + f:write("\n") +end + +local function writeObject(object, namespace, f) + local key = object.key + local name = object.name + + writeComment(object.description, f) + writeLink(key, f) + + --# ---@class Blob + f:write("---@class ") + f:write(name) + if name == "Mat4" then + f:write(": number[]") + end + f:write("\n") + + local vec_n = tonumber(name:sub(4)) + if vec_n and name:sub(1, 3) == "Vec" then + local c = "xyzw" + for i = 1, vec_n do + f:write("---@field ") + f:write(c:sub(i, i)) + f:write(" number\n") + end + end + + --if object.constructors then + -- for _, const in ipairs(object.constructors) do + -- f:write("---@see ") + -- f:write(const) + -- f:write(" # (Constructor)\n") + -- end + --end + + for _, func in ipairs(object.methods) do + writeOperator(func, f) + end + + --# local Blob = {} + f:write("local ") + f:write(name) + f:write(" = {}\n\n") + + for _, func in ipairs(object.methods) do + writeFunction(func, name, true, f) + end + + return name +end + +return function(api) + local root = lovr.filesystem.getSource() + + local OUTPUT = root .. "/luals/" + local API_OUTPUT = OUTPUT .. "library/" + + if lovr.system.getOS() == "Windows" then + os.execute("mkdir " .. API_OUTPUT:gsub("/", "\\")) + else + os.execute("mkdir -p " .. API_OUTPUT) + end + + print("Processing modules") + for _, module in ipairs(api.modules) do + local key = module.key + local name = key:match("([^.]+)$") + local is_main_module = key == "lovr" + + io.write(("- %-20s"):format(key .. "...")) + + -- We skip external modules as they do not have any meaningful annotations. + -- Plus, they often have their own, separately mantained annotations instead. + if module.external then + print("SKIP") + goto continue + end + + local f = io.open(API_OUTPUT .. key .. ".lua", "w+") + assert(f, "Could not open file, make sure you got permissions!") + + --# ---@meta lovr.audio + f:write("---@meta ") + f:write(key) + f:write("\n\n") + + writeComment(module.description, f) + writeLink(key, f) + + --# ---@class lovr.audio + f:write("---@class ") + f:write(key) + if is_main_module then + f:write(": table\n") + for _, call in ipairs(api.callbacks) do + writeCallback(call, f) + end + else + f:write("\n") + end + + --# lovr.audio = {} + f:write(key) + f:write(" = {}\n\n") + + -- We do this order just because LuaLS might act up when + -- types and functions are declared out of order. + for _, enum in ipairs(module.enums) do + writeEnum(enum, f) + end + + for _, objt in ipairs(module.objects) do + writeObject(objt, name, f) + end + + for _, func in ipairs(module.functions) do + writeFunction(func, key, false, f) + end + + -- We write all the darn globals in the world. + if is_main_module then + f:write("vec2 = lovr.math.vec2\n") + f:write("Vec2 = lovr.math.newVec2\n") + f:write("vec3 = lovr.math.vec3\n") + f:write("Vec3 = lovr.math.newVec3\n") + f:write("vec4 = lovr.math.vec4\n") + f:write("Vec4 = lovr.math.newVec4\n") + f:write("mat4 = lovr.math.mat4\n") + f:write("Mat4 = lovr.math.newMat4\n") + f:write("quat = lovr.math.quat\n") + f:write("Quat = lovr.math.newQuat\n") + else + f:write("return ") + f:write(key) + end + f:close() + + print("OK") + + ::continue:: + end + + -- Write our manifest :) + do + local f = assert(io.open(OUTPUT .. "config.json", "w+")) + + f:write('{\n') + f:write(' "name": "LÖVR",\n') + f:write(' "words": ["lovr%.%w+"],\n') + f:write(' "settings": {\n') + f:write(' "Lua.runtime.version" : "LuaJIT"\n') + f:write(' }\n') + f:write('}\n') + f:close() + + print("Manifest written successfully.") + end +end From c2d480f35a1bd2a31d88edfeeca893c5a5d44056 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Tue, 30 Dec 2025 01:06:14 -0300 Subject: [PATCH 02/19] Get rid of unnecessary keyword lookup --- api/generators/luals.lua | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index 447e5730..7951af27 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -1,29 +1,5 @@ local DOCS_URL = "https://lovr.org/docs/" -local KEYWORD_LOOKUP = { - ["and"] = true, - ["break"] = true, - ["do"] = true, - ["else"] = true, - ["elseif"] = true, - ["end"] = true, - ["false"] = true, - ["for"] = true, - ["function"] = true, - ["if"] = true, - ["in"] = true, - ["local"] = true, - ["nil"] = true, - ["not"] = true, - ["or"] = true, - ["repeat"] = true, - ["return"] = true, - ["then"] = true, - ["true"] = true, - ["until"] = true, - ["while"] = true, -} - local OPERATOR_LOOKUP = { ["add"] = "add", ["sub"] = "sub", @@ -153,10 +129,8 @@ local function writeOperator(func, f) end local function handleParam(name) - if name:sub(1, 3) == "..." then -- ...something -> ... + if name:sub(1, 3) == "..." then -- ...something -> ... return "..." - elseif KEYWORD_LOOKUP[name] then -- return -> return_ - return name .. "_" end return name -- a -> a end From db0f2298a5ecef95dc9351b3baa0313302fbc310 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:54:22 -0300 Subject: [PATCH 03/19] Added support for options and transient table types. --- api/generators/luals.lua | 50 +++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index 7951af27..73f7ff89 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -57,27 +57,36 @@ end -- Utility for translating from the pseudo-syntax used for types in Lövr's API -- to the syntax used for LuaLS annotations. -local function handleType(t) +local function handleType(t, tab) + if tab then + local entries = {} + + for _, e in ipairs(tab) do + local opt = e.default and "?" or "" + table.insert(entries, e.name .. ": " .. handleType(e.type, e.table) .. opt) + end + + return "{" .. table.concat(entries, ", ") .. "}" + end + if t == "*" then -- * -> any return "any" end - local a = t:match("{(.*)}") + local a = t:match("{(.*)}") -- {type} -> type[] if a then + return handleType(a) .. "[]" + end + + if t:find("|") then -- {A | B} -> (A | B) local things = {} - for item in a:gmatch("[^|]+") do + for item in t:gmatch("[^|]+") do -- trim whitespace around each item item = item:match("^%s*(.-)%s*$") table.insert(things, handleType(item)) end - -- {type} -> type[] - if #things == 1 then - return things[1] .. "[]" - end - - -- {A | B} -> (A | B)[] - return "(" .. table.concat(things, "|") .. ")[]" + return "(" .. table.concat(things, " | ") .. ")" end -- A -> A @@ -100,7 +109,7 @@ local function writeOperator(func, f) local params = {} for _, arg in ipairs(var.arguments) do - table.insert(params, handleType(arg.type)) + table.insert(params, handleType(arg.type, arg.table)) end if (#var.returns > 1) then @@ -109,7 +118,7 @@ local function writeOperator(func, f) local returns = {} for _, ret in ipairs(var.returns) do - table.insert(returns, handleType(ret.type)) + table.insert(returns, handleType(ret.type, ret.table)) end --# ---@operator add(Vec2): Vec2 @@ -156,7 +165,7 @@ local function writeFunction(func, namespace, is_method, f) do for _, arg in ipairs(first.arguments) do local param = handleParam(arg.name) - local type = handleType(arg.type) .. (arg.default and "?" or "") + local type = handleType(arg.type, arg.table) .. (arg.default and "?" or "") f:write("---@param ") f:write(param) @@ -175,7 +184,7 @@ local function writeFunction(func, namespace, is_method, f) for _, ret in ipairs(first.returns) do f:write("---@return ") - f:write(handleType(ret.type)) + f:write(handleType(ret.type, ret.table)) if ret.description then f:write(" # ") writeSingleLine(ret.description, f) @@ -197,12 +206,12 @@ local function writeFunction(func, namespace, is_method, f) if arg.default then param = param .. "?" end - table.insert(p, param .. ": " .. handleType(arg.type)) + table.insert(p, param .. ": " .. handleType(arg.type, arg.table)) end local returns = {} for _, ret in ipairs(var.returns) do - table.insert(returns, handleType(ret.type)) + table.insert(returns, handleType(ret.type, ret.table)) end f:write("---@overload fun(") @@ -236,12 +245,12 @@ local function writeCallback(call, f) if arg.default then param = param .. "?" end - table.insert(p, param .. ": " .. handleType(arg.type)) + table.insert(p, param .. ": " .. handleType(arg.type, arg.table)) end local returns = {} for _, ret in ipairs(var.returns) do - table.insert(returns, handleType(ret.type)) + table.insert(returns, handleType(ret.type, ret.table)) end writeComment(call.description, f) @@ -259,7 +268,7 @@ local function writeCallback(call, f) f:write("\n") end -local function writeObject(object, namespace, f) +local function writeObject(object, f) local key = object.key local name = object.name @@ -323,7 +332,6 @@ return function(api) print("Processing modules") for _, module in ipairs(api.modules) do local key = module.key - local name = key:match("([^.]+)$") local is_main_module = key == "lovr" io.write(("- %-20s"):format(key .. "...")) @@ -369,7 +377,7 @@ return function(api) end for _, objt in ipairs(module.objects) do - writeObject(objt, name, f) + writeObject(objt, f) end for _, func in ipairs(module.functions) do From 2f6d340771e4aea0fc07f8bf79c6c13f47202637 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Tue, 30 Dec 2025 17:58:21 -0300 Subject: [PATCH 04/19] Added Notes, Examples. Changed naming scheme for output files. Added globals.lua --- api/generators/luals.lua | 87 +++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index 73f7ff89..0081326d 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -23,6 +23,8 @@ end -- likes. -- CONSIDER: Maybe break them even further at periods? Not sure. local function writeComment(cmt, f) + cmt = cmt:gsub('^%s*(.-)%s*$', '%1') -- Remove newlines at the beginning and end + for line in cmt:gmatch('[^\r\n]+') do f:write("--- ") f:write(line) @@ -35,6 +37,42 @@ local function writeSingleLine(cmt, f) f:write((cmt:gsub("\n", ""))) end +local function writeInfo(data, f) + if data.description then + writeComment(data.description, f) + end + + if data.notes then + f:write("---\n") + f:write("--- ## NOTES:\n") + writeComment(data.notes, f) + end + + if data.examples then + f:write("---\n") + f:write("--- ## EXAMPLES:\n") + for i, example in ipairs(data.examples) do + f:write("--- ### ") + f:write(i) + f:write(".\n") + + if example.description then + writeComment(example.description, f) + end + + if example.code then + f:write("--- ```lua\n") + writeComment(example.code, f) + f:write("--- ```\n") + end + end + end + + if data.key then + writeLink(data.key, f) + end +end + local function writeEnum(enum, f) writeComment(enum.description, f) @@ -148,8 +186,7 @@ local function writeFunction(func, namespace, is_method, f) local key = func.key local name = func.name - writeComment(func.description, f) - writeLink(key, f) + writeInfo(func, f) if func.related then --# ---@see Robot.destroy @@ -253,8 +290,7 @@ local function writeCallback(call, f) table.insert(returns, handleType(ret.type, ret.table)) end - writeComment(call.description, f) - writeLink("lovr." .. name, f) + writeInfo(call, f) f:write("---@field ") f:write(name) @@ -332,6 +368,7 @@ return function(api) print("Processing modules") for _, module in ipairs(api.modules) do local key = module.key + local name = module.name local is_main_module = key == "lovr" io.write(("- %-20s"):format(key .. "...")) @@ -343,7 +380,7 @@ return function(api) goto continue end - local f = io.open(API_OUTPUT .. key .. ".lua", "w+") + local f = io.open(API_OUTPUT .. name .. ".lua", "w+") assert(f, "Could not open file, make sure you got permissions!") --# ---@meta lovr.audio @@ -351,8 +388,7 @@ return function(api) f:write(key) f:write("\n\n") - writeComment(module.description, f) - writeLink(key, f) + writeInfo(module, f) --# ---@class lovr.audio f:write("---@class ") @@ -384,22 +420,8 @@ return function(api) writeFunction(func, key, false, f) end - -- We write all the darn globals in the world. - if is_main_module then - f:write("vec2 = lovr.math.vec2\n") - f:write("Vec2 = lovr.math.newVec2\n") - f:write("vec3 = lovr.math.vec3\n") - f:write("Vec3 = lovr.math.newVec3\n") - f:write("vec4 = lovr.math.vec4\n") - f:write("Vec4 = lovr.math.newVec4\n") - f:write("mat4 = lovr.math.mat4\n") - f:write("Mat4 = lovr.math.newMat4\n") - f:write("quat = lovr.math.quat\n") - f:write("Quat = lovr.math.newQuat\n") - else - f:write("return ") - f:write(key) - end + f:write("return ") + f:write(key) f:close() print("OK") @@ -407,6 +429,25 @@ return function(api) ::continue:: end + -- Write our globals + do + local f = assert(io.open(API_OUTPUT .. "globals.lua", "w+")) + + f:write("---@meta\n\n") + f:write("vec2 = lovr.math.vec2\n") + f:write("Vec2 = lovr.math.newVec2\n") + f:write("vec3 = lovr.math.vec3\n") + f:write("Vec3 = lovr.math.newVec3\n") + f:write("vec4 = lovr.math.vec4\n") + f:write("Vec4 = lovr.math.newVec4\n") + f:write("mat4 = lovr.math.mat4\n") + f:write("Mat4 = lovr.math.newMat4\n") + f:write("quat = lovr.math.quat\n") + f:write("Quat = lovr.math.newQuat\n") + + f:close() + end + -- Write our manifest :) do local f = assert(io.open(OUTPUT .. "config.json", "w+")) From 9c763acfdd1f32ac71f62681ea735c9e6f376258 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Tue, 30 Dec 2025 18:13:26 -0300 Subject: [PATCH 05/19] Callback typing codegen cleanup. Added callbacks.lua Now it's not clumped anymore! --- api/generators/luals.lua | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index 0081326d..c9b454c0 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -272,7 +272,7 @@ local function writeFunction(func, namespace, is_method, f) f:write("\n\n") end -local function writeCallback(call, f) +local function writeCallbackType(call, f) local name = call.name local var = call.variants[1] @@ -292,8 +292,9 @@ local function writeCallback(call, f) writeInfo(call, f) - f:write("---@field ") + f:write("---@alias ") f:write(name) + f:write("_callback") f:write(" fun(") f:write(table.concat(p, ", ")) f:write(")") @@ -301,7 +302,7 @@ local function writeCallback(call, f) f:write(": ") f:write(table.concat(returns, ", ")) end - f:write("\n") + f:write("\n\n") end local function writeObject(object, f) @@ -393,21 +394,23 @@ return function(api) --# ---@class lovr.audio f:write("---@class ") f:write(key) + f:write(is_main_module and ":table\n" or "\n") + if is_main_module then - f:write(": table\n") for _, call in ipairs(api.callbacks) do - writeCallback(call, f) + f:write("---@field ") + f:write(call.name) + f:write(" ") + f:write(call.name) + f:write("_callback") + f:write("\n") end - else - f:write("\n") end --# lovr.audio = {} f:write(key) f:write(" = {}\n\n") - -- We do this order just because LuaLS might act up when - -- types and functions are declared out of order. for _, enum in ipairs(module.enums) do writeEnum(enum, f) end @@ -429,6 +432,18 @@ return function(api) ::continue:: end + do + local f = assert(io.open(API_OUTPUT .. "callbacks.lua", "w+")) + + f:write("---@meta\n\n") + + for _, call in ipairs(api.callbacks) do + local c = writeCallbackType(call, f) + end + + f:close() + end + -- Write our globals do local f = assert(io.open(API_OUTPUT .. "globals.lua", "w+")) From 6c9145833aed2964a66426f5403e4c4f5d091650 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Tue, 30 Dec 2025 19:05:46 -0300 Subject: [PATCH 06/19] Add [].name syntax for arrays of tables --- api/generators/luals.lua | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index c9b454c0..3b7b999e 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -99,9 +99,22 @@ local function handleType(t, tab) if tab then local entries = {} + local arrayable = {} for _, e in ipairs(tab) do - local opt = e.default and "?" or "" - table.insert(entries, e.name .. ": " .. handleType(e.type, e.table) .. opt) + if e.name:sub(1, 3) == "[]." then -- [].name: type -> {[number]: {name: type}} + e.name = e.name:sub(4) + table.insert(arrayable, e) + else -- {name: type} -> {name: type} + local opt = e.default and "?" or "" + local n = e.name .. ": " .. handleType(e.type, e.table) .. opt + table.insert(entries, n) + end + end + + if #arrayable > 0 then + table.insert(entries, + "[number]: " .. handleType(nil, arrayable) + ) end return "{" .. table.concat(entries, ", ") .. "}" From 476d09c84f359e71d2495b353276b2604dd2ff95 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:28:44 -0300 Subject: [PATCH 07/19] Fully support the [][n].name syntax, Add a workaround for Lua code blocks. --- api/generators/luals.lua | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index 3b7b999e..d4cd79b7 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -25,7 +25,19 @@ end local function writeComment(cmt, f) cmt = cmt:gsub('^%s*(.-)%s*$', '%1') -- Remove newlines at the beginning and end + local code_mode for line in cmt:gmatch('[^\r\n]+') do + -- WORKAROUND: LuaLS should be able to turn these into code blocks on its own, + -- sadly, LuaLS is stupid! so we got to do this manually. + + if not line:match("%S") then + local indented = line:sub(1, 1) == " " + if indented ~= code_mode then + f:write("--- ```lua\n") + end + code_mode = indented + end + f:write("--- ") f:write(line) f:write("\n") @@ -101,8 +113,11 @@ local function handleType(t, tab) local arrayable = {} for _, e in ipairs(tab) do - if e.name:sub(1, 3) == "[]." then -- [].name: type -> {[number]: {name: type}} - e.name = e.name:sub(4) + local from_array = e.name:sub(1, 2) == "[]" + local has_dot = e.name:sub(3, 3) == "." + + if from_array then + e.name = e.name:sub(has_dot and 4 or 3) table.insert(arrayable, e) else -- {name: type} -> {name: type} local opt = e.default and "?" or "" From da47a4a0d1c567851636e8b514349bab3eeb7e90 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Tue, 30 Dec 2025 22:21:36 -0300 Subject: [PATCH 08/19] Cleanup, commented stuff a bit better. --- api/generators/luals.lua | 132 ++++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 63 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index d4cd79b7..351966a2 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -9,19 +9,9 @@ local OPERATOR_LOOKUP = { --["length"] = "len", } --- this link usually assumes that anything with a proper API definition has to have --- a documentation page. (it usually is the case.) -local function writeLink(key, f) - f:write("---\n") - f:write("--- [Open in browser](") - f:write(DOCS_URL) - f:write(key) - f:write(")\n") -end - --- Breaks a multi-line string (such as a description) into a bunch of commented --- likes. --- CONSIDER: Maybe break them even further at periods? Not sure. +--- Breaks a multi-line string (such as a description) into a bunch of commented +--- lines, takes care of code blocks, input should be valid markdown. +--- CONSIDER: Maybe break them even further at periods? Not sure. local function writeComment(cmt, f) cmt = cmt:gsub('^%s*(.-)%s*$', '%1') -- Remove newlines at the beginning and end @@ -30,6 +20,7 @@ local function writeComment(cmt, f) -- WORKAROUND: LuaLS should be able to turn these into code blocks on its own, -- sadly, LuaLS is stupid! so we got to do this manually. + -- Ignore empty lines if not line:match("%S") then local indented = line:sub(1, 1) == " " if indented ~= code_mode then @@ -49,6 +40,8 @@ local function writeSingleLine(cmt, f) f:write((cmt:gsub("\n", ""))) end +--- Writes information related to an Object, Function, Enum, etc. +--- such as description, notes, examples, documentation links, and related things. local function writeInfo(data, f) if data.description then writeComment(data.description, f) @@ -81,11 +74,26 @@ local function writeInfo(data, f) end if data.key then - writeLink(data.key, f) + f:write("---\n") + f:write("--- [Open in browser](") + f:write(DOCS_URL) + f:write(data.key) + f:write(")\n") + end + + if data.related then + f:write("---\n") + for _, rel in ipairs(data.related) do + f:write("---@see ") + f:write((rel:gsub(":", "."))) + f:write("\n") + end end end +--- Generates an "@alias" directive for fake enums. local function writeEnum(enum, f) + -- We don't write full info here, because, sadly, LuaLS will ignore it. writeComment(enum.description, f) --# ---@alias MyEnum @@ -116,6 +124,8 @@ local function handleType(t, tab) local from_array = e.name:sub(1, 2) == "[]" local has_dot = e.name:sub(3, 3) == "." + -- {[].name: type} -> {[number]: {name: type}} + -- {[][1].name: type} -> {[number]: {[1]: {name: type}}} if from_array then e.name = e.name:sub(has_dot and 4 or 3) table.insert(arrayable, e) @@ -210,21 +220,13 @@ local function handleParam(name) return name -- a -> a end +--- Writes a function definition with all of its possible variants as +--- overloads. local function writeFunction(func, namespace, is_method, f) - local key = func.key local name = func.name writeInfo(func, f) - if func.related then - --# ---@see Robot.destroy - for _, rel in ipairs(func.related) do - f:write("---@see ") - f:write((rel:gsub(":", "."))) - f:write("\n") - end - end - local params = {} local first = func.variants[1] do @@ -262,6 +264,7 @@ local function writeFunction(func, namespace, is_method, f) local var = func.variants[i] local p = {} + -- Apparently overloads don't include "self" by default. if is_method then table.insert(p, "self: " .. namespace) end @@ -300,45 +303,14 @@ local function writeFunction(func, namespace, is_method, f) f:write("\n\n") end -local function writeCallbackType(call, f) - local name = call.name - local var = call.variants[1] - - local p = {} - for _, arg in ipairs(var.arguments) do - local param = handleParam(arg.name) - if arg.default then - param = param .. "?" - end - table.insert(p, param .. ": " .. handleType(arg.type, arg.table)) - end - - local returns = {} - for _, ret in ipairs(var.returns) do - table.insert(returns, handleType(ret.type, ret.table)) - end - - writeInfo(call, f) - - f:write("---@alias ") - f:write(name) - f:write("_callback") - f:write(" fun(") - f:write(table.concat(p, ", ")) - f:write(")") - if #returns > 0 then - f:write(": ") - f:write(table.concat(returns, ", ")) - end - f:write("\n\n") -end - +--- Writes an object type by treating it like a @class, handles vector and matrix types. +--- Only does x/y/z/w. No swizzles, no r/g/b/a. local function writeObject(object, f) - local key = object.key local name = object.name - writeComment(object.description, f) - writeLink(key, f) + -- We remove the related field because it is not supported for classes :( + object.related = nil + writeInfo(object, f) --# ---@class Blob f:write("---@class ") @@ -388,6 +360,7 @@ return function(api) local OUTPUT = root .. "/luals/" local API_OUTPUT = OUTPUT .. "library/" + -- Make sure the output folders exist. if lovr.system.getOS() == "Windows" then os.execute("mkdir " .. API_OUTPUT:gsub("/", "\\")) else @@ -425,13 +398,13 @@ return function(api) f:write(is_main_module and ":table\n" or "\n") if is_main_module then + --# ---@field draw draw_callback for _, call in ipairs(api.callbacks) do f:write("---@field ") f:write(call.name) f:write(" ") f:write(call.name) - f:write("_callback") - f:write("\n") + f:write("_callback\n") end end @@ -451,8 +424,10 @@ return function(api) writeFunction(func, key, false, f) end + --# return lovr.audio f:write("return ") f:write(key) + f:close() print("OK") @@ -460,13 +435,44 @@ return function(api) ::continue:: end + -- Write our callback types do local f = assert(io.open(API_OUTPUT .. "callbacks.lua", "w+")) f:write("---@meta\n\n") for _, call in ipairs(api.callbacks) do - local c = writeCallbackType(call, f) + local name = call.name + local var = call.variants[1] + + local p = {} + for _, arg in ipairs(var.arguments) do + local param = handleParam(arg.name) + if arg.default then + param = param .. "?" + end + table.insert(p, param .. ": " .. handleType(arg.type, arg.table)) + end + + local returns = {} + for _, ret in ipairs(var.returns) do + table.insert(returns, handleType(ret.type, ret.table)) + end + + writeInfo(call, f) + + --# ---@alias draw_callback fun(pass: Pass) + f:write("---@alias ") + f:write(name) + f:write("_callback") + f:write(" fun(") + f:write(table.concat(p, ", ")) + f:write(")") + if #returns > 0 then + f:write(": ") + f:write(table.concat(returns, ", ")) + end + f:write("\n\n") end f:close() From d04f24b3a97be1b8b36aa41b5b20a32f5c8d7ce9 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Tue, 30 Dec 2025 22:32:28 -0300 Subject: [PATCH 09/19] Modified to follow the code style of the repository. --- api/generators/luals.lua | 863 ++++++++++++++++++++------------------- 1 file changed, 432 insertions(+), 431 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index 351966a2..7c6fc89a 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -1,515 +1,516 @@ -local DOCS_URL = "https://lovr.org/docs/" +local root = lovr.filesystem.getSource() + +local OUTPUT = root .. '/luals/' +local API_OUTPUT = OUTPUT .. 'library/' +local DOCS_URL = 'https://lovr.org/docs/' local OPERATOR_LOOKUP = { - ["add"] = "add", - ["sub"] = "sub", - ["div"] = "div", - ["mul"] = "mul", - --["equals"] = "eq", - --["length"] = "len", + ['add'] = 'add', + ['sub'] = 'sub', + ['div'] = 'div', + ['mul'] = 'mul', + --['equals'] = 'eq', + --['length'] = 'len', } --- Breaks a multi-line string (such as a description) into a bunch of commented --- lines, takes care of code blocks, input should be valid markdown. --- CONSIDER: Maybe break them even further at periods? Not sure. local function writeComment(cmt, f) - cmt = cmt:gsub('^%s*(.-)%s*$', '%1') -- Remove newlines at the beginning and end - - local code_mode - for line in cmt:gmatch('[^\r\n]+') do - -- WORKAROUND: LuaLS should be able to turn these into code blocks on its own, - -- sadly, LuaLS is stupid! so we got to do this manually. - - -- Ignore empty lines - if not line:match("%S") then - local indented = line:sub(1, 1) == " " - if indented ~= code_mode then - f:write("--- ```lua\n") - end - code_mode = indented - end - - f:write("--- ") - f:write(line) - f:write("\n") + cmt = cmt:gsub('^%s*(.-)%s*$', '%1') -- Remove newlines at the beginning and end + + local code_mode + for line in cmt:gmatch('[^\r\n]+') do + -- WORKAROUND: LuaLS should be able to turn these into code blocks on its own, + -- sadly, LuaLS is stupid! so we got to do this manually. + + -- Ignore empty lines + if not line:match('%S') then + local indented = line:sub(1, 1) == ' ' + if indented ~= code_mode then + f:write('--- ```lua\n') + end + code_mode = indented end + + f:write('--- ') + f:write(line) + f:write('\n') + end end --- Turns a multiline string into a single line string. local function writeSingleLine(cmt, f) - f:write((cmt:gsub("\n", ""))) + f:write((cmt:gsub('\n', ''))) end --- Writes information related to an Object, Function, Enum, etc. --- such as description, notes, examples, documentation links, and related things. local function writeInfo(data, f) - if data.description then - writeComment(data.description, f) - end - - if data.notes then - f:write("---\n") - f:write("--- ## NOTES:\n") - writeComment(data.notes, f) + if data.description then + writeComment(data.description, f) + end + + if data.notes then + f:write('---\n') + f:write('--- ## NOTES:\n') + writeComment(data.notes, f) + end + + if data.examples then + f:write('---\n') + f:write('--- ## EXAMPLES:\n') + for i, example in ipairs(data.examples) do + f:write('--- ### ') + f:write(i) + f:write('.\n') + + if example.description then + writeComment(example.description, f) + end + + if example.code then + f:write('--- ```lua\n') + writeComment(example.code, f) + f:write('--- ```\n') + end end - - if data.examples then - f:write("---\n") - f:write("--- ## EXAMPLES:\n") - for i, example in ipairs(data.examples) do - f:write("--- ### ") - f:write(i) - f:write(".\n") - - if example.description then - writeComment(example.description, f) - end - - if example.code then - f:write("--- ```lua\n") - writeComment(example.code, f) - f:write("--- ```\n") - end - end - end - - if data.key then - f:write("---\n") - f:write("--- [Open in browser](") - f:write(DOCS_URL) - f:write(data.key) - f:write(")\n") - end - - if data.related then - f:write("---\n") - for _, rel in ipairs(data.related) do - f:write("---@see ") - f:write((rel:gsub(":", "."))) - f:write("\n") - end + end + + if data.key then + f:write('---\n') + f:write('--- [Open in browser](') + f:write(DOCS_URL) + f:write(data.key) + f:write(')\n') + end + + if data.related then + f:write('---\n') + for _, rel in ipairs(data.related) do + f:write('---@see ') + f:write((rel:gsub(':', '.'))) + f:write('\n') end + end end ---- Generates an "@alias" directive for fake enums. +--- Generates an '@alias' directive for fake enums. local function writeEnum(enum, f) - -- We don't write full info here, because, sadly, LuaLS will ignore it. - writeComment(enum.description, f) - - --# ---@alias MyEnum - f:write("---@alias ") - f:write(enum.name) - f:write("\n") - - --# --| 'Cool' # Denotes a particular coolness. - for _, value in ipairs(enum.values) do - f:write("---| '\"") - f:write(value.name) - f:write("\"' # ") - writeSingleLine(value.description, f) - f:write("\n") - end - - f:write("\n") + -- We don't write full info here, because, sadly, LuaLS will ignore it. + writeComment(enum.description, f) + + --# ---@alias MyEnum + f:write('---@alias ') + f:write(enum.name) + f:write('\n') + + --# --| 'Cool' # Denotes a particular coolness. + for _, value in ipairs(enum.values) do + f:write('---| \'') + f:write(value.name) + f:write('\' # ') + writeSingleLine(value.description, f) + f:write('\n') + end + + f:write('\n') end -- Utility for translating from the pseudo-syntax used for types in Lövr's API -- to the syntax used for LuaLS annotations. local function handleType(t, tab) - if tab then - local entries = {} - - local arrayable = {} - for _, e in ipairs(tab) do - local from_array = e.name:sub(1, 2) == "[]" - local has_dot = e.name:sub(3, 3) == "." - - -- {[].name: type} -> {[number]: {name: type}} - -- {[][1].name: type} -> {[number]: {[1]: {name: type}}} - if from_array then - e.name = e.name:sub(has_dot and 4 or 3) - table.insert(arrayable, e) - else -- {name: type} -> {name: type} - local opt = e.default and "?" or "" - local n = e.name .. ": " .. handleType(e.type, e.table) .. opt - table.insert(entries, n) - end - end - - if #arrayable > 0 then - table.insert(entries, - "[number]: " .. handleType(nil, arrayable) - ) - end - - return "{" .. table.concat(entries, ", ") .. "}" + if tab then + local entries = {} + + local arrayable = {} + for _, e in ipairs(tab) do + local from_array = e.name:sub(1, 2) == '[]' + local has_dot = e.name:sub(3, 3) == '.' + + -- {[].name: type} -> {[number]: {name: type}} + -- {[][1].name: type} -> {[number]: {[1]: {name: type}}} + if from_array then + e.name = e.name:sub(has_dot and 4 or 3) + table.insert(arrayable, e) + else -- {name: type} -> {name: type} + local opt = e.default and '?' or '' + local n = e.name .. ': ' .. handleType(e.type, e.table) .. opt + table.insert(entries, n) + end end - if t == "*" then -- * -> any - return "any" + if #arrayable > 0 then + table.insert(entries, + '[number]: ' .. handleType(nil, arrayable) + ) end - local a = t:match("{(.*)}") -- {type} -> type[] - if a then - return handleType(a) .. "[]" - end + return '{' .. table.concat(entries, ', ') .. '}' + end - if t:find("|") then -- {A | B} -> (A | B) - local things = {} - for item in t:gmatch("[^|]+") do - -- trim whitespace around each item - item = item:match("^%s*(.-)%s*$") - table.insert(things, handleType(item)) - end + if t == '*' then -- * -> any + return 'any' + end - return "(" .. table.concat(things, " | ") .. ")" + local a = t:match('{(.*)}') -- {type} -> type[] + if a then + return handleType(a) .. '[]' + end + + if t:find('|') then -- {A | B} -> (A | B) + local things = {} + for item in t:gmatch('[^|]+') do + -- trim whitespace around each item + item = item:match('^%s*(.-)%s*$') + table.insert(things, handleType(item)) end - -- A -> A - return t + return '(' .. table.concat(things, ' | ') .. ')' + end + + -- A -> A + return t end -- Assumes that an object with :sub, also has :__sub, for example local function writeOperator(func, f) - local name = OPERATOR_LOOKUP[func.name] - if not name then - return - end - - for i = 1, #func.variants do - local var = func.variants[i] + local name = OPERATOR_LOOKUP[func.name] + if not name then + return + end - if (#var.arguments > 1) then - goto continue - end + for i = 1, #func.variants do + local var = func.variants[i] - local params = {} - for _, arg in ipairs(var.arguments) do - table.insert(params, handleType(arg.type, arg.table)) - end + if (#var.arguments > 1) then + goto continue + end - if (#var.returns > 1) then - goto continue - end + local params = {} + for _, arg in ipairs(var.arguments) do + table.insert(params, handleType(arg.type, arg.table)) + end - local returns = {} - for _, ret in ipairs(var.returns) do - table.insert(returns, handleType(ret.type, ret.table)) - end + if (#var.returns > 1) then + goto continue + end - --# ---@operator add(Vec2): Vec2 - f:write("---@operator ") - f:write(name) - f:write("(") - f:write(table.concat(params, ", ")) - f:write(")") - if #returns > 0 then - f:write(": ") - f:write(table.concat(returns, ", ")) - end - f:write("\n") + local returns = {} + for _, ret in ipairs(var.returns) do + table.insert(returns, handleType(ret.type, ret.table)) + end - ::continue:: + --# ---@operator add(Vec2): Vec2 + f:write('---@operator ') + f:write(name) + f:write('(') + f:write(table.concat(params, ', ')) + f:write(')') + if #returns > 0 then + f:write(': ') + f:write(table.concat(returns, ', ')) end + f:write('\n') + + ::continue:: + end end local function handleParam(name) - if name:sub(1, 3) == "..." then -- ...something -> ... - return "..." - end - return name -- a -> a + if name:sub(1, 3) == '...' then -- ...something -> ... + return '...' + end + return name -- a -> a end --- Writes a function definition with all of its possible variants as --- overloads. local function writeFunction(func, namespace, is_method, f) - local name = func.name - - writeInfo(func, f) - - local params = {} - local first = func.variants[1] - do - for _, arg in ipairs(first.arguments) do - local param = handleParam(arg.name) - local type = handleType(arg.type, arg.table) .. (arg.default and "?" or "") - - f:write("---@param ") - f:write(param) - f:write(" ") - f:write(type) - f:write(" # ") - writeSingleLine(arg.description, f) - if arg.default then - f:write(" (default: ") - f:write(arg.default) - f:write(")") - end - f:write("\n") - table.insert(params, param) - end - - for _, ret in ipairs(first.returns) do - f:write("---@return ") - f:write(handleType(ret.type, ret.table)) - if ret.description then - f:write(" # ") - writeSingleLine(ret.description, f) - end - f:write("\n") - end + local name = func.name + + writeInfo(func, f) + + local params = {} + local first = func.variants[1] + do + for _, arg in ipairs(first.arguments) do + local param = handleParam(arg.name) + local type = handleType(arg.type, arg.table) .. (arg.default and '?' or '') + + f:write('---@param ') + f:write(param) + f:write(' ') + f:write(type) + f:write(' # ') + writeSingleLine(arg.description, f) + if arg.default then + f:write(' (default: ') + f:write(arg.default) + f:write(')') + end + f:write('\n') + table.insert(params, param) end - for i = 2, #func.variants do - local var = func.variants[i] - - local p = {} - -- Apparently overloads don't include "self" by default. - if is_method then - table.insert(p, "self: " .. namespace) - end + for _, ret in ipairs(first.returns) do + f:write('---@return ') + f:write(handleType(ret.type, ret.table)) + if ret.description then + f:write(' # ') + writeSingleLine(ret.description, f) + end + f:write('\n') + end + end - for _, arg in ipairs(var.arguments) do - local param = handleParam(arg.name) - if arg.default then - param = param .. "?" - end - table.insert(p, param .. ": " .. handleType(arg.type, arg.table)) - end + for i = 2, #func.variants do + local var = func.variants[i] - local returns = {} - for _, ret in ipairs(var.returns) do - table.insert(returns, handleType(ret.type, ret.table)) - end + local p = {} + -- Apparently overloads don't include 'self' by default. + if is_method then + table.insert(p, 'self: ' .. namespace) + end - f:write("---@overload fun(") - f:write(table.concat(p, ", ")) - f:write(")") - if #returns > 0 then - f:write(": ") - f:write(table.concat(returns, ", ")) - end - f:write("\n") + for _, arg in ipairs(var.arguments) do + local param = handleParam(arg.name) + if arg.default then + param = param .. '?' + end + table.insert(p, param .. ': ' .. handleType(arg.type, arg.table)) end - f:write("function ") - f:write(namespace) - f:write(is_method and ":" or ".") - f:write(name) - f:write("(") - f:write(table.concat(params, ", ")) - f:write(") end") + local returns = {} + for _, ret in ipairs(var.returns) do + table.insert(returns, handleType(ret.type, ret.table)) + end - f:write("\n\n") + f:write('---@overload fun(') + f:write(table.concat(p, ', ')) + f:write(')') + if #returns > 0 then + f:write(': ') + f:write(table.concat(returns, ', ')) + end + f:write('\n') + end + + f:write('function ') + f:write(namespace) + f:write(is_method and ':' or '.') + f:write(name) + f:write('(') + f:write(table.concat(params, ', ')) + f:write(') end') + + f:write('\n\n') end --- Writes an object type by treating it like a @class, handles vector and matrix types. --- Only does x/y/z/w. No swizzles, no r/g/b/a. local function writeObject(object, f) - local name = object.name - - -- We remove the related field because it is not supported for classes :( - object.related = nil - writeInfo(object, f) - - --# ---@class Blob - f:write("---@class ") - f:write(name) - if name == "Mat4" then - f:write(": number[]") - end - f:write("\n") - - local vec_n = tonumber(name:sub(4)) - if vec_n and name:sub(1, 3) == "Vec" then - local c = "xyzw" - for i = 1, vec_n do - f:write("---@field ") - f:write(c:sub(i, i)) - f:write(" number\n") - end - end - - --if object.constructors then - -- for _, const in ipairs(object.constructors) do - -- f:write("---@see ") - -- f:write(const) - -- f:write(" # (Constructor)\n") - -- end - --end - - for _, func in ipairs(object.methods) do - writeOperator(func, f) + local name = object.name + + -- We remove the related field because it is not supported for classes :( + object.related = nil + writeInfo(object, f) + + --# ---@class Blob + f:write('---@class ') + f:write(name) + if name == 'Mat4' then + f:write(': number[]') + end + f:write('\n') + + local vec_n = tonumber(name:sub(4)) + if vec_n and name:sub(1, 3) == 'Vec' then + local c = 'xyzw' + for i = 1, vec_n do + f:write('---@field ') + f:write(c:sub(i, i)) + f:write(' number\n') end - - --# local Blob = {} - f:write("local ") - f:write(name) - f:write(" = {}\n\n") - - for _, func in ipairs(object.methods) do - writeFunction(func, name, true, f) - end - - return name + end + + --if object.constructors then + -- for _, const in ipairs(object.constructors) do + -- f:write('---@see ') + -- f:write(const) + -- f:write(' # (Constructor)\n') + -- end + --end + + for _, func in ipairs(object.methods) do + writeOperator(func, f) + end + + --# local Blob = {} + f:write('local ') + f:write(name) + f:write(' = {}\n\n') + + for _, func in ipairs(object.methods) do + writeFunction(func, name, true, f) + end + + return name end -return function(api) - local root = lovr.filesystem.getSource() - - local OUTPUT = root .. "/luals/" - local API_OUTPUT = OUTPUT .. "library/" - - -- Make sure the output folders exist. - if lovr.system.getOS() == "Windows" then - os.execute("mkdir " .. API_OUTPUT:gsub("/", "\\")) - else - os.execute("mkdir -p " .. API_OUTPUT) +local function processModule(module, callbacks) + local key = module.key + local name = module.name + local is_main_module = key == 'lovr' + + io.write(('- %-20s'):format(key .. '...')) + + -- We skip external modules as they do not have any meaningful annotations. + -- Plus, they often have their own, separately mantained annotations instead. + if module.external then + print('SKIP') + return + end + + local f = io.open(API_OUTPUT .. name .. '.lua', 'w+') + assert(f, 'Could not open file, make sure you got permissions!') + + --# ---@meta lovr.audio + f:write('---@meta ') + f:write(key) + f:write('\n\n') + + writeInfo(module, f) + + --# ---@class lovr.audio + f:write('---@class ') + f:write(key) + f:write(is_main_module and ':table\n' or '\n') + + if is_main_module then + --# ---@field draw draw_callback + for _, call in ipairs(callbacks) do + f:write('---@field ') + f:write(call.name) + f:write(' ') + f:write(call.name) + f:write('_callback\n') end + end - print("Processing modules") - for _, module in ipairs(api.modules) do - local key = module.key - local name = module.name - local is_main_module = key == "lovr" - - io.write(("- %-20s"):format(key .. "...")) - - -- We skip external modules as they do not have any meaningful annotations. - -- Plus, they often have their own, separately mantained annotations instead. - if module.external then - print("SKIP") - goto continue - end - - local f = io.open(API_OUTPUT .. name .. ".lua", "w+") - assert(f, "Could not open file, make sure you got permissions!") - - --# ---@meta lovr.audio - f:write("---@meta ") - f:write(key) - f:write("\n\n") - - writeInfo(module, f) - - --# ---@class lovr.audio - f:write("---@class ") - f:write(key) - f:write(is_main_module and ":table\n" or "\n") - - if is_main_module then - --# ---@field draw draw_callback - for _, call in ipairs(api.callbacks) do - f:write("---@field ") - f:write(call.name) - f:write(" ") - f:write(call.name) - f:write("_callback\n") - end - end - - --# lovr.audio = {} - f:write(key) - f:write(" = {}\n\n") - - for _, enum in ipairs(module.enums) do - writeEnum(enum, f) - end + --# lovr.audio = {} + f:write(key) + f:write(' = {}\n\n') - for _, objt in ipairs(module.objects) do - writeObject(objt, f) - end + for _, enum in ipairs(module.enums) do + writeEnum(enum, f) + end - for _, func in ipairs(module.functions) do - writeFunction(func, key, false, f) - end + for _, objt in ipairs(module.objects) do + writeObject(objt, f) + end - --# return lovr.audio - f:write("return ") - f:write(key) + for _, func in ipairs(module.functions) do + writeFunction(func, key, false, f) + end - f:close() + --# return lovr.audio + f:write('return ') + f:write(key) - print("OK") + f:close() - ::continue:: - end + print('OK') +end - -- Write our callback types - do - local f = assert(io.open(API_OUTPUT .. "callbacks.lua", "w+")) - - f:write("---@meta\n\n") - - for _, call in ipairs(api.callbacks) do - local name = call.name - local var = call.variants[1] - - local p = {} - for _, arg in ipairs(var.arguments) do - local param = handleParam(arg.name) - if arg.default then - param = param .. "?" - end - table.insert(p, param .. ": " .. handleType(arg.type, arg.table)) - end - - local returns = {} - for _, ret in ipairs(var.returns) do - table.insert(returns, handleType(ret.type, ret.table)) - end - - writeInfo(call, f) - - --# ---@alias draw_callback fun(pass: Pass) - f:write("---@alias ") - f:write(name) - f:write("_callback") - f:write(" fun(") - f:write(table.concat(p, ", ")) - f:write(")") - if #returns > 0 then - f:write(": ") - f:write(table.concat(returns, ", ")) - end - f:write("\n\n") +return function(api) + -- Make sure the output folders exist. + if lovr.system.getOS() == 'Windows' then + os.execute('mkdir ' .. API_OUTPUT:gsub('/', '\\')) + else + os.execute('mkdir -p ' .. API_OUTPUT) + end + + print('Processing modules') + for _, module in ipairs(api.modules) do + processModule(module, api.callbacks) + end + + -- Write our callback types + do + local f = assert(io.open(API_OUTPUT .. 'callbacks.lua', 'w+')) + + f:write('---@meta\n\n') + + for _, call in ipairs(api.callbacks) do + local name = call.name + local var = call.variants[1] + + local p = {} + for _, arg in ipairs(var.arguments) do + local param = handleParam(arg.name) + if arg.default then + param = param .. '?' end - - f:close() + table.insert(p, param .. ': ' .. handleType(arg.type, arg.table)) + end + + local returns = {} + for _, ret in ipairs(var.returns) do + table.insert(returns, handleType(ret.type, ret.table)) + end + + writeInfo(call, f) + + --# ---@alias draw_callback fun(pass: Pass) + f:write('---@alias ') + f:write(name) + f:write('_callback') + f:write(' fun(') + f:write(table.concat(p, ', ')) + f:write(')') + if #returns > 0 then + f:write(': ') + f:write(table.concat(returns, ', ')) + end + f:write('\n\n') end - -- Write our globals - do - local f = assert(io.open(API_OUTPUT .. "globals.lua", "w+")) - - f:write("---@meta\n\n") - f:write("vec2 = lovr.math.vec2\n") - f:write("Vec2 = lovr.math.newVec2\n") - f:write("vec3 = lovr.math.vec3\n") - f:write("Vec3 = lovr.math.newVec3\n") - f:write("vec4 = lovr.math.vec4\n") - f:write("Vec4 = lovr.math.newVec4\n") - f:write("mat4 = lovr.math.mat4\n") - f:write("Mat4 = lovr.math.newMat4\n") - f:write("quat = lovr.math.quat\n") - f:write("Quat = lovr.math.newQuat\n") - - f:close() - end - - -- Write our manifest :) - do - local f = assert(io.open(OUTPUT .. "config.json", "w+")) - - f:write('{\n') - f:write(' "name": "LÖVR",\n') - f:write(' "words": ["lovr%.%w+"],\n') - f:write(' "settings": {\n') - f:write(' "Lua.runtime.version" : "LuaJIT"\n') - f:write(' }\n') - f:write('}\n') - f:close() - - print("Manifest written successfully.") - end + f:close() + end + + -- Write our globals + do + local f = assert(io.open(API_OUTPUT .. 'globals.lua', 'w+')) + + f:write('---@meta\n\n') + f:write('vec2 = lovr.math.vec2\n') + f:write('Vec2 = lovr.math.newVec2\n') + f:write('vec3 = lovr.math.vec3\n') + f:write('Vec3 = lovr.math.newVec3\n') + f:write('vec4 = lovr.math.vec4\n') + f:write('Vec4 = lovr.math.newVec4\n') + f:write('mat4 = lovr.math.mat4\n') + f:write('Mat4 = lovr.math.newMat4\n') + f:write('quat = lovr.math.quat\n') + f:write('Quat = lovr.math.newQuat\n') + + f:close() + end + + -- Write our manifest :) + do + local f = assert(io.open(OUTPUT .. 'config.json', 'w+')) + + f:write('{\n') + f:write(' "name": "LÖVR",\n') + f:write(' "words": ["lovr%.%w+"],\n') + f:write(' "settings": {\n') + f:write(' "Lua.runtime.version" : "LuaJIT"\n') + f:write(' }\n') + f:write('}\n') + f:close() + + print('Manifest written successfully.') + end end From 6b1fb5c50466cc064ddec82cebba5801f788fc51 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Tue, 30 Dec 2025 22:36:24 -0300 Subject: [PATCH 10/19] Oops, minor indentation issue --- api/generators/luals.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index 7c6fc89a..d4d1f081 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -78,8 +78,8 @@ local function writeInfo(data, f) end if data.key then - f:write('---\n') - f:write('--- [Open in browser](') + f:write('---\n') + f:write('--- [Open in browser](') f:write(DOCS_URL) f:write(data.key) f:write(')\n') From a5325f2758e72ed8d88ac8af4c47c05078cfba1c Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Tue, 30 Dec 2025 22:41:30 -0300 Subject: [PATCH 11/19] My code editor has failed me. --- api/generators/luals.lua | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index d4d1f081..ddcb3294 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -53,7 +53,7 @@ local function writeInfo(data, f) if data.notes then f:write('---\n') - f:write('--- ## NOTES:\n') + f:write('--- ## NOTES:\n') writeComment(data.notes, f) end @@ -334,14 +334,6 @@ local function writeObject(object, f) end end - --if object.constructors then - -- for _, const in ipairs(object.constructors) do - -- f:write('---@see ') - -- f:write(const) - -- f:write(' # (Constructor)\n') - -- end - --end - for _, func in ipairs(object.methods) do writeOperator(func, f) end From cc5c73afd195ed7078438710886178bdd36e7b7e Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Wed, 31 Dec 2025 10:36:03 -0300 Subject: [PATCH 12/19] Made every object inherit from Object. --- api/generators/luals.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index ddcb3294..591c6328 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -321,6 +321,8 @@ local function writeObject(object, f) f:write(name) if name == 'Mat4' then f:write(': number[]') + elseif name ~= 'Object' then + f:write(': Object') end f:write('\n') From c561744ce955b396ed102397d828474e95b59794 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:44:37 -0300 Subject: [PATCH 13/19] Fixed Mat4. --- api/generators/luals.lua | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index 591c6328..87ae1c17 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -316,15 +316,18 @@ local function writeObject(object, f) object.related = nil writeInfo(object, f) + -- if object.constructors then + -- for _, const in ipairs(object.constructors) do + -- f:write('---@see ') + -- f:write(const) + -- f:write(' # (Constructor)\n') + -- end + -- end + --# ---@class Blob f:write('---@class ') f:write(name) - if name == 'Mat4' then - f:write(': number[]') - elseif name ~= 'Object' then - f:write(': Object') - end - f:write('\n') + f:write(': Object\n') local vec_n = tonumber(name:sub(4)) if vec_n and name:sub(1, 3) == 'Vec' then @@ -336,6 +339,10 @@ local function writeObject(object, f) end end + if name == 'Mat4' then + f:write('---@field [number] number\n') + end + for _, func in ipairs(object.methods) do writeOperator(func, f) end From 5fcf5c85dd0a47752d91ea1abb4359b0c23a3df8 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:01:30 -0300 Subject: [PATCH 14/19] Minor fix, to solve circular dependency on Object --- api/generators/luals.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index 87ae1c17..bef62beb 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -327,7 +327,7 @@ local function writeObject(object, f) --# ---@class Blob f:write('---@class ') f:write(name) - f:write(': Object\n') + f:write(name == 'Object' and '\n' or ': Object\n') local vec_n = tonumber(name:sub(4)) if vec_n and name:sub(1, 3) == 'Vec' then From 1041339067fefddf12585cea15e8a5697b359c5c Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:09:47 -0300 Subject: [PATCH 15/19] Fixed oversight where enums would have special, unescaped characters for some of its values. --- api/generators/luals.lua | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index bef62beb..19f60354 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -105,10 +105,16 @@ local function writeEnum(enum, f) f:write(enum.name) f:write('\n') + -- We want to escape some stuff here + local special = { + ['\\'] = '\\\\', + ['\''] = '\\\'', + } + --# --| 'Cool' # Denotes a particular coolness. for _, value in ipairs(enum.values) do f:write('---| \'') - f:write(value.name) + f:write(special[value.name] or value.name) f:write('\' # ') writeSingleLine(value.description, f) f:write('\n') @@ -316,18 +322,18 @@ local function writeObject(object, f) object.related = nil writeInfo(object, f) - -- if object.constructors then - -- for _, const in ipairs(object.constructors) do - -- f:write('---@see ') - -- f:write(const) - -- f:write(' # (Constructor)\n') - -- end - -- end + if object.constructors then + for _, const in ipairs(object.constructors) do + f:write('---@see ') + f:write(const) + f:write(' # (Constructor)\n') + end + end --# ---@class Blob f:write('---@class ') f:write(name) - f:write(name == 'Object' and '\n' or ': Object\n') + f:write(': Object\n') local vec_n = tonumber(name:sub(4)) if vec_n and name:sub(1, 3) == 'Vec' then From ecf06a87e63ae0834ed652000265acf972d36607 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:12:29 -0300 Subject: [PATCH 16/19] Fixed a very dumb regression --- api/generators/luals.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index 19f60354..90e533c1 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -333,7 +333,7 @@ local function writeObject(object, f) --# ---@class Blob f:write('---@class ') f:write(name) - f:write(': Object\n') + f:write(name == 'Object' and '\n' or ': Object\n') local vec_n = tonumber(name:sub(4)) if vec_n and name:sub(1, 3) == 'Vec' then From 08e772c37661da136b213b83056f341f7a149e5c Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:38:30 -0300 Subject: [PATCH 17/19] Improved readability of output code, improved readability of regex expressions. --- api/generators/luals.lua | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index 90e533c1..40eaf08b 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -33,6 +33,8 @@ local function writeComment(cmt, f) code_mode = indented end + line = line:gsub(' +', ' ') -- Removes extra spaces + f:write('--- ') f:write(line) f:write('\n') @@ -40,8 +42,12 @@ local function writeComment(cmt, f) end --- Turns a multiline string into a single line string. +--- Removes extra space. local function writeSingleLine(cmt, f) - f:write((cmt:gsub('\n', ''))) + cmt = cmt:gsub('\n', '') -- Removes newlines + cmt = cmt:gsub(' +', ' ') -- Removes extra space. + + f:write(cmt) end --- Writes information related to an Object, Function, Enum, etc. @@ -88,11 +94,15 @@ local function writeInfo(data, f) if data.related then f:write('---\n') for _, rel in ipairs(data.related) do + rel = rel:gsub(':', '.') -- Replace ":" with ".", as per LuaLS convention. f:write('---@see ') - f:write((rel:gsub(':', '.'))) + f:write(rel) f:write('\n') end end + + -- Add some space! + f:write('---\n') end --- Generates an '@alias' directive for fake enums. @@ -111,11 +121,18 @@ local function writeEnum(enum, f) ['\''] = '\\\'', } + local size = 0 + for _, value in ipairs(enum.values) do + size = math.max(size, #value.name) + end + + local fmt = '---| %-' .. (size + 5) .. 's # ' + --# --| 'Cool' # Denotes a particular coolness. for _, value in ipairs(enum.values) do - f:write('---| \'') - f:write(special[value.name] or value.name) - f:write('\' # ') + local n = special[value.name] or value.name + f:write(fmt:format('\'' .. n .. '\'')) + writeSingleLine(value.description, f) f:write('\n') end @@ -251,9 +268,9 @@ local function writeFunction(func, namespace, is_method, f) f:write(' # ') writeSingleLine(arg.description, f) if arg.default then - f:write(' (default: ') + f:write(' (default: `') f:write(arg.default) - f:write(')') + f:write('`)') end f:write('\n') table.insert(params, param) From 35aa15f69207dfe7e9802ebf7934ecbcaedc5018 Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Wed, 31 Dec 2025 18:03:17 -0300 Subject: [PATCH 18/19] Removed extra space removal feature because it would mess up with the code. Might revisit it eventually. --- api/generators/luals.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index 40eaf08b..fcc51a23 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -33,8 +33,6 @@ local function writeComment(cmt, f) code_mode = indented end - line = line:gsub(' +', ' ') -- Removes extra spaces - f:write('--- ') f:write(line) f:write('\n') From c6028e780bf0238026ccb2aa11aa3f065f70cd8e Mon Sep 17 00:00:00 2001 From: Nelson Lopez <38925212+darltrash@users.noreply.github.com> Date: Thu, 1 Jan 2026 11:57:05 -0300 Subject: [PATCH 19/19] Now respects the .extends field. --- api/generators/luals.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/generators/luals.lua b/api/generators/luals.lua index fcc51a23..b41c9081 100644 --- a/api/generators/luals.lua +++ b/api/generators/luals.lua @@ -348,7 +348,11 @@ local function writeObject(object, f) --# ---@class Blob f:write('---@class ') f:write(name) - f:write(name == 'Object' and '\n' or ': Object\n') + if object.extends then + f:write(": ") + f:write(object.extends) + end + f:write("\n") local vec_n = tonumber(name:sub(4)) if vec_n and name:sub(1, 3) == 'Vec' then