Module:UtilsArg

local p = {} local h = {} local validators = {}

local Constants = mw.loadData("Module:Constants/Data") local utilsString = require("Module:UtilsString") local utilsTable = require("Module:UtilsTable") local utilsVar = require("Module:UtilsVar")

local CAT_DEPRECATED_PARAMS = "Category:"..Constants.category.deprecatedParams local CAT_INVALID_ARGS = "Category:"..Constants.category.invalidArgs

local parentFrame = mw.getCurrentFrame and mw.getCurrentFrame:getParent if parentFrame then h.templatePage = parentFrame:getTitle h.instanceCounter = utilsVar.counter("instanceCounter"..h.templatePage) h.instanceCounter.increment end

function p.parse(frameArgs, templateSpec) local args = {} local unknownParams = utilsTable.clone(frameArgs) local repeatedParams = templateSpec.repeatedGroup and templateSpec.repeatedGroup.params or {} local repeatedParamsMap = utilsTable.invert(repeatedParams) local isRepeated = h.isRepeated(repeatedParamsMap) local err = { args = {}, categories = {}, }	-- Parse ordinary args for k, v in pairs(templateSpec.params) do		if not repeatedParamsMap[k] then args[v.name or k] = h.parseArg(frameArgs[k], v)			unknownParams[k] = nil for i, alias in ipairs(v.aliases or {}) do				args[alias] = h.parseArg(frameArgs[alias], v)				args[v.name or k] = args[v.name or k] or args[alias] -- if both the parameter and its alias is used, the alias should not overrid unknownParams[alias] = nil end end end -- Parse repeatedGroup args local repeated = templateSpec.repeatedGroup and templateSpec.repeatedGroup.name if repeated then for k, v in pairs(unknownParams) do		local isRepeated, index, param = isRepeated(k) if isRepeated then local paramSpec = templateSpec.params[param] args[repeated] = args[repeated] or {} local arg = h.parseArg(v, paramSpec) if arg then args[repeated][index] = args[repeated][index] or {} args[repeated][index][param] = arg end unknownParams[k] = nil end end args[repeated] = args[repeated] and utilsTable.compact(args[repeated]) or {} -- in case a number is accidentally skipped for a whole "row" end -- Parse variadic args local variadicParam = templateSpec.params["..."] if variadicParam then local i = #templateSpec.params + 1 while frameArgs[i] do			local varArg = h.parseArg(frameArgs[i], variadicParam) if varArg then args[variadicParam.name] = args[variadicParam.name] or {} table.insert(args[variadicParam.name], varArg) end unknownParams[i] = nil i = i + 1 end end -- Validate for paramKey, paramSpec in pairs(templateSpec.params) do		if not repeatedParamsMap[paramKey] then -- repeated args are an edge case for validation and need to be handled separately local paramName = paramSpec.name or paramKey local paramValue = args[paramName] h.validateAndAddErrors(err, args, paramSpec, paramName, paramValue) else for i in ipairs(args[repeated] or {}) do				local paramName = paramKey..i -- no need to check paramSpec.name as repeated params should always be named arguments local paramValue = args[repeated][i][paramKey] h.validateAndAddErrors(err, args, paramSpec, paramName, paramValue) end end end -- Do any post processing such as sorting and removing duplicates for k, v in pairs(templateSpec.params) do		local arg = args[v.name or k]		if arg and v.enum and v.split and v.sortAndRemoveDuplicates then args[v.name or k] = utilsTable.intersection(v.enum, arg) end end -- Handle any args left that don't have corresponding params defined for k, v in pairs(unknownParams) do		-- value is not strictly necessary but can make it easier to search for the invalid usage when the template is used several times on a page local errMsg = string.format("No such parameter  is defined for this template. Value:  ", k, v)		h.warn(errMsg) err.args[k] = err.categories = utilsTable.concat(err.categories, "Category:Articles Using Unknown Parameters in Template Calls") end if #err.categories == 0 then err = nil end if err and mw.title.getCurrentTitle.nsText == "User" then err.categories = {} end if err then local categoryText = "" local categories = utilsTable.unique(err.categories) for i, cat in ipairs(categories) do			categoryText = categoryText..""..cat.."" end err.categoryText = categoryText end return args, err end

function p.enum(enum, value, name) local err, category = validators.enum(enum, value, name) if not err or #err == 0 then return nil else return { messages = err, category = category, }	end end

