Module:LibraryUtil

-- From MediaWiki source file

- -- Find line/module name via `debug.stacktrace` for debugging - function getCodeLocation(level) local trace = debug.traceback('', (level or 1) + 2) return ((trace:gsub('\nstack traceback:\n', ):match('^\t([^\n<>]+):')..':'):gsub('\t?%(tail call%): ?%?', )) end

mw.oldLog = mw.log local oldLog = mw.oldLog local logLevel = 2

mw.log = function(...) local function newLog(...) return oldLog(getCodeLocation(logLevel), mw.allToString(...)) end mw.log = newLog newLog(...) logLevel = 1 end

local logObjectLevel = 2 mw.logObject = function(...) local function newLog(...) return oldLog(getCodeLocation(logObjectLevel), mw.dumpObject(...)) end mw.logObject = newLog newLog(...) logObjectLevel = 1 end

- -- Create an argument number/name - local function makeArgNumber(val) return table.concat{ type(val) == 'number' and '#' or '\, val, type(val) ~= "number" and '\ or '' } end

- -- Get the namae of a function based off of a stack level using `debug.stracktrace` - function getParentName(level) level = (level or 0) + 3 local stack = debug.traceback(, level) or 

stack = (stack:gsub('\nstack traceback:\n', )		:match('in function ([^\n]+)') or ) :gsub('^\'(.-)\'$', '%1') stack = (stack:match('[<>:]') or stack == '') and '?' or stack return stack end

- -- Utility function to find the index of a value in a table - local function indexOf(t, value) if type(t) ~= 'table' then return -1 end local i = 1 while t[i] do		if t[i] == value then return i end i = i+1 end return -1 end

- -- Helper function for `checkType` to check of a set of given types matches a value - local function typeMatches(valType, val, types, nilOk, numberOk) local tpTypes = type(types) local tn = valType == 'number' and val or tonumber(val) local isStringNumber = not not (tn and (types == 'string' or indexOf(types, 'string') ~= -1)) if tpTypes == 'table' and #types == 1 then types = types[1]; tpTypes = 'string' end if valType == 'nil' and nilOk then return true elseif val == nil and types == nil then return false end if tpTypes == 'string' then return (valType == types or (types == 'number' and not not tn)) or (isStringNumber and numberOk) end for i = 1, #types, 1 do		local v = types[i] if v == valType or (v == 'number' and tn) or isStringNumber then return true end end return false end

- -- Format a list of types in an error message for `checkType` and it's related functions - local function generateTypes(types) if type(types) == "string" then return types end local n = #types

if n == 1 then return types[1] end

return table.concat(types, '/') end

- -- Format an error message for `checkType` and related functions - local function generateMsg(name, index, msg, types) local msg = string.format("bad argument %s to '%s' (%s)",		makeArgNumber(index),		name,		types and (generateTypes(types)..' expected, got '..msg) or msg  ); return msg end

- -- Data for `checkType` - _G.typeMatches = typeMatches _G.getParentName = getParentName _G.makeArgNumber = makeArgNumber _G.generateMsg = generateMsg

local validTypes = { ['string']=true, ['number']=true, ['nil']=true, ['table']=true, ['boolean']=true, ['function']=true, ['any']=true, } - -- Check if a set of given types is valid - function validateTypes(types) local tp = type(types) local len = tp == 'table' and #types if not types or (tp ~= "table" and tp ~= "string") or types == "" or (tp == 'table' and len > 7) then return false end if (tp == 'string' or tp == 'table') and len == 0 then return true end if len == 1 and tp == 'table' then types = types[1]; tp = 'string' end if tp == "string" then return not not validTypes[types] end for i = 1, len, 1 do		if not validTypes[types[i]] then return false end end return true end

- -- function: checkType -- -- modes: --  type checker: checkType(name?: string, pos: number, value: any, types: string|table, nilOk?: boolean) --  no value error: checkType(name: 'no value', pos: number, types?: string|table, level?: number)

-- Options to types: --  *name: Changes the function to the value provided --  *base: Makes the value provided have the correct number base - function checkType(name, argIdx, arg, expectTypes, nilOk) local isConstructor, tpName

