123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- local serpent = require 'serpent'
- -- Helpers
- local function copy(t)
- if type(t) ~= 'table' then return t end
- local result = {}
- for k, v in pairs(t) do
- result[k] = copy(v)
- end
- return result
- end
- local function unindent(code)
- local indent = code:match('^(% +)')
- if indent then
- return code:gsub('\n' .. indent, '\n'):gsub('^' .. indent, ''):gsub('%s*$', '')
- else
- return code
- end
- end
- local function unwrap(str)
- if not str then return str end
- str = unindent(str)
- return str:gsub('([^\n])\n(%S)', function(a, b)
- if b == '-' or b == '>' then
- return a .. '\n' .. b
- else
- return a .. ' ' .. b
- end
- end)
- :gsub('^%s+', '')
- :gsub('%s+$', '')
- end
- local function pluralify(t, key)
- t[key .. 's'] = t[key .. 's'] or (t[key] and { t[key] } or nil)
- t[key] = nil
- return t[key .. 's']
- end
- local lookup = {}
- local function track(obj)
- lookup[obj.key] = obj
- end
- local function warn(s, ...)
- print(string.format(s, ...))
- end
- local function warnIf(cond, ...)
- if cond then warn(...) end
- end
- -- Processors
- local function processExample(example)
- if type(example) == 'string' then
- return {
- code = unindent(example)
- }
- else
- example.description = unwrap(example.description)
- example.code = unindent(example.code)
- end
- return example
- end
- local function processEnum(path, parent)
- local enum = require(path)
- enum.name = path:match('[^/]+$')
- enum.key = enum.name
- enum.module = parent.key
- enum.description = unwrap(enum.description)
- enum.notes = unwrap(enum.notes)
- for _, value in ipairs(enum.values) do
- value.description = unwrap(value.description)
- end
- track(enum)
- return enum
- end
- local function processFunction(path, parent)
- local fn = require(path)
- fn.name = path:match('[^/]+$')
- fn.key = parent.name:match('^[A-Z]') and (parent.key .. ':' .. fn.name) or (path:gsub('/', '.'):gsub('callbacks%.', ''))
- fn.description = unwrap(fn.description)
- fn.module = parent.module or parent.key
- fn.notes = unwrap(fn.notes)
- fn.examples = pluralify(fn, 'example')
- for k, example in ipairs(fn.examples or {}) do
- fn.examples[k] = processExample(example)
- end
- if not fn.variants then
- local missingVariants = (not fn.arguments[1] and next(fn.arguments)) or (not fn.returns[1] and next(fn.returns))
- warnIf(missingVariants, 'Function %q is missing variants', fn.key)
- fn.variants = {
- {
- arguments = fn.arguments,
- returns = fn.returns
- }
- }
- else
- assert(fn.arguments, string.format('Function %q with variants does not have arguments list', fn.key))
- for name, arg in pairs(fn.arguments) do
- arg.name = name
- end
- assert(fn.returns, string.format('Function %q with variants does not have returns list', fn.key))
- for name, ret in pairs(fn.returns) do
- ret.name = name
- end
- for _, variant in ipairs(fn.variants) do
- for i, name in ipairs(variant.arguments) do
- warnIf(not fn.arguments[name], string.format('Function %q variant argument %q does not exist', fn.key, name))
- variant.arguments[i] = copy(fn.arguments[name])
- end
- for i, name in ipairs(variant.returns) do
- warnIf(not fn.returns[name], string.format('Function %q variant return %q does not exist', fn.key, name))
- variant.returns[i] = copy(fn.returns[name])
- end
- end
- end
- for _, variant in ipairs(fn.variants) do
- local function processTable(t)
- if not t then return end
- for _, field in ipairs(t) do
- field.description = unwrap(field.description)
- processTable(field.table)
- end
- t.description = unwrap(t.description)
- end
- variant.description = unwrap(variant.description)
- assert(variant.arguments, string.format('Variant for %q is missing arguments', fn.key))
- for _, arg in ipairs(variant.arguments) do
- arg.description = unwrap(arg.description)
- processTable(arg.table)
- end
- assert(variant.returns, string.format('Variant for %q is missing returns', fn.key))
- for _, ret in ipairs(variant.returns) do
- ret.description = unwrap(ret.description)
- processTable(ret.table)
- end
- end
- fn.arguments = nil
- fn.returns = nil
- track(fn)
- return fn
- end
- local function processObject(path, parent)
- local object = require(path .. '.init')
- object.key = path:match('[^/]+$')
- object.name = object.key
- object.description = unwrap(object.description)
- object.summary = object.summary or object.description
- object.module = parent.key
- object.constructors = pluralify(object, 'constructor')
- object.notes = unwrap(object.notes)
- object.examples = pluralify(object, 'example')
- if object.sections then
- for _, section in ipairs(object.sections) do
- section.description = unwrap(section.description)
- end
- end
- local methods = {}
- for _, file in ipairs(lovr.filesystem.getDirectoryItems(path)) do
- if file ~= 'init.lua' then
- local method = file:gsub('%..+$', '')
- local key = ('%s:%s'):format(object.name, method)
- methods[key] = processFunction(path .. '/' .. method, object)
- end
- end
- if object.methods then
- for i, key in ipairs(object.methods) do
- object.methods[i] = methods[key]
- warnIf(not methods[key], '%s links to unknown method %q', object.key, key)
- methods[key] = nil
- end
- for method in pairs(methods) do
- warn('%s is missing link to %q', object.key, method)
- end
- else
- object.methods = {}
- for key, method in pairs(methods) do
- table.insert(object.methods, method)
- end
- table.sort(object.methods, function(a, b) return a.key < b.key end)
- end
- for k, example in ipairs(object.examples or {}) do
- object.examples[k] = processExample(example)
- end
- track(object)
- return object
- end
- local function processModule(path)
- local module = require(path .. '.init') -- So we avoid requiring the module itself
- module.key = module.external and path:match('[^/]+$') or path:gsub('/', '.')
- module.name = module.external and module.key or module.key:match('[^%.]+$')
- module.description = unwrap(module.description)
- module.functions = {}
- module.objects = {}
- module.enums = {}
- module.notes = unwrap(module.notes)
- if module.sections then
- for _, section in ipairs(module.sections) do
- section.description = unwrap(section.description)
- end
- end
- module.examples = pluralify(module, 'example')
- for k, example in ipairs(module.examples or {}) do
- module.examples[k] = processExample(example)
- end
- for _, file in ipairs(lovr.filesystem.getDirectoryItems(path)) do
- local childPath = path .. '/' .. file
- local childModule = childPath:gsub('%..+$', '')
- local isFile = lovr.filesystem.isFile(childPath)
- local capitalized = file:match('^[A-Z]')
- if file ~= 'init.lua' and not capitalized and isFile then
- table.insert(module.functions, processFunction(childModule, module))
- elseif capitalized and not isFile then
- table.insert(module.objects, processObject(childModule, module))
- elseif capitalized and isFile then
- table.insert(module.enums, processEnum(childModule, module))
- end
- end
- table.sort(module.functions, function(a, b) return a.key < b.key end)
- table.sort(module.objects, function(a, b) return a.key < b.key end)
- table.sort(module.enums, function(a, b) return a.key < b.key end)
- track(module)
- return module
- end
- -- Validation
- local function validateRelated(item)
- for _, key in ipairs(item.related or {}) do
- warnIf(not lookup[key], 'Related item for %s not found: %s', item.key, key)
- warnIf(key == item.key, 'Item %s should not be related to itself', key)
- end
- end
- local function validateEnum(enum)
- for i, value in ipairs(enum.values) do
- warnIf(not value.name, 'Enum %s value #%d is missing name', enum.name, i)
- warnIf(not value.description, 'Enum %s value #%d is missing description', enum.name, i)
- end
- validateRelated(enum)
- end
- local function validateFunction(fn)
- if fn.tag then
- local found = false
- for _, section in ipairs(lookup[fn.module].sections or {}) do
- if section.tag == fn.tag then found = true break end
- end
- for _, object in ipairs(lookup[fn.module].objects) do
- for _, section in ipairs(object.sections or {}) do
- if section.tag == fn.tag then found = true break end
- end
- end
- warnIf(not found, 'Unknown tag %s for %s', fn.tag, fn.key)
- end
- for _, variant in ipairs(fn.variants) do
- for _, arg in ipairs(variant.arguments) do
- warnIf(not arg or not arg.name, 'Invalid argument for variant of %s', fn.key)
- 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)
- end
- for _, ret in ipairs(variant.returns) do
- warnIf(not ret or not ret.name, 'Invalid return for variant of %s', fn.key)
- 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)
- end
- end
- validateRelated(fn)
- end
- local function validateObject(object)
- for _, constructor in ipairs(object.constructors or {}) do
- warnIf(not lookup[constructor], 'Constructor for %s not found: %s', object.key, constructor)
- end
- for _, method in ipairs(object.methods or {}) do
- validateFunction(method)
- end
- validateRelated(object)
- end
- local function validateModule(module)
- for _, object in ipairs(module.objects) do
- validateObject(object)
- end
- for _, fn in ipairs(module.functions) do
- validateFunction(fn)
- end
- for _, fn in ipairs(module.enums) do
- validateEnum(fn)
- end
- end
- function lovr.load()
- local api = {
- modules = {},
- callbacks = {}
- }
- -- Modules
- table.insert(api.modules, processModule('lovr'))
- for _, file in ipairs(lovr.filesystem.getDirectoryItems('lovr')) do
- local path = 'lovr/' .. file
- if file ~= 'callbacks' and file:match('^[a-z]') and lovr.filesystem.isDirectory(path) then
- table.insert(api.modules, processModule(path))
- end
- end
- -- Callbacks
- local callbacks = 'lovr/callbacks'
- for _, file in ipairs(lovr.filesystem.getDirectoryItems(callbacks)) do
- table.insert(api.callbacks, processFunction(callbacks .. '/' .. file:gsub('%.lua', ''), api.modules[1]))
- end
- -- Validate
- for _, callback in ipairs(api.callbacks) do
- validateFunction(callback)
- end
- for _, module in ipairs(api.modules) do
- validateModule(module)
- end
- -- Sort
- table.sort(api.modules, function(a, b) return a.key < b.key end)
- table.sort(api.callbacks, function(a, b) return a.key < b.key end)
- -- Serialize
- local file = io.open(lovr.filesystem.getSource() .. '/init.lua', 'w')
- assert(file, 'Could not open init.lua for writing')
- local keyPriority = {
- name = 1,
- tag = 2,
- summary = 3,
- type = 4,
- description = 5,
- key = 6,
- module = 7,
- arguments = 8,
- returns = 9
- }
- local function sort(keys, t)
- table.sort(keys, function(lhs, rhs)
- local leftPrio = keyPriority[lhs]
- local rightPrio = keyPriority[rhs]
- if leftPrio and rightPrio then
- return leftPrio < rightPrio
- elseif leftPrio or rightPrio then
- return leftPrio ~= nil
- else
- return lhs < rhs
- end
- end)
- end
- local contents = 'return ' .. serpent.block(api, { comment = false, sortkeys = sort })
- file:write(contents)
- file:close()
- -- Bye
- lovr.event.quit()
- end
|