function h.warn(errMessage) local fullMessage = string.format("Misuse of %s at instance %s: %s", h.templatePage, h.instanceCounter.value, errMessage) mw.addWarning(fullMessage) -- We also log the message as mw.addWarning currently doesn't work if the editor has enabled the preference "Show previews without reloading the page". -- Unfortunately neither mw.addWarning nor mw.log work with the visual editor (even in source mode). -- An alternative would be to throw an actual script error as most wikis do. -- However, full errors degrade the reader experience. -- Many of our validation errors are not so critical that readers should know that they even exist. mw.log(fullMessage) end

function h.isRepeated(repeatedParamsMap) -- @param param a template parameter e.g. "tab1" -- @return boolean indicating whether parameter is part of a repeated group -- @return number index of the repition, e.g. 1 for "tab1", 2 for "tab2", etc. -- @return name of the parameter without the index, e.g. "tab" for "tab1", "tab2", etc.	return function(param) if type(param) == "number" then return false end local name = utilsString.trim(param, "0-9") local index = tonumber(string.sub(param, #name + 1)) if not repeatedParamsMap[name] or not index then return false end return true, index, name end end

function h.parseArg(arg, param) if arg and param.trim then arg = utilsString.trim(arg) end if arg and param.nilIfEmpty then arg = utilsString.nilIfEmpty(arg) end if arg and param.split then local pattern = type(param.split) == "string" and param.split or nil arg = utilsString.split(arg, pattern) end arg = arg or param.default if param.type == "number" then local num = tonumber(arg) if num then arg = num end end return arg end

function h.validateAndAddErrors(err, args, paramSpec, paramName, paramValue) local argErrors, errorCategories = h.validate(paramValue, paramSpec, paramName, args) if #argErrors > 0 then err.args[paramName] = utilsTable.concat(err.args[paramName] or {}, argErrors) end err.categories = utilsTable.concat(err.categories, errorCategories) end

function h.validate(arg, param, paramName, args) local errors = {} local categories = {} local validatorNames = {"required", "deprecated", "enum", "type"} -- do validation in this order for i, validatorName in ipairs(validatorNames) do		local validatorData = param[validatorName] if validatorName == "enum" and param.enum then local enum = param.enum if type(param.enum) == "function" then local dependency = args[param.enumDependsOn] enum = dependency and enum(dependency) end validatorData = enum end if validatorName == "type" and validatorData and arg == nil then validatorData = nil -- we shouldn't do type validation if arg is nil and not required end local errorMessages, defaultCat if validatorData ~= nil then errorMessages, defaultCat = validators[validatorName](validatorData, arg, paramName) end -- Here we allow custom categories so that editors can do targeted maintenance on a specific template parameter -- For example, we deprecate a parameter and use a custom category to clean up all the pages that use that parameter local cat = defaultCat if (validatorName == "required" or validatorName == "deprecated") and type(validatorData) == "string" then cat = validatorData end if errorMessages then for _, err in ipairs(errorMessages) do				table.insert(errors, {					msg = err,					category = cat				}) table.insert(categories, cat) end break -- run only one validator for now as there isn't yet any situtation where it makes sense to run several end end return errors, categories end

function validators.required(required, value, name) if not required then return end if value == nil then local err = string.format(" parameter is required.", name) h.warn(err) return {err}, CAT_INVALID_ARGS end end function validators.enum(enum, value, name) if not enum then return end -- Sometimes `value` is an "array", sometimes it's just a primitive value -- We can simplify the code by folding the latter case into the former -- i.e. making `value` always an array local isMultiValue = type(value) == "table" if not isMultiValue then value = { value } end local errors = {} for k, v in ipairs(value) do		if not utilsTable.keyOf(enum, v) then local path = name if isMultiValue then path = path .. string.format("[%s]", k)			end local msg if enum.reference then msg = string.format(" has unexpected value  . For a list of accepted values, refer to %s.", path, v, enum.reference) else local acceptedValues = utilsTable.print(enum, true) msg = string.format(" has unexpected value  . The accepted values are:  ", path, v, acceptedValues) end table.insert(errors, msg) h.warn(msg) end end return errors, CAT_INVALID_ARGS end function validators.deprecated(deprecated, value, name) if not deprecated then return end if value ~= nil then if type(value) == "table" then value = utilsTable.print(value, true) end local err = string.format(" is deprecated but has value  .", name, value) h.warn(err) return {err}, CAT_DEPRECATED_PARAMS end end function validators.type(expectedType, value, name) if expectedType == "number" and tonumber(value) == nil then local msg = " is expected to be a number but was:  " h.warn(msg) return {msg}, CAT_INVALID_ARGS end end

return p