cats.lua 9.2 KB

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