if name == true then name = nil isConstructor = true end tpName = type(name) local tpTypes = type(expectTypes) local tpArg = type(arg); -- Argument overloads if (tpName == "number" and (tpArg == 'table' or tpArg == 'string') and tpTypes ~= 'string' and tpTypes ~= 'table') or (name == nil and argIdx == nil) then nilOk = expectTypes expectTypes = arg or 'string' arg = argIdx or nil argIdx = name or 1 name = nil end name = name or expectTypes.name local t = {} local tpArg = type(arg) t.checkType = error -- Check if types match and if there is an error local level = isConstructor and 1 or (tpName == "number" and name or 0) local numOk = expectTypes.numberOk or expectTypes.numOk local matches = name ~= 'no value' and typeMatches(tpArg, arg, expectTypes, nilOk, (expectTypes and numOk)) local isError = (tpName == "string" and name ~= 'no value') or not matches local fName local tn	local numArg = expectTypes.numArg if expectTypes and expectTypes.numArg and not expectTypes.base then expectTypes.base = 10 end -- Optimize as much as possible if isError then fName = (name ~= 'no value' and tpName ~= 'number') and name or getParentName(name == 'no value' and expectTypes or level) end if name == 'no value' then t.checkType(generateMsg(fName, argIdx, arg and 'no value' or 'value expected', arg), (expectTypes or level)+3) end -- Check if `base` is valid if expectTypes.base and (expectTypes.base < 2 or expectTypes.base > 36) then error('the option "base" must be between 2 and 36', level+2) end -- Return if `numArg` is valid if expectTypes and #expectTypes == 0 then return arg end if numArg or numOk then tn = (tpArg ~= 'string' and tpArg ~= 'number') and nil or tonumber(arg, expectTypes.base) end if (tpArg == 'string' or tpArg == 'number') and numArg and tn then return tn	end if tpArg == 'number' and numOk then return arg end -- Error if types did not match if not matches then local msg = generateMsg(fName, argIdx, tpArg, expectTypes) t.checkType(msg, level+3) end -- Check if argument is convertable to a number if expectTypes and expectTypes.base and not tn then fName = getParentName(name == 'no value' and expectTypes or level); local msg = generateMsg(fName, argIdx, 'value is not convertable to a base '..expectTypes.base..' number') t.checkType(msg, level+3) elseif expectTypes and expectTypes.base and tn then return tn	end return arg end

- -- "Lite" version of `checkType` due to performance issues - function checkTypeLight(name, argIdx, arg, expectTypes, nilOk) local isMulti = expectTypes.lower ~= string.lower -- Check using properties instead of type local argType = type(arg) if arg == nil and nilOk then return arg end if isMulti then for _, v in pairs(expectTypes) do			if argType == v then return arg end end return ({ checkType=error }).checkType(string.format("bad argument #%d to '%s' (%s expected, got %s)", argIdx, name, table.concat(expectTypes, '/'), argType), 3) end if argType ~= expectTypes then ({ checkType=error }).checkType(string.format("bad argument #%d to '%s' (%s expected, got %s)", argIdx, name, expectTypes, argType), 3) end return arg end

