Module:UtilsArg

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

local utilsString = require("Module:UtilsString") local utilsTable = require("Module:UtilsTable") local utilsVar = require("Module:UtilsVar")

h.templatePage = mw.getCurrentFrame:getParent:getTitle h.instanceCounter = utilsVar.counter("instanceCounter"..h.templatePage) h.instanceCounter.increment

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 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 {} args[repeated][index] = args[repeated][index] or {} utilsTable.merge(args[repeated][index], {					[param] = h.parseArg(v, paramSpec)				}) unknownParams[k] = nil end end args[repeated] = utilsTable.compact(args[repeated]) end -- Parse variadic args local variadicParam = templateSpec.params["..."] if variadicParam then args[variadicParam.name] = {} local i = #templateSpec.params + 1 while frameArgs[i] do			local varArg = h.parseArg(frameArgs[i], variadicParam) if varArg then table.insert(args[variadicParam.name], varArg) end unknownParams[i] = nil i = i + 1 end end -- Validate for k, v in pairs(templateSpec.params) do		local argErrors, errorCategories = h.validate(args[v.name or k], v, v.name or k, args) if #argErrors > 0 then err.args[v.name or k] = utilsTable.concat(err.args[v.name or k] or {}, argErrors) end err.categories = utilsTable.concat(err.categories, errorCategories) 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 in pairs(unknownParams) do		local errMsg = string.format("No such parameter  is defined for this template.", k)		h.warn(errMsg) err.args[k] = err.categories = utilsTable.concat(err.categories, "Category:Pages using Unknown Parameters") end if #err.categories == 0 then err = nil end if err and mw.title.getCurrentTitle.nsText == "User" then err.categories = {} end return args, err 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.split then local pattern = type(param.split) == "string" and param.split or nil arg = utilsString.split(arg, pattern) end if arg and param.nilIfEmpty then arg = utilsString.nilIfEmpty(arg) 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.validate(arg, param, paramName, args) local errors = {} local categories = {} for validatorName, validatorFn in pairs(validators) 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 local errorMessages, defaultCat if validatorData ~= nil then errorMessages, defaultCat = validatorFn(validatorData, arg, paramName) end local cat = defaultCat if validatorName == "required" and type(validatorData) == "string" then cat = validatorData end for _, err in ipairs(errorMessages or {}) do			table.insert(errors, {				msg = err,				category = cat			}) table.insert(categories, cat) 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}, "Category:Pages with Invalid Arguments" 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, "Category:Pages with Invalid Arguments" end function validators.deprecated(deprecated, value, name) if not deprecated then return end if value ~= nil then local err = string.format(" is deprecated but has value  .", name, value) h.warn(err) return {err}, "Category:Pages with Deprecated Arguments" 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}, "Category:Pages with Invalid Arguments" end end

p.Schemas = { parse = { frameArgs = { type = "any", required = true, desc = "Table of arguments obtained from .", },		templateSpec = { type = "any", required = true, desc = "Template documentation object.", }	} }

