local f = io.popen('git branch --show-current') local dev = f and f:read('*a'):match('%w+') == 'dev' f:close() -- 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 == '>' or b == ':' or a:match(':::') 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 local function getVisibleDirectoryItems(path) local input = lovr.filesystem.getDirectoryItems(path) local output = {} for _, item in pairs(input) do if item:match('^%.') then warn('Skipping hidden file "%s"', item) else table.insert(output, item) end end return output end -- Processors local function processExample(example, key) if type(example) == 'string' then return { code = unindent(example) } else assert(example.code, string.format('%s example is missing code', key)) 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.deprecated = type(fn.deprecated) == 'string' and unwrap(fn.deprecated) or fn.deprecated 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, fn.key) end assert(fn.variants and #fn.variants > 0, string.format('Function %q is missing variants', fn.key)) assert(fn.arguments, string.format('Function %q does not have arguments list', fn.key)) assert(fn.returns, string.format('Function %q does not have returns list', fn.key)) for name, arg in pairs(fn.arguments) do arg.name = name end for name, ret in pairs(fn.returns) do ret.name = name end for i, variant in ipairs(fn.variants) do assert(variant.arguments, string.format('%q variant #%d is missing arguments', fn.key, i)) assert(variant.returns, string.format('%q variant #%d is missing returns', fn.key, i)) for j, name in ipairs(variant.arguments) do warnIf(not fn.arguments[name], string.format('%s uses unknown argument %s', fn.key, name)) variant.arguments[j] = copy(fn.arguments[name]) end for j, name in ipairs(variant.returns) do warnIf(not fn.returns[name], string.format('%s uses unknown return %s', fn.key, name)) variant.returns[j] = copy(fn.returns[name]) 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) for _, arg in ipairs(variant.arguments) do arg.description = unwrap(arg.description) processTable(arg.table) end 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') assert(type(object) == 'table', string.format('%s/init.lua did not return a table', path)) 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(getVisibleDirectoryItems(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, object.key) 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, module.key) end for _, file in ipairs(getVisibleDirectoryItems(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], '%s has unknown related item %s', item.key, key) warnIf(key == item.key, '%s should not be related to itself', key) end end local function validateEnum(enum) warnIf(not enum.summary, 'Enum %s is missing summary', enum.name) warnIf(not enum.description, 'Enum %s is missing description', enum.name) 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 validateType(type, fields, key, kind) if not type then warn('Missing %s type in %s', kind, key) elseif type:match('^[A-Z]') then warnIf(not lookup[type], 'Invalid %s type "%s" in %s', kind, type, key) else local valid = { boolean = true, number = true, table = true, string = true, userdata = true, lightuserdata = true, ['function'] = true, ['*'] = true } warnIf(not valid[type], 'Invalid %s type "%s" in %s', kind, type, key) if type == 'table' and fields then for i, field in ipairs(fields) do validateType(field.type, field.table, key, kind) end end end 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) validateType(arg.type, arg.table, fn.key, 'argument') end for _, ret in ipairs(variant.returns) do warnIf(not ret or not ret.name, 'Invalid return for variant of %s', fn.key) validateType(ret.type, ret.table, fn.key, 'return') 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) if object.sections and not method.deprecated then local found = false for _, section in ipairs(object.sections) do if section.tag and section.tag == method.tag then found = true break else for _, link in ipairs(section.links or {}) do if link == method.key then found = true break end end end end warnIf(not found, '%s is missing from its parent\'s page', method.key) end end local metatable = debug.getregistry()[object.name] if dev and metatable then local hasMethod = {} for _, method in ipairs(object.methods or {}) do if not metatable[method.name] and not method.deprecated then warn('%s has docs for unknown method %s', object.name, method.name) end hasMethod[method.name] = true end if object.extends then for i, method in ipairs(lookup[object.extends].methods) do hasMethod[method.name] = true end end local ignore = { type = true, release = true, monkey = true } for name in pairs(metatable) do if not name:match('^__') and not ignore[name] then warnIf(not hasMethod[name], '%s is missing docs for %s', object.name, name) end end end validateRelated(object) end local function validateModule(module) local t = lovr[module.name] for _, object in ipairs(module.objects) do validateObject(object) end for _, fn in ipairs(module.functions) do validateFunction(fn) if dev and not fn.deprecated then warnIf(t and not t[fn.name], '%s has docs for unknown function %s', module.key, fn.name) end if module.sections and not fn.deprecated then local found = false for _, section in ipairs(module.sections) do if section.tag and section.tag == fn.tag then found = true break else for _, link in ipairs(section.links or {}) do if link == fn.key then found = true break end end end end warnIf(not found, '%s is missing a parent link/tag', fn.key) end end local ignore = { setSource = true, getBundlePath = true, openConsole = true } for name in pairs(t or {}) do local key = ('%s.%s'):format(module.key, name) warnIf(not ignore[name] and not lookup[key], 'Missing docs for %s', key) end for _, fn in ipairs(module.enums) do validateEnum(fn) end end function lovr.load() local api = { modules = {}, callbacks = {} } -- So errhand exits lovr.graphics = nil -- 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(getVisibleDirectoryItems(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) -- Generate local ok, generator = pcall(require, 'generators.' .. (arg[1] or 'lua')) if not ok then print(('Could not load generator %q: %s'):format(name, generator)) else generator(api) end -- Bye lovr.event.quit() end