main.lua 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  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. warnIf(not methods[key], '%s links to unknown method %q', object.key, key)
  165. methods[key] = nil
  166. end
  167. for method in pairs(methods) do
  168. warn('%s is missing link to %q', object.key, method)
  169. end
  170. else
  171. object.methods = {}
  172. for key, method in pairs(methods) do
  173. table.insert(object.methods, method)
  174. end
  175. table.sort(object.methods, function(a, b) return a.key < b.key end)
  176. end
  177. for k, example in ipairs(object.examples or {}) do
  178. object.examples[k] = processExample(example)
  179. end
  180. track(object)
  181. return object
  182. end
  183. local function processModule(path)
  184. local module = require(path .. '.init') -- So we avoid requiring the module itself
  185. module.key = module.external and path:match('[^/]+$') or path:gsub('/', '.')
  186. module.name = module.external and module.key or module.key:match('[^%.]+$')
  187. module.description = unwrap(module.description)
  188. module.functions = {}
  189. module.objects = {}
  190. module.enums = {}
  191. module.notes = unwrap(module.notes)
  192. if module.sections then
  193. for _, section in ipairs(module.sections) do
  194. section.description = unwrap(section.description)
  195. end
  196. end
  197. module.examples = pluralify(module, 'example')
  198. for k, example in ipairs(module.examples or {}) do
  199. module.examples[k] = processExample(example)
  200. end
  201. for _, file in ipairs(lovr.filesystem.getDirectoryItems(path)) do
  202. local childPath = path .. '/' .. file
  203. local childModule = childPath:gsub('%..+$', '')
  204. local isFile = lovr.filesystem.isFile(childPath)
  205. local capitalized = file:match('^[A-Z]')
  206. if file ~= 'init.lua' and not capitalized and isFile then
  207. table.insert(module.functions, processFunction(childModule, module))
  208. elseif capitalized and not isFile then
  209. table.insert(module.objects, processObject(childModule, module))
  210. elseif capitalized and isFile then
  211. table.insert(module.enums, processEnum(childModule, module))
  212. end
  213. end
  214. table.sort(module.functions, function(a, b) return a.key < b.key end)
  215. table.sort(module.objects, function(a, b) return a.key < b.key end)
  216. table.sort(module.enums, function(a, b) return a.key < b.key end)
  217. track(module)
  218. return module
  219. end
  220. -- Validation
  221. local function validateRelated(item)
  222. for _, key in ipairs(item.related or {}) do
  223. warnIf(not lookup[key], 'Related item for %s not found: %s', item.key, key)
  224. warnIf(key == item.key, 'Item %s should not be related to itself', key)
  225. end
  226. end
  227. local function validateEnum(enum)
  228. for i, value in ipairs(enum.values) do
  229. warnIf(not value.name, 'Enum %s value #%d is missing name', enum.name, i)
  230. warnIf(not value.description, 'Enum %s value #%d is missing description', enum.name, i)
  231. end
  232. validateRelated(enum)
  233. end
  234. local function validateFunction(fn)
  235. if fn.tag then
  236. local found = false
  237. for _, section in ipairs(lookup[fn.module].sections or {}) do
  238. if section.tag == fn.tag then found = true break end
  239. end
  240. for _, object in ipairs(lookup[fn.module].objects) do
  241. for _, section in ipairs(object.sections or {}) do
  242. if section.tag == fn.tag then found = true break end
  243. end
  244. end
  245. warnIf(not found, 'Unknown tag %s for %s', fn.tag, fn.key)
  246. end
  247. for _, variant in ipairs(fn.variants) do
  248. for _, arg in ipairs(variant.arguments) do
  249. warnIf(not arg or not arg.name, 'Invalid argument for variant of %s', fn.key)
  250. 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)
  251. end
  252. for _, ret in ipairs(variant.returns) do
  253. warnIf(not ret or not ret.name, 'Invalid return for variant of %s', fn.key)
  254. 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)
  255. end
  256. end
  257. validateRelated(fn)
  258. end
  259. local function validateObject(object)
  260. for _, constructor in ipairs(object.constructors or {}) do
  261. warnIf(not lookup[constructor], 'Constructor for %s not found: %s', object.key, constructor)
  262. end
  263. for _, method in ipairs(object.methods or {}) do
  264. validateFunction(method)
  265. end
  266. validateRelated(object)
  267. end
  268. local function validateModule(module)
  269. for _, object in ipairs(module.objects) do
  270. validateObject(object)
  271. end
  272. for _, fn in ipairs(module.functions) do
  273. validateFunction(fn)
  274. end
  275. for _, fn in ipairs(module.enums) do
  276. validateEnum(fn)
  277. end
  278. end
  279. function lovr.load()
  280. local api = {
  281. modules = {},
  282. callbacks = {}
  283. }
  284. -- Modules
  285. table.insert(api.modules, processModule('lovr'))
  286. for _, file in ipairs(lovr.filesystem.getDirectoryItems('lovr')) do
  287. local path = 'lovr/' .. file
  288. if file ~= 'callbacks' and file:match('^[a-z]') and lovr.filesystem.isDirectory(path) then
  289. table.insert(api.modules, processModule(path))
  290. end
  291. end
  292. -- Callbacks
  293. local callbacks = 'lovr/callbacks'
  294. for _, file in ipairs(lovr.filesystem.getDirectoryItems(callbacks)) do
  295. table.insert(api.callbacks, processFunction(callbacks .. '/' .. file:gsub('%.lua', ''), api.modules[1]))
  296. end
  297. -- Validate
  298. for _, callback in ipairs(api.callbacks) do
  299. validateFunction(callback)
  300. end
  301. for _, module in ipairs(api.modules) do
  302. validateModule(module)
  303. end
  304. -- Sort
  305. table.sort(api.modules, function(a, b) return a.key < b.key end)
  306. table.sort(api.callbacks, function(a, b) return a.key < b.key end)
  307. -- Serialize
  308. local file = io.open(lovr.filesystem.getSource() .. '/init.lua', 'w')
  309. assert(file, 'Could not open init.lua for writing')
  310. local keyPriority = {
  311. name = 1,
  312. tag = 2,
  313. summary = 3,
  314. type = 4,
  315. description = 5,
  316. key = 6,
  317. module = 7,
  318. arguments = 8,
  319. returns = 9
  320. }
  321. local function sort(keys, t)
  322. table.sort(keys, function(lhs, rhs)
  323. local leftPrio = keyPriority[lhs]
  324. local rightPrio = keyPriority[rhs]
  325. if leftPrio and rightPrio then
  326. return leftPrio < rightPrio
  327. elseif leftPrio or rightPrio then
  328. return leftPrio ~= nil
  329. else
  330. return lhs < rhs
  331. end
  332. end)
  333. end
  334. local contents = 'return ' .. serpent.block(api, { comment = false, sortkeys = sort })
  335. file:write(contents)
  336. file:close()
  337. -- Bye
  338. lovr.event.quit()
  339. end