cats.lua 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. -- We're disabling `@see` tags for the time being because they
  2. -- ended up confusing the language server a little, breaking
  3. -- all objects from `lovr.math.*`.
  4. local ENABLE_SEE_TAGS = false
  5. -- Whether to generate an extra file with the globals
  6. -- from the `lovr.math` module.
  7. local INCLUDE_GLOBALS = true
  8. local root = lovr.filesystem.getSource()
  9. local function getPath(filename, sep)
  10. sep = sep or "/"
  11. return filename:match("(.*" .. sep .. ")")
  12. end
  13. local function ensureDirectoryExists(path)
  14. os.execute("mkdir -p " .. path)
  15. end
  16. --- Writes `contents` into a file identified by `filename`.
  17. local function writeFile(filename, contents)
  18. local fullpath = ('%s/cats/%s'):format(root, filename)
  19. -- make sure the directory exists
  20. local path = getPath(fullpath)
  21. if path then
  22. ensureDirectoryExists(path)
  23. end
  24. local file = io.open(fullpath, "w")
  25. if not file then
  26. print("Failed to open file for writing: " .. fullpath)
  27. return
  28. end
  29. file:write(contents)
  30. file:write("\n")
  31. file:close()
  32. end
  33. --- Adds a single value to a table.
  34. local function add(t, v)
  35. t[#t + 1] = v
  36. end
  37. -- Shorthand to concatenate a table of strings with a delimiter.
  38. local function join(t, delimiter)
  39. return table.concat(t, delimiter)
  40. end
  41. -- Shorthand to map a table with a function.
  42. local function map(t, fn)
  43. local out = {}
  44. for i, v in ipairs(t) do
  45. out[i] = fn(v)
  46. end
  47. return out
  48. end
  49. --- Returns true if any element in the table `t` satisfies the predicate `fn`.
  50. local function any(t, fn)
  51. for _, v in ipairs(t) do
  52. if fn(v) then
  53. return true
  54. end
  55. end
  56. return false
  57. end
  58. --- Renders the given string as a docstring. If the string
  59. --- contains newlines, they are prefixed with `---`.
  60. local function doc(str)
  61. str = str or ""
  62. return "---" .. str:gsub("\n", "\n---"):gsub('%s*$', '')
  63. end
  64. --- Determines if a parameter is optional. If it is, returns a `?` suffix.
  65. --- If it's not, returns an empty string.
  66. local function optionalSuffix(param)
  67. if param.type ~= 'table' then
  68. return (param.default ~= nil) and '?' or ''
  69. elseif not param.table then
  70. return ''
  71. elseif any(param.table, function(field) return field.default == nil end) then
  72. return ''
  73. else
  74. return '?'
  75. end
  76. end
  77. local function docHeader(str)
  78. return doc("#### " .. str .. "\n")
  79. end
  80. --- Render `@see` tags for a definition.
  81. local function seeTags(out, def)
  82. if not ENABLE_SEE_TAGS then
  83. return
  84. end
  85. if def.related then
  86. for _, related in ipairs(def.related) do
  87. add(out, doc("@see " .. related))
  88. end
  89. end
  90. end
  91. -- Forward declaration for `argumentType`.
  92. local argumentType
  93. --- Returns the type of a table argument.
  94. local function tableType(arg)
  95. local out = {}
  96. for _, field in ipairs(arg.table) do
  97. add(out, ("%s: %s"):format(field.name, argumentType(field)))
  98. end
  99. return ("{%s}"):format(join(out, ", "))
  100. end
  101. --- Returns the type of a function argument.
  102. function argumentType(arg)
  103. if arg.type == "*" then
  104. return "any"
  105. elseif arg.type == 'table' and arg.table then
  106. return tableType(arg)
  107. else
  108. return arg.type
  109. end
  110. end
  111. --- Returns the name of a function argument.
  112. function argumentName(arg)
  113. if arg.name:sub(1, 3) == '...' then
  114. return '...'
  115. else
  116. return arg.name
  117. end
  118. end
  119. --- Renders the header (description, notes, examples, etc.) for anything that's like a function.
  120. local function renderFunctionHeader(out, func)
  121. add(out, "")
  122. add(out, doc(func.description))
  123. if (func.notes) then
  124. add(out, doc(""))
  125. add(out, docHeader("Notes:"))
  126. add(out, doc(func.notes))
  127. end
  128. if (func.examples) then
  129. for _, example in ipairs(func.examples) do
  130. add(out, doc(""))
  131. add(out, docHeader("Example:"))
  132. if example.description then
  133. add(out, doc(example.description))
  134. add(out, doc(""))
  135. end
  136. if example.code then
  137. add(out, doc("```lua"))
  138. add(out, doc(example.code))
  139. add(out, doc("```"))
  140. end
  141. end
  142. end
  143. add(out, doc(""))
  144. seeTags(out, func)
  145. end
  146. local function renderType(out, tag, variant)
  147. local returns
  148. if #variant.returns > 0 then
  149. returns = join(map(variant.returns, function(ret)
  150. return ret.type
  151. end), ", ")
  152. else
  153. returns = "nil"
  154. end
  155. local params = join(map(variant.arguments, function(arg)
  156. return ("%s%s: %s"):format(argumentName(arg), optionalSuffix(arg), argumentType(arg))
  157. end), ", ")
  158. add(out, doc(("%s fun(%s): %s"):format(tag, params, returns)))
  159. end
  160. local function renderFunctionVariant(out, func, variant)
  161. renderFunctionHeader(out, func)
  162. -- Document parameters
  163. if variant.arguments then
  164. for _, arg in ipairs(variant.arguments) do
  165. add(out, doc(("@param %s%s %s %s"):format(argumentName(arg), optionalSuffix(arg), argumentType(arg), arg.description)))
  166. end
  167. end
  168. -- Document return type
  169. if variant.returns then
  170. for _, ret in ipairs(variant.returns) do
  171. add(out, doc(("@return %s %s %s"):format(ret.type, ret.name, ret.description)))
  172. end
  173. end
  174. -- Build function signature
  175. local signature = join(map(variant.arguments, function(arg)
  176. return argumentName(arg)
  177. end), ", ")
  178. add(out, ("function %s(%s) end"):format(func.key, signature))
  179. end
  180. local function renderFunction(out, func)
  181. for _, variant in ipairs(func.variants) do
  182. renderFunctionVariant(out, func, variant)
  183. end
  184. end
  185. local function renderEnum(out, enum)
  186. add(out, "")
  187. add(out, doc(enum.description))
  188. add(out, doc("@alias " .. enum.key))
  189. for _, value in ipairs(enum.values) do
  190. add(out, doc(value.description))
  191. add(out, doc("| \"%s\""):format(value.name))
  192. end
  193. end
  194. local function generateSwizzlePermutations(fields, length)
  195. local permutations = {}
  196. local function go(swizzle)
  197. swizzle = swizzle or ""
  198. if #swizzle == length then
  199. add(permutations, swizzle)
  200. else
  201. for _, field in ipairs(fields) do
  202. go(swizzle .. field)
  203. end
  204. end
  205. end
  206. go()
  207. return permutations
  208. end
  209. local function renderSwizzleFields(out, fields)
  210. for i = 1, 4 do
  211. local type = i == 1 and "number" or "Vec" .. i
  212. local comp = i == 1 and "component" or "components"
  213. for _, swizzle in ipairs(generateSwizzlePermutations(fields, i)) do
  214. local desc = ("The %s %s of the vector."):format(swizzle, comp)
  215. add(out, doc(("@field %s %s %s"):format(swizzle, type, desc)))
  216. end
  217. end
  218. end
  219. --- Renders a module's information to CATS format.
  220. local function renderModule(mod)
  221. local out = {}
  222. add(out, doc("@meta"))
  223. add(out, "")
  224. if mod.description then
  225. add(out, doc(mod.description))
  226. add(out, doc(""))
  227. end
  228. add(out, doc(("@class %s"):format(mod.key)))
  229. add(out, ("%s = {}"):format(mod.key))
  230. -- Render functions
  231. for _, func in ipairs(mod.functions) do
  232. renderFunction(out, func)
  233. end
  234. -- Render objects
  235. for _, obj in ipairs(mod.objects) do
  236. add(out, "")
  237. add(out, doc(obj.description))
  238. add(out, doc("@class %s"):format(obj.key))
  239. -- fields
  240. if obj.fields then
  241. for _, field in ipairs(obj.fields) do
  242. add(out, doc(("@field %s %s %s"):format(field.name, field.type, field.description)))
  243. end
  244. end
  245. -- swizzles
  246. if obj.swizzles then
  247. for _, swizzle in ipairs(obj.swizzles.components) do
  248. renderSwizzleFields(out, swizzle)
  249. end
  250. end
  251. -- see tags
  252. seeTags(out, obj)
  253. -- definition
  254. add(out, ("local %s = {}"):format(obj.name))
  255. -- Render object methods
  256. for _, func in ipairs(obj.methods) do
  257. renderFunction(out, func)
  258. end
  259. end
  260. -- Render enums
  261. for _, enum in ipairs(mod.enums) do
  262. renderEnum(out, enum)
  263. end
  264. return join(out, "\n")
  265. end
  266. local function generateModuleDocumentation(api)
  267. for _, v in ipairs(api.modules) do
  268. local text = renderModule(v)
  269. local filename = v.name
  270. writeFile(("library/%s.lua"):format(filename), text)
  271. end
  272. end
  273. local function renderCallback(out, callback)
  274. renderFunctionHeader(out, callback)
  275. for i = 1, #callback.variants do
  276. local variant = callback.variants[i]
  277. renderType(out, "@type", variant)
  278. end
  279. add(out, ("%s = nil"):format(callback.key))
  280. end
  281. local function generateCallbackDocumentation(api)
  282. local out = {}
  283. add(out, doc("@meta"))
  284. for _, callback in ipairs(api.callbacks) do
  285. renderCallback(out, callback)
  286. end
  287. writeFile("library/callback.lua", join(out, "\n"))
  288. end
  289. local function generateAddonConfig()
  290. local out = {}
  291. add(out, '{')
  292. add(out, ' "name": "LÖVR",')
  293. add(out, ' "words": ["lovr%.%w+"],')
  294. add(out, ' "settings": {')
  295. add(out, ' "Lua.runtime.version": "LuaJIT",')
  296. add(out, ' "Lua.diagnostics.globals": ["lovr"]')
  297. add(out, ' }')
  298. add(out, '}')
  299. writeFile("config.json", join(out, "\n"))
  300. end
  301. local function generateGlobalsDocumentation()
  302. local out = {}
  303. add(out, doc("@meta"))
  304. add(out, "vec2 = lovr.math.vec2")
  305. add(out, "Vec2 = lovr.math.newVec2")
  306. add(out, "vec3 = lovr.math.vec3")
  307. add(out, "Vec3 = lovr.math.newVec3")
  308. add(out, "vec4 = lovr.math.vec4")
  309. add(out, "Vec4 = lovr.math.newVec4")
  310. add(out, "mat4 = lovr.math.mat4")
  311. add(out, "Mat4 = lovr.math.newMat4")
  312. add(out, "quat = lovr.math.quat")
  313. add(out, "Quat = lovr.math.newQuat")
  314. writeFile("library/globals.lua", join(out, "\n"))
  315. end
  316. return function(api)
  317. generateModuleDocumentation(api)
  318. generateCallbackDocumentation(api)
  319. generateAddonConfig()
  320. if INCLUDE_GLOBALS then
  321. generateGlobalsDocumentation()
  322. end
  323. end