main.lua 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. local serpent = require 'serpent'
  2. -- Helpers
  3. local function copy(t)
  4. if type(t) ~= 'table' then return t end
  5. local result = {}
  6. for k, v in pairs(t) do
  7. result[k] = copy(v)
  8. end
  9. return result
  10. end
  11. local function unindent(code)
  12. local indent = code:match('^(% +)')
  13. if indent then
  14. return code:gsub('\n' .. indent, '\n'):gsub('^' .. indent, ''):gsub('%s*$', '')
  15. else
  16. return code
  17. end
  18. end
  19. local function unwrap(str)
  20. if not str then return str end
  21. str = unindent(str)
  22. return str:gsub('([^\n])\n(%S)', function(a, b)
  23. if b == '-' or b == '>' then
  24. return a .. '\n' .. b
  25. else
  26. return a .. ' ' .. b
  27. end
  28. end)
  29. :gsub('^%s+', '')
  30. :gsub('%s+$', '')
  31. end
  32. local function pluralify(t, key)
  33. t[key .. 's'] = t[key .. 's'] or (t[key] and { t[key] } or nil)
  34. t[key] = nil
  35. return t[key .. 's']
  36. end
  37. local lookup = {}
  38. local function track(obj)
  39. lookup[obj.key] = obj
  40. end
  41. local function warnIf(cond, s, ...)
  42. if cond then print(string.format(s, ...)) end
  43. end
  44. -- Processors
  45. local function processExample(example)
  46. if type(example) == 'string' then
  47. return {
  48. code = unindent(example)
  49. }
  50. else
  51. example.description = unwrap(example.description)
  52. example.code = unindent(example.code)
  53. end
  54. return example
  55. end
  56. local function processEnum(path, parent)
  57. local enum = require(path)
  58. enum.name = path:match('[^/]+$')
  59. enum.key = enum.name
  60. enum.module = parent.key
  61. enum.description = unwrap(enum.description)
  62. enum.notes = unwrap(enum.notes)
  63. for _, value in ipairs(enum.values) do
  64. value.description = unwrap(value.description)
  65. end
  66. track(enum)
  67. return enum
  68. end
  69. local function processFunction(path, parent)
  70. local fn = require(path)
  71. fn.name = path:match('[^/]+$')
  72. fn.key = parent.name:match('^[A-Z]') and (parent.key .. ':' .. fn.name) or (path:gsub('/', '.'):gsub('callbacks%.', ''))
  73. fn.description = unwrap(fn.description)
  74. fn.module = parent.module or parent.key
  75. fn.notes = unwrap(fn.notes)
  76. fn.examples = pluralify(fn, 'example')
  77. for k, example in ipairs(fn.examples or {}) do
  78. fn.examples[k] = processExample(example)
  79. end
  80. if not fn.variants then
  81. local missingVariants = (not fn.arguments[1] and next(fn.arguments)) or (not fn.returns[1] and next(fn.returns))
  82. warnIf(missingVariants, 'Function %q is missing variants', fn.key)
  83. fn.variants = {
  84. {
  85. arguments = fn.arguments,
  86. returns = fn.returns
  87. }
  88. }
  89. else
  90. assert(fn.arguments, string.format('Function %q with variants does not have arguments list', fn.key))
  91. for name, arg in pairs(fn.arguments) do
  92. arg.name = name
  93. end
  94. assert(fn.returns, string.format('Function %q with variants does not have returns list', fn.key))
  95. for name, ret in pairs(fn.returns) do
  96. ret.name = name
  97. end
  98. for _, variant in ipairs(fn.variants) do
  99. for i, name in ipairs(variant.arguments) do
  100. warnIf(not fn.arguments[name], string.format('Function %q variant argument %q does not exist', fn.key, name))
  101. variant.arguments[i] = copy(fn.arguments[name])
  102. end
  103. for i, name in ipairs(variant.returns) do
  104. warnIf(not fn.returns[name], string.format('Function %q variant return %q does not exist', fn.key, name))
  105. variant.returns[i] = copy(fn.returns[name])
  106. end
  107. end
  108. end
  109. for _, variant in ipairs(fn.variants) do
  110. local function processTable(t)
  111. if not t then return end
  112. for _, field in ipairs(t) do
  113. field.description = unwrap(field.description)
  114. processTable(field.table)
  115. end
  116. t.description = unwrap(t.description)
  117. end
  118. variant.description = unwrap(variant.description)
  119. assert(variant.arguments, string.format('Variant for %q is missing arguments', fn.key))
  120. for _, arg in ipairs(variant.arguments) do
  121. arg.description = unwrap(arg.description)
  122. processTable(arg.table)
  123. end
  124. assert(variant.returns, string.format('Variant for %q is missing returns', fn.key))
  125. for _, ret in ipairs(variant.returns) do
  126. ret.description = unwrap(ret.description)
  127. processTable(ret.table)
  128. end
  129. end
  130. fn.arguments = nil
  131. fn.returns = nil
  132. track(fn)
  133. return fn
  134. end
  135. local function processObject(path, parent)
  136. local object = require(path .. '.init')
  137. object.key = path:match('[^/]+$')
  138. object.name = object.key
  139. object.description = unwrap(object.description)
  140. object.summary = object.summary or object.description
  141. object.module = parent.key
  142. object.methods = {}
  143. object.constructors = pluralify(object, 'constructor')
  144. object.notes = unwrap(object.notes)
  145. object.examples = pluralify(object, 'example')
  146. if object.sections then
  147. for _, section in ipairs(object.sections) do
  148. section.description = unwrap(section.description)
  149. end
  150. end
  151. for k, example in ipairs(object.examples or {}) do
  152. object.examples[k] = processExample(example)
  153. end
  154. for _, file in ipairs(lovr.filesystem.getDirectoryItems(path)) do
  155. if file ~= 'init.lua' then
  156. table.insert(object.methods, processFunction(path .. '/' .. file:gsub('%..+$', ''), object))
  157. end
  158. end
  159. table.sort(object.methods, function(a, b) return a.key < b.key end)
  160. track(object)
  161. return object
  162. end
  163. local function processModule(path)
  164. local module = require(path .. '.init') -- So we avoid requiring the module itself
  165. module.key = module.external and path:match('[^/]+$') or path:gsub('/', '.')
  166. module.name = module.external and module.key or module.key:match('[^%.]+$')
  167. module.description = unwrap(module.description)
  168. module.functions = {}
  169. module.objects = {}
  170. module.enums = {}
  171. module.notes = unwrap(module.notes)
  172. if module.sections then
  173. for _, section in ipairs(module.sections) do
  174. section.description = unwrap(section.description)
  175. end
  176. end
  177. module.examples = pluralify(module, 'example')
  178. for k, example in ipairs(module.examples or {}) do
  179. module.examples[k] = processExample(example)
  180. end
  181. for _, file in ipairs(lovr.filesystem.getDirectoryItems(path)) do
  182. local childPath = path .. '/' .. file
  183. local childModule = childPath:gsub('%..+$', '')
  184. local isFile = lovr.filesystem.isFile(childPath)
  185. local capitalized = file:match('^[A-Z]')
  186. if file ~= 'init.lua' and not capitalized and isFile then
  187. table.insert(module.functions, processFunction(childModule, module))
  188. elseif capitalized and not isFile then
  189. table.insert(module.objects, processObject(childModule, module))
  190. elseif capitalized and isFile then
  191. table.insert(module.enums, processEnum(childModule, module))
  192. end
  193. end
  194. table.sort(module.functions, function(a, b) return a.key < b.key end)
  195. table.sort(module.objects, function(a, b) return a.key < b.key end)
  196. table.sort(module.enums, function(a, b) return a.key < b.key end)
  197. track(module)
  198. return module
  199. end
  200. -- Validation
  201. local function validateRelated(item)
  202. for _, key in ipairs(item.related or {}) do
  203. warnIf(not lookup[key], 'Related item for %s not found: %s', item.key, key)
  204. warnIf(key == item.key, 'Item %s should not be related to itself', key)
  205. end
  206. end
  207. local function validateEnum(enum)
  208. for i, value in ipairs(enum.values) do
  209. warnIf(not value.name, 'Enum %s value #%d is missing name', enum.name, i)
  210. warnIf(not value.description, 'Enum %s value #%d is missing description', enum.name, i)
  211. end
  212. validateRelated(enum)
  213. end
  214. local function validateFunction(fn)
  215. if fn.tag then
  216. local found = false
  217. for _, section in ipairs(lookup[fn.module].sections or {}) do
  218. if section.tag == fn.tag then found = true break end
  219. end
  220. for _, object in ipairs(lookup[fn.module].objects) do
  221. for _, section in ipairs(object.sections or {}) do
  222. if section.tag == fn.tag then found = true break end
  223. end
  224. end
  225. warnIf(not found, 'Unknown tag %s for %s', fn.tag, fn.key)
  226. end
  227. for _, variant in ipairs(fn.variants) do
  228. for _, arg in ipairs(variant.arguments) do
  229. warnIf(not arg or not arg.name, 'Invalid argument for variant of %s', fn.key)
  230. warnIf(not arg.type or (arg.type:match('^[A-Z]') and not lookup[arg.type]), 'Invalid or missing argument type %s in %s', arg.type, fn.key)
  231. end
  232. for _, ret in ipairs(variant.returns) do
  233. warnIf(not ret or not ret.name, 'Invalid return for variant of %s', fn.key)
  234. warnIf(not ret.type or (ret.type:match('^[A-Z]') and not lookup[ret.type]), 'Invalid or missing return type %s for %s variant', ret.type, fn.key)
  235. end
  236. end
  237. validateRelated(fn)
  238. end
  239. local function validateObject(object)
  240. for _, constructor in ipairs(object.constructors or {}) do
  241. warnIf(not lookup[constructor], 'Constructor for %s not found: %s', object.key, constructor)
  242. end
  243. for _, method in ipairs(object.methods or {}) do
  244. validateFunction(method)
  245. end
  246. validateRelated(object)
  247. end
  248. local function validateModule(module)
  249. for _, object in ipairs(module.objects) do
  250. validateObject(object)
  251. end
  252. for _, fn in ipairs(module.functions) do
  253. validateFunction(fn)
  254. end
  255. for _, fn in ipairs(module.enums) do
  256. validateEnum(fn)
  257. end
  258. end
  259. function lovr.load()
  260. local api = {
  261. modules = {},
  262. callbacks = {}
  263. }
  264. -- Modules
  265. table.insert(api.modules, processModule('lovr'))
  266. for _, file in ipairs(lovr.filesystem.getDirectoryItems('lovr')) do
  267. local path = 'lovr/' .. file
  268. if file ~= 'callbacks' and file:match('^[a-z]') and lovr.filesystem.isDirectory(path) then
  269. table.insert(api.modules, processModule(path))
  270. end
  271. end
  272. -- Callbacks
  273. local callbacks = 'lovr/callbacks'
  274. for _, file in ipairs(lovr.filesystem.getDirectoryItems(callbacks)) do
  275. table.insert(api.callbacks, processFunction(callbacks .. '/' .. file:gsub('%.lua', ''), api.modules[1]))
  276. end
  277. -- Validate
  278. for _, module in ipairs(api.modules) do
  279. validateModule(module)
  280. end
  281. -- Sort
  282. table.sort(api.modules, function(a, b) return a.key < b.key end)
  283. table.sort(api.callbacks, function(a, b) return a.key < b.key end)
  284. -- Serialize
  285. local file = io.open(lovr.filesystem.getSource() .. '/init.lua', 'w')
  286. assert(file, 'Could not open init.lua for writing')
  287. local keyPriority = {
  288. name = 1,
  289. tag = 2,
  290. summary = 3,
  291. type = 4,
  292. description = 5,
  293. key = 6,
  294. module = 7,
  295. arguments = 8,
  296. returns = 9
  297. }
  298. local function sort(keys, t)
  299. table.sort(keys, function(a, b) return (keyPriority[a] or 1000) < (keyPriority[b] or 1000) end)
  300. end
  301. local contents = 'return ' .. serpent.block(api, { comment = false, sortkeys = sort })
  302. file:write(contents)
  303. file:close()
  304. -- Bye
  305. lovr.event.quit()
  306. end