- -- function: checkArgs(types: table, ...arguments?: any) -- -- Checks a given set of arguments against a list of types in a compact and streamlined manner - function checkArgs(types, ...) checkTypeLight('checkArgs', 1, types, { 'string', 'table' }) if type(types) == 'string' then types = { types } end local t = {} local ret = { ... }	-- Number of arguments local n = select('#', ...) t.checkTypeArgs = error local level = type(types.level) == "number" and (types.level >= -1 and types.level or -types.level) or 0 local len = #types -- Valid types local validTypes = { ['string']=true, ['nil']=true, ['number']=true, ['table']=true, ['boolean']=true, ['function']=true, ['any']=true, ['stringable']=true, }	local toIter if types.strict then toIter = (len >= n and len or n)	else toIter = len end local fName for i = 1, (len >= n and len or n) do		local any -- Variables local curTypes = type(types[i]) ~= "table" and { types[i] } or types[i] local emptyOk = curTypes.emptyOk or curTypes.emptyok local nilOk = curTypes.nilOk or curTypes.nilok local numberOk = curTypes.numberOk or curTypes.numOk or curTypes.numok or curTypes.numberok local val = ({ ... })[i] local argIndex = select('#', ...)+1 if i > #types and types.strict then fName = getParentName(level); t.checkTypeArgs(('bad argument #%d to \'%s\' (%d arguments expected, got %d)'):format(i, fName, #types, i), level+3) end -- Case for nilOk if nilOk and emptyOk == nil then emptyOk = true end -- Special Case if nilOk is false and emptyOk is false if nilOk and emptyOk == false then table.insert(curTypes, 'nil') end -- Special case for the 'any' type if indexOf(curTypes, 'any') ~= -1 or (emptyOk == false and #curTypes == 0) then any = true curTypes = { numberOk = numberOk, nilOk = nilOk, emptyOk = emptyOk, }		end local tpVal = type(val) -- If argument is nil, but not empty, and the expected type is 'any' and nilOk is false, error if nilOk == false and any and not (n n) and tpVal == 'nil' then fName = getParentName(level); -- Error message #1 local valueExpected = ('bad argument #%d to \'%s\' (value expected)'):format(i, fName) t.checkTypeArgs(valueExpected, level+3) end -- If argument is empty if n n and (not emptyOk or tpVal == 'nil') then if any then fName = getParentName(level); local valueExpected = ('bad argument #%d to \'%s\' (value expected)'):format(i, fName) -- Value expected error if argument is empty t.checkTypeArgs(valueExpected, level+3) elseif not nilOk or (nilOk and emptyOk == false) then fName = getParentName(level); -- No value error t.checkTypeArgs(generateMsg(fName, argIndex, 'no value', generateTypes(curTypes)), level+3) end else -- Else check its type as normal ret[i] = checkType((level ~= 0 and level+1 or 1), i, val, curTypes, nilOk) end end return unpack(ret) end

checkTypeArgs = checkArgs

- -- Alias for `checkType` - function checkTypeMulti(t, types) checkType(1, t, 'table') checkType(2, types, 'table') t.checkTypeMulti = error for i = 1, #types do		checkType(true, i, t[i], types[i]) end end

- -- function: alertDeprecation(name: string, useInstead?: string, level?: number) -- -- Throws a derecation error message with an optional substitute for the deprecated function - function alertDeprecation(...) local name, useInstead, level = checkTypeArgs({ 		'string', { 'string', nilOk=true }, { 'number', nilOk=true } 	}, ...) local t = {} t.alertDeprecation = error if type(name) == "table" then name, useInstead = unpack{ name.name or name[1], name.useInstead or name.use or name[2], }	end t.alertDeprecation(string.format( 'function %q is deprecated%s', name or getParentName, useInstead and string.format(', use the function %q instead', useInstead) or '' ), (level or 0)+3) end

- -- function: forEachArgs(types: string, ...arguments?: string) -- -- Returns an iterator which iterates over the list of given arguments, and asserts the type of each argument. -- Useful in variable argument functions. - function forEachArgs(types, ...) checkType(1, types, { 'string', 'table' }) local startIndex = types.startIndex and types.startIndex-1 or 0 local required = types.required or 0 local i = 0+(startIndex or 0) local args = { ... }	local lim = select('#', ...) local ind = indexOf(types, 'any') local t = {} t.checkType = error return function i = i + 1 if lim lim then if ind ~= -1 then t.checkType(generateMsg(getParentName, i, 'value expected'), 3) elseif ind == -1 then checkType('no value', i, types, 1) end end if i <= lim then if ind == -1 then checkType(1, i, args[i], types, types.nilOk) end return i, args[i], args else return nil, nil end end, ... end

- -- function: makeCheckSelfFunction(libraryName: string, varName: string, selfObj?: table, selfObjDesc?: string) -- -- Creates a function which asserts that the given object is an instance of another. - function makeCheckSelfFunction(libraryName, varName, selfObj, selfObjDesc) if type(varName) == 'table' then varName = libraryName selfObjDesc = selfObj end if type(libraryName) == 'table' then selfObj = libraryName libraryName = varName end return function(self, method) if self ~= selfObj then method = method or getParentName; ({ checkSelf=error })['checkSelf'](string.format( "%s: invalid %s. Did you call .%s with a dot instead of a colon, i.e. " .. "%s.%s(...) instead of %s:%s(...)?", libraryName, selfObjDesc or 'self object', method, varName, method, varName, method ), 3)		end end end

- -- function: formattedError(formatStr?: string, level?: number, ...substitions?: string | number) -- -- Throws an error but with an option to use `string.format`. - function formattedError(formatStr, level, ...) checkTypeLight('formattedError', 2, level, { 'number' }, true)

