Module:Loader

require('Module:MultiRequire') local table, string, dpl, _ = multiRequire('Module:Table', 'Module:String', 'Module:DPL', 'Module:LibraryUtil') local require, pcall = require, pcall

local p = {} _G.loader = p p.registry = _G.loader.registry or {} local loadable = table.Set{ 'bit32', 'libraryUtil', 'ustring', 'luabit.hex', 'luabit.bit', } local stack = table.slice(string.split(debug.traceback(''), '\n\t'), 2) local moduleName = (not stack[2]:match('mw.lua:487:') and stack[3] or stack[1]):match('^(.+):%d+:.+')

local function formatError(e, module, path) if type(e) ~= 'string' then return nil end if e:match('not found') then return ('Module %q was not found (in path %q)'):format(module, path) elseif e:match('loop') then return ('Loop or previous error loading module %q'):format(module) elseif e:match('^[Mm]odule:') then return ('Exception in loading module %q at line %s: %s'):format(module, e:match('^Module:%w+:(%d+)'), e:gsub('^Module:%w+:%d+:%s*', '')) end end

function formatDataLoadError(e, module, path) if type(e) ~= 'string' then return nil end if e:match('not found') then return ('Module %q was not found (in path %q)'):format(module, path) elseif e:match('unsupported') then return ('The data from module %q contains an unsupported data type %q'):format(module, e:match('unsupported data type [\"\'](.-)[\"\']')) elseif e:match('metatable') then return ('The data from module %q contains a table with a metatable'):format(module, e:match('unsupported data type [\"\'](.-)[\"\']')) elseif e:match('loop') then return ('Loop or previous error loading module %q'):format(module) end end

- -- Helper function to create a require function - local function createRequireFunc(basePath, isData) return function(...) local args = { ... }		local ret = {} local options local doUnpack = true if type(args[1]) == 'table' then args = args[1] doUnpack = false end if type(args[2]) == 'table' then options = args[2] end options = options or {} for i, path in forEachArgs({ 'string', 'table', required=1 }, unpack(args)) do			local oldPath = path assertTrue(path ~= '', 'Path may not be empty (in argument #%d)', 2, i)			path = p.resolvePath(path, basePath or moduleName) if p.registry[path] then args[i] = p.registry[path] else local success, res = pcall(isData and mw.loadData or require, path) assertTrue(success, (isData and formatDataLoadError or formatError)(res, path, oldPath), 2) args[i] = res p.registry[path] = res end end if doUnpack then return unpack(args) else return args end end end

- -- function: .loadData(...dataModules: table |string[]) -- -- Loads the listed modules as data. Accepts relative paths - p.loadData = createRequireFunc(nil, true)

- -- function: .require(...modules: table |string[]) -- -- Loads the listed modules. Accepts relative paths - p.require = createRequireFunc

- -- function: .resolveRelativePath(relativePath: string, basePath: string) -- -- Resolves a relative path based on `basePath`. - function p.resolvePath(relativePath, basePath) checkType(1, relativePath, 'string') checkType(2, basePath, 'string', true) basePath = basePath or moduleName:gsub('^[Mm]odule:', '') assertTrue(relativePath ~= '', 'Path may not be empty', 3) if not loadable[relativePath] and not loadable[basePath] then relativePath = relativePath:gsub('^[Mm]odule:', '') basePath = basePath:gsub('^[Mm]odule:', '') end local stack = table.tableUtil(string.split(basePath, "/")) local parts = table.tableUtil(string.split(relativePath, "/")) local base = stack:deepCopy if parts[1]:match"~" or (not parts:some(function(_, v) return v:match('%.') end) and parts[1] ~= '') then parts[1] = parts[1]:gsub("~", '') return 'Module:'..parts:concat('/') end for i = 1, #parts, 1 do		local v = parts[i]:gsub('^%$$', base[1]) if v ~= "." then assertFalse((i > 1 and i < #parts) and v == "", "Invalid path %q: Path level/name must not be empty", 2, relativePath) if v == ".." then stack:pop elseif v ~= "" then stack:push(v) end elseif #base > 1 then stack:pop end end return 'Module:'..table.concat(stack:map(function(v, i)		return v:gsub('^#$', stack[i-1] or stack[1]) end), '/'); end

- -- function: getModuleNames -- -- Lists all the availble modules to load. - function p.getModuleNames return dpl.list{ namespace='Module', } end

- -- function: getRegistry -- -- Gets the loader's registry - function p.getRegistry return p.registry end

- -- function: register(path: string, payload: function) -- -- Registers a module to the registry. - function p.register(...) local path, payload = checkArgs({ 'string', 'function' }, ...) path = path:gsub('^[Mm]odule:', '') local oldPath = path path = p.resolvePath(path) local module = { exports={}, name=path, }	local checkSelf = makeCheckSelfFunction(module, 'module') function module:addExport(...) checkSelf(self) local name, payload = checkArgs({ 'string', { 'any' } }, ...) self.exports[name] = payload return payload end local success, res = pcall(payload, createRequireFunc(path), module) if res == nil then res = 'unknown error' else res = tostring(res) end

assertTrue(success, 'Exception in registring module %q%s: %s', 2, path, res:match('^.-:%d+:') and (' in %s at line %s'):format(res:match('^(.-):%d+:'), res:match('^.-:(%d+):')) or , res:gsub('^(.-):%d+:%s*', )) p.registry[path] = module.exports return module.exports end

- -- function: readDir(dir: string, recurse?: boolean) -- -- Reads a directory. - function p.readDir(...) local dir, recurse = checkArgs({ 'string', { 'boolean', nilOk=true } }, ...) assertTrue(dir ~= '', 'path may not be empty', 2) dir = p.resolvePath(dir) local results = table.filter(dpl.getSubpages(dir), '') assertTrue(mw.title.new(dir).exists or #results ~= 0, 'Directory "%s/" not found', 2, dir) return results end

- -- function: readModule(...directories: string) -- -- Gets the contents of a module (Already existing on-wiki). - function p.readModule(...) local doUnpack = true local args = { ... }	if type(args[1]) == 'table' then args = args[1] doUnpack = false end

for i, path in forEachArgs({ 'string', required=1 }, unpack(args)) do		path = p.resolvePath(path) local title = mw.title.new(path) assertTrue(title.exists, 'Module %q not found', 2, path) args[i] = title:getContent end if doUnpack then return unpack(args) else return args end end

- -- function: removeModule(...paths: string) -- -- Removes modules from the registry. - function p.removeModule(...) local removed = {} for _, path in forEachArgs({ 'string', required=1 }, ...) do		path = p.resolvePath(path) assertTrue(p.registry[path], 'Module %q not found in module registry', 2, path) removed[path] = p.registry[path] p.registry[path] = nil end return removed end

- -- function: requireDir(dir: string, recurse?: boolean) -- -- Loads a directory. - function p.requireDir(...) local dir, recurse = checkArgs({ 'string', { 'boolean', nilOk=true } }, ...) assertTrue(dir ~= '', 'path may not be empty', 2) local results = p.readDir(dir, recurse) return p.require(unpack(results)) end

p.stack = stack

return p