p.Documentation = { parse = { desc = "This function validates template input and parses it into a table for use in the rest of the module.", params = {"frameArgs", "templateSpec"}, returns = { "A table of arguments parsed from the template input.", "A table of validation errors, or nil if there are none. The error messages are also logged using .", },		cases = { outputOnly = true, {				desc = "Positional arguments are assigned to their names.", snippet = "PositionalAndNamedArgs", expect = { {						game = "OoT", page = "Boss Key", },					nil }			},			{				desc = "Special parameter  is used to parse an array of trailing template arguments", snippet = "TrailingArgs", expect = { {						games = {"OoT", "MM", "TWW", "TP"} },					nil }			},			{				desc = " used with other positional args", snippet = "TrailingArgsWithPositionalArgs", expect = { {						foo = "foo", bar = "bar", games = {"OoT", "MM", "TWW", "TP"}, },					nil }			},			{				desc = "Validation of required arguments.", snippet = "RequiredArgs", expect = { {						baz = "Baz" },					{						categories = { "Category:Pages with Invalid Arguments", "Category:Custom Category Name", },						args = { bar = { {									category = "Category:Custom Category Name", msg = " parameter is required.", },							},							foo = { {									category = "Category:Pages with Invalid Arguments", msg = " parameter is required.", },							},						},					},				},			},			{				desc = "Validation of deprecated arguments.", snippet = "Deprecated", expect = { { oldArg = "foo" }, {						categories = {"Category:Pages with Deprecated Arguments"}, args = { oldArg = { {									category = "Category:Pages with Deprecated Arguments", msg = " is deprecated but has value  .", },							},						},					},				}			},			{				desc = "Using an unknown parameter counts as an error.", snippet = "Unkown", expect = { {}, 					{						categories = {"Category:Pages using Unknown Parameters"}, args = { foo = { {									message = "No such parameter  is defined for this template.", category = "Category:Pages using Unknown Parameters", },							},						},					}				},			},			{				desc = "Can parse numbers", snippet = "Number", expect = { { foo = 9000 }, nil }			},			{				desc = "Returns an error if a non-number is passed when a number is expected.", snippet = "InvalidNumber", expect = { { foo = "notANumber" }, {						categories = {"Category:Pages with Invalid Arguments"}, args = { foo = { {									category = "Category:Pages with Invalid Arguments", msg = ' is expected to be a number but was:  ', },							},						},					},				},			},			{				desc = "Default values", snippet = "Default", expect = { { 						someParamWithDefault = "foo", someParamWithDefaultNumber = 1, param3 = "bar", param4 = "" }				}			},			{				desc = " can be set on a parameter so that utilsString.trim is called for the argument.", snippet = "Trim", expect = },			{				desc = " can be set on a parameter so that utilsString.nilIfEmpty is called for the argument.", snippet = "NilIfEmpty", expect = {{}, nil} },			{				desc = " can be set on a parameter so that utilsString.split is called for the argument.", snippet = "Split", expect = { { foo = {"a", "b", "c"} }, },			},			{				desc = " using a custom splitting pattern", snippet = "SplitPattern", expect = { { foo = {"a", "b", "c"} }, },			},			{				desc = "If  and   are set, then the argument is invalid if it is an empty string.", snippet = "NilIfEmptyWithRequiredArgs", expect = { {},					{						categories = {"Category:Pages with Invalid Arguments"}, args = { game = { {									category = "Category:Pages with Invalid Arguments", msg = " parameter is required.", },							},						},					},				},			},			{				desc = "If,  , and   are set, then the argument is invalid if it is a blank string.", snippet = "TrimNilIfEmptyRequired", expect = { {},					{						categories = {"Category:Pages with Invalid Arguments"}, args = { game = { {									category = "Category:Pages with Invalid Arguments", msg = " parameter is required.", },							},						},					},				},			},			{				desc = " validation.", snippet = "Enum", expect = { {						triforce2 = "Limpah", game = "ALttZ", triforce1 = "Kooloo", },					{						categories = { "Category:Pages with Invalid Arguments", "Category:Pages with Invalid Arguments", "Category:Pages with Invalid Arguments", },						args = { triforce2 = { {									category = "Category:Pages with Invalid Arguments", msg = " has unexpected value  . For a list of accepted values, refer to Triforce.", },							},							game = { {									category = "Category:Pages with Invalid Arguments", msg = " has unexpected value  . For a list of accepted values, refer to Data:Franchise.", },							},							triforce1 = { {									category = "Category:Pages with Invalid Arguments", msg = ' has unexpected value. The accepted values are: ', },							},						},					},				},			},			{				desc = " is used to parse comma-separated strings as arrays. Each array item can be validated against an  .", snippet = "SplitEnum", expect = { {						games = {"OoT", "fakeGame", "BotW"}, },					{						categories = {"Category:Pages with Invalid Arguments"}, args = { games = { {									category = "Category:Pages with Invalid Arguments", msg = " has unexpected value  . For a list of accepted values, refer to Data:Franchise.", }							}						}					}				}			},			{				desc = " can be used alongside   and  . Entries are sorted to match the sort order of the enum.", snippet = "SplitEnumSortAndRemoveDuplicates", expect = { { games = {"OoT", "BotW"} }, nil },			},			{				desc = " can be written as a function, when the list of acceptable values depends on the value of another argument.", snippet = "EnumDependsOn", expect = { {						term = "Dinolfos", game = "TP" },					{						categories = {"Category:Pages with Invalid Arguments"}, args = { term = { {									category = "Category:Pages with Invalid Arguments", msg = ' has unexpected value. The accepted values are: ', },							},						},					},				},			},			{				desc = "If  refers to a required parameter, then   is not evaluated when that parameter is nil.", snippet = "EnumDependsOnNil", expect = { { term = "Dinolfos" }, {						categories = {"Category:Pages with Invalid Arguments"}, args = { game = { {									category = "Category:Pages with Invalid Arguments", msg = " parameter is required.", }							},						},					},				},			},			{				desc = "Altogether now", snippet = "TermStorePass", expect = { {						term = "Dinolfos", games = {"OoT", "MM"}, },					nil }			},			{				snippet = "TermStoreFail", expect = { {						plural = "true", games = {"YY", "ZZ"}, },					{						categories = { "Category:Pages with Invalid Arguments", "Category:Pages with Deprecated Arguments", "Category:Pages with Invalid Arguments", "Category:Pages with Invalid Arguments", },						args = { term = { {									category = "Category:Pages with Invalid Arguments", msg = " parameter is required.", },							},							games = { {									category = "Category:Pages with Invalid Arguments", msg = " has unexpected value  . For a list of accepted values, refer to Data:Franchise." },								{									category = "Category:Pages with Invalid Arguments", msg = " has unexpected value  . For a list of accepted values, refer to Data:Franchise." },							},							plural = { {									category = "Category:Pages with Deprecated Arguments", msg = " is deprecated but has value  .", },							},						},					},				},			},			{				desc = ", , and validators such as   are applied to individual trailing arguments", snippet = "TrailingArgsStringTrimNilIfEmptyEnum", expect = { {						games = {"OoT", "MM", "ALttZ"}, },					{						categories = {"Category:Pages with Invalid Arguments"}, args = { games = { {								category = "Category:Pages with Invalid Arguments", msg = " has unexpected value  . For a list of accepted values, refer to Data:Franchise.", }						},						},					},				},			},			{				desc = "repeatedGroup", snippet = "RepeatedGroup", expect = { {						tabs = { {								tab = "Tab 1", content = "Content 1", },							{								tab = "Tab 2", content = "Content 2", },							{ tab = "Tab 4" }, { content = "Content 5" }, }					},					nil }			},		},	} }

return p