main.lua 11 KB

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