local t = { ... }

if #t > 0 then for i, v in ipairs(t) do			t[i] = tostring(v) end end local formatStr = type(formatStr) == 'string' and formatStr or 'unknown error' return ({ formattedError=error })['formattedError'](string.format(formatStr, unpack(t)), tonumber(level) == 0 and level or (level or 1)+1) end

- -- function: formattedAssert(v?: any, formatStr?: string, level?: number, ...substitions?: string | number) -- -- Works like the native `assert` but allows an option for using `formattedError`. - function formattedAssert(v, formatStr, level, ...) local formatStr = type(formatStr) == 'string' and formatStr or 'assertion failed!' if not v then formattedError(formatStr, level == 0 and level or (level or 1)+1, ...) else return v	end end

- -- Inverse alias for `formattedAssert` - function assertFalse(v, formatStr, level, ...) local formatStr = type(formatStr) == 'string' and formatStr or 'assertion failed!' if v then formattedError(formatStr, level == 0 and level or (level or 1)+1, ...) else return v	end end

assertTrue = formattedAssert

- -- function: mw.title.existsWithoutWanted(title:string) -- -- Checks if a page exists without marking it as a Special:WantedPages. title is name including namespace (if there is one) -- [WARNING] Since this doesn't us backlinking (what causes it to be "wanted") the value returned for this function will not be reevaluated for a potentially infinite time after the page is created/deleted. Edits/null edits/purging will update it properly. -- [EXPENSIVE] Note that just like a normal page `exists` check, this is an expensive function (although it runs faster) - function mw.title.existsWithoutWanted(title) local frame = mw.getCurrentFrame -- PROTECTIONEXPIRY is a magic word that lets us check a page in a roundabout way without marking it as wanted. -- Trick taken from: https://www.mediawiki.org/wiki/Extension_talk:Scribunto/Lua_reference_manual#Avoid_creating_a_wanted_page_link_when_checking_if_page_exist return frame:callParserFunction('PROTECTIONEXPIRY:edit', title) ~= '' end

pageExists = mw.title.existsWithoutWanted

- -- function: pipeline(...) -- -- Creates a function pipeline, where each argument is stored then passed to a function -- when it is found in the arguments list, then it is invoked. - function pipeline(...) local value = checkArgs('any', ...) local prependArgs = { value } local t, functionFound for k, v in forEachArgs({ 'any', startIndex=2 }, ...) do		if type(v) ~= 'function' then if #prependArgs == 0 then prependArgs[#prependArgs+1] = value end prependArgs[#prependArgs+1] = v		else t = { pcall(v, unpack(prependArgs)) } functionFound = true if not t[1] then t[2] = tostring(t[2]) -- Error message if invocation failed assertTrue(					t[1], 					'Exception in calling function%s%s at que posistion #%d: %s', 					2, 					t[2]:match('^(.-):(.-):') and ' in module "'..t[2]:match('^(.-):(.-):')..'"' or ,					t[2]:match('^(.-):(.-):') and ' at line '..({ t[2]:match('^(.-):(.-):') })[2]..' called' or ,					k,					t[2]:gsub('^(.-):(.-):', '')				) end table.remove(t or {}, 1) prependArgs = t		end end if functionFound then return unpack(t) else return ... end end

- -- deprecated - function customArgError(cond, name, index, msg, ...) local tmp = { ... }	local t = {} t.assertTrue = error if #tmp == 1 and type(tmp[1]) == "table" then params = tmp[1] else params = tmp end -- mw.log('[WARN] [Deprecation] usage of `customArgError` is deprecated, use `assertTrue` instead.') if cond then t.assertTrue(string.format('bad argument to %s to \'%s\' (%s)', makeArgNumber(index), name, params and string.format(msg, unpack(params)) or msg ), 3) end end

- -- function: bind(targetFn: function, ...items?: any) -- -- Wraps `targetFn` in a new function which serves as a proxy prepending any arguments -- given when the new function is called - function bind(targetFn, ...) local mt = getmetatable(targetFn) if not (mt and (mt.__call or mt.__isClass)) then checkType(1, targetFn, 'function') end local args = { ... }	return function(...) for _, v in ipairs{ ... } do			table.insert(args, v)		end local values = { targetFn(unpack(args)) } return unpack(values) end end

_G.getStackName = getParentName return _G