main.lua 12 KB

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