Module:parameters: Difference between revisions

From Linguifex
Jump to navigation Jump to search
No edit summary
No edit summary
Line 1: Line 1:
local export = {}
local export = {}


local m_str_utils = require("Module:string utilities")
local families_module = "Module:families"
local families_module = "Module:families"
local function_module = "Module:fun"
local labels_module = "Module:labels"
local labels_module = "Module:labels"
local languages_module = "Module:languages"
local languages_module = "Module:languages"
local math_module = "Module:math"
local pages_module = "Module:pages"
local parse_utilities_module = "Module:parse utilities"
local parse_utilities_module = "Module:parse utilities"
local references_module = "Module:references"
local references_module = "Module:references"
local scripts_module = "Module:scripts"
local scripts_module = "Module:scripts"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local wikimedia_languages_module = "Module:wikimedia languages"
local wikimedia_languages_module = "Module:wikimedia languages"
local yesno_module = "Module:yesno"


local require_when_needed = require("Module:utilities/require when needed")
local mw = mw
local mw_title = mw.title
local string = string
local table = table


local dump = mw.dumpObject
local dump = mw.dumpObject
local floor = math.floor
local find = string.find
local gsplit = mw.text.gsplit
local format = string.format
local gmatch = string.gmatch
local gsub = string.gsub
local gsub = string.gsub
local huge = math.huge
local insert = table.insert
local insert = table.insert
local list_to_set = require("Module:table").listToSet
local ipairs = ipairs
local list_to_text = mw.text.listToText
local list_to_text = mw.text.listToText
local make_title = mw_title.makeTitle
local match = string.match
local match = string.match
local max = math.max
local max = math.max
local new_title = mw_title.new
local next = next
local pairs = pairs
local pairs = pairs
local pattern_escape = m_str_utils.pattern_escape
local pcall = pcall
local remove_holes = require_when_needed("Module:parameters/remove holes")
local rawset = rawset
local rsplit = m_str_utils.split
local require = require
local scribunto_param_key = m_str_utils.scribunto_param_key
local sort = table.sort
local sort = table.sort
local trim = mw.text.trim
local sub = string.sub
local tonumber = tonumber
local traceback = debug.traceback
local type = type
local type = type
local yesno = require_when_needed("Module:yesno")
 
local current_title_text, current_namespace -- Defined when needed.
local namespaces = mw.site.namespaces
 
local function decode_entities(...)
decode_entities = require(string_utilities_module).decode_entities
return decode_entities(...)
end
 
local function get_family_by_code(...)
get_family_by_code = require(families_module).getByCode
return get_family_by_code(...)
end
 
local function get_family_by_name(...)
get_family_by_name = require(families_module).getByCanonicalName
return get_family_by_name(...)
end
 
local function get_language_by_code(...)
get_language_by_code = require(languages_module).getByCode
return get_language_by_code(...)
end
 
local function get_language_by_name(...)
get_language_by_name = require(languages_module).getByCanonicalName
return get_language_by_name(...)
end
 
local function get_script_by_code(...)
get_script_by_code = require(scripts_module).getByCode
return get_script_by_code(...)
end
 
local function get_script_by_name(...)
get_script_by_name = require(scripts_module).getByCanonicalName
return get_script_by_name(...)
end
 
local function get_wm_lang_by_code(...)
get_wm_lang_by_code = require(wikimedia_languages_module).getByCode
return get_wm_lang_by_code(...)
end
 
local function get_wm_lang_by_code_with_fallback(...)
get_wm_lang_by_code_with_fallback = require(wikimedia_languages_module).getByCodeWithFallback
return get_wm_lang_by_code_with_fallback(...)
end
 
local function gsplit(...)
gsplit = require(string_utilities_module).gsplit
return gsplit(...)
end
 
local function is_callable(...)
is_callable = require(function_module).is_callable
return is_callable(...)
end
 
local function is_finite_real_number(...)
is_finite_real_number = require(math_module).is_finite_real_number
return is_finite_real_number(...)
end
 
local function is_integer(...)
is_integer = require(math_module).is_integer
return is_integer(...)
end
 
local function is_internal_title(...)
is_internal_title = require(pages_module).is_internal_title
return is_internal_title(...)
end
 
local function is_positive_integer(...)
is_positive_integer = require(math_module).is_positive_integer
return is_positive_integer(...)
end
 
local function iterate_list(...)
iterate_list = require(table_module).iterateList
return iterate_list(...)
end
 
local function list_to_set(...)
list_to_set = require(table_module).listToSet
return list_to_set(...)
end
 
local function num_keys(...)
num_keys = require(table_module).numKeys
return num_keys(...)
end
 
local function parse_references(...)
parse_references = require(references_module).parse_references
return parse_references(...)
end
 
local function pattern_escape(...)
pattern_escape = require(string_utilities_module).pattern_escape
return pattern_escape(...)
end
 
local function scribunto_param_key(...)
scribunto_param_key = require(string_utilities_module).scribunto_param_key
return scribunto_param_key(...)
end
 
local function sorted_pairs(...)
sorted_pairs = require(table_module).sortedPairs
return sorted_pairs(...)
end
 
local function split(...)
split = require(string_utilities_module).split
return split(...)
end
 
local function split_labels_on_comma(...)
split_labels_on_comma = require(labels_module).split_labels_on_comma
return split_labels_on_comma(...)
end
 
local function split_on_comma(...)
split_on_comma = require(parse_utilities_module).split_on_comma
return split_on_comma(...)
end
 
local function trim(...)
trim = require(string_utilities_module).trim
return trim(...)
end
 
local function yesno(...)
yesno = require(yesno_module)
return yesno(...)
end


--[==[ intro:
--[==[ intro:
Line 42: Line 191:
local params = {
local params = {
[1] = {required = true, type = "language", default = "und"},
[1] = {required = true, type = "language", default = "und"},
[2] = {},
[2] = true,
[3] = {list = true},
[3] = {list = true},
["alt"] = {},
["alt"] = true,
["id"] = {},
["id"] = true,
["sc"] = {type = "script"},
["sc"] = {type = "script"},
["tr"] = {},
["tr"] = true,
["ts"] = {},
["ts"] = true,
["lit"] = {},
["lit"] = true,
}
}


Line 60: Line 209:
The `params` table should have the parameter names as the keys, and a (possibly empty) table of parameter tags as the
The `params` table should have the parameter names as the keys, and a (possibly empty) table of parameter tags as the
value. An empty table as the value merely states that the parameter exists, but should not receive any special
value. An empty table as the value merely states that the parameter exists, but should not receive any special
treatment. Possible parameter tags are listed below:
treatment; if desired, empty tables can be replaced with the value `true` as a perforamnce optimization.
 
Possible parameter tags are listed below:


; {required = true}
; {required = true}
Line 97: Line 248:
   that are aliases and required at the same time cause an error to be thrown.
   that are aliases and required at the same time cause an error to be thrown.
; {allow_empty = true}
; {allow_empty = true}
: If the argument is an empty string value, it is not converted to {nil}, but kept as-is.
: If the argument is an empty string value, it is not converted to {nil}, but kept as-is. The use of `allow_empty` is
; {allow_whitespace = true}
  disallowed if a type has been specified, and causes an error to be thrown.
; {no_trim = true}
: Spacing characters such as spaces and newlines at the beginning and end of a positional parameter are not removed.
: Spacing characters such as spaces and newlines at the beginning and end of a positional parameter are not removed.
   (MediaWiki itself automatically trims spaces and newlines at the edge of named parameters.)
   (MediaWiki itself automatically trims spaces and newlines at the edge of named parameters.) The use of `no_trim` is
  disallowed if a type has been specified, and causes an error to be thrown.
; {type =}
; {type =}
: Specifies what value type to convert the argument into. The default is to leave it as a text string. Alternatives are:
: Specifies what value type to convert the argument into. The default is to leave it as a text string. Alternatives are:
:; {type = "boolean"}
:; {type = "boolean"}
:: The value is treated as a boolean value, either true or false. No value, the empty string, and the strings {"0"},
:: The value is treated as a boolean value, either true or false. No value, the empty string, and the strings {"0"},
   {"no"}, {"n"} and {"false"} are treated as {false}, all other values are considered {true}.
   {"no"}, {"n"}, {"false"}, {"f"} and {"off"} are treated as {false}, all other values are considered {true}.
:; {type = "number"}
:; {type = "number"}
:: The value is converted into a number, or {nil} if the value is not parsable as a number.
:: The value is converted into a number, and throws an error if the value is not parsable as a number. Input values may
  be signed (`+` or `-`), and may contain decimal points and leading zeroes. If {allow_hex = true}, then hexadecimal
  values in the form {"0x100"} may optionally be used instead, which otherwise have the same syntax restrictions
  (including signs, decimal digits, and leading zeroes after {"0x"}). Hexadecimal inputs are not case-sensitive. Lua's
  special number values (`inf` and `nan`) are not possible inputs.
:; {type = "language"}
:; {type = "language"}
:: The value is interpreted as a full or [[Wiktionary:Languages#Etymology-only languages|etymology-only language]] code
:: The value is interpreted as a full or [[Wiktionary:Languages#Etymology-only languages|etymology-only language]] code
Line 120: Line 277:
   are not allowed. The additional setting {family = true} can be given to allow
   are not allowed. The additional setting {family = true} can be given to allow
   [[Wiktionary:Language families|language family codes]] to be considered valid and the corresponding object returned.
   [[Wiktionary:Language families|language family codes]] to be considered valid and the corresponding object returned.
:; {type = "wikimedia language"}
:; {type = "Wikimedia language"}
:: The value is interpreted as a code and converted into a wikimedia language object. If the code is invalid, then an
:: The value is interpreted as a code and converted into a Wikimedia language object. If the code is invalid, then an
   error is thrown. If {method = "fallback"} is specified, conventional language codes which are different from their
   error is thrown. If {fallback = true} is specified, conventional language codes which are different from their
   Wikimedia equivalent will also be accepted as a fallback.
   Wikimedia equivalent will also be accepted as a fallback.
:; {type = "family"}
:; {type = "family"}
Line 130: Line 287:
:: The value is interpreted as a script code (or name, if {method = "name"}) and converted into the corresponding object
:: The value is interpreted as a script code (or name, if {method = "name"}) and converted into the corresponding object
   (see [[Module:scripts]]). If the code or name is invalid, then an error is thrown.
   (see [[Module:scripts]]). If the code or name is invalid, then an error is thrown.
:; {type = "title"}
:: The value is interpreted as a page title and converted into the corresponding object (see the [[mw:Extension:Scribunto/Lua_reference_manual#Title_library|Title library]]). If the page title is invalid, then an error is thrown; by default, external titles (i.e. those on other wikis) are not treated as valid. Options are:
::; {namespace = n}
::: The default namespace, where {n} is a namespace number; this is treated as {0} (the mainspace) if not specified.
::; {allow_external = true}
::: External titles are treated as valid.
::; {prefix = "namespace override"} (default)
::: The default namespace prefix will be prefixed to the value is already prefixed by a namespace prefix. For instance, the input {"Foo"} with namespace {10} returns {"Template:Foo"}, {"Wiktionary:Foo"} returns {"Wiktionary:Foo"}, and {"Template:Foo"} returns {"Template:Foo"}. Interwiki prefixes cannot act as overrides, however: the input {"fr:Foo"} returns {"Template:fr:Foo"}.
::; {prefix = "force"}
::: The default namespace prefix will be prefixed unconditionally, even if the value already appears to be prefixed. This is the way that {{tl|#invoke:}} works when calling modules from the module namespace ({828}): the input {"Foo"} returns {"Module:Foo"}, {"Wiktionary:Foo"} returns {"Module:Wiktionary:Foo"}, and {"Module:Foo"} returns {"Module:Module:Foo"}.
::; {prefix = "full override"}
::: The same as {prefix = "namespace override"}, except that interwiki prefixes can also act as overrides. For instance, {"el:All topics"} with namespace {14} returns {"el:Category:All topics"}. Due to the limitations of MediaWiki, only the first prefix in the value may act as an override, so the namespace cannot be overridden if the first prefix is an interwiki prefix: e.g. {"el:Template:All topics"} with namespace {14} returns {"el:Category:Template:All topics"}.
:; {type = "qualifier"}
:; {type = "qualifier"}
:: The value is interpreted as a qualifier and converted into the correct format for passing into `format_qualifiers()`
:: The value is interpreted as a qualifier and converted into the correct format for passing into `format_qualifiers()`
   in [[Module:qualifiers]] (which currently just means converting it to a one-item list).
   in [[Module:qualifier]] (which currently just means converting it to a one-item list).
:; {type = "labels"}
:; {type = "labels"}
:: The value is interpreted as a comma-separated list of labels and converted into the correct format for passing into
:: The value is interpreted as a comma-separated list of labels and converted into the correct format for passing into
Line 143: Line 312:
   [[Module:references]], and converted into a list of objects of the form accepted by `format_references()` in the same
   [[Module:references]], and converted into a list of objects of the form accepted by `format_references()` in the same
   module. If a syntax error is found in the reference format, an error is thrown.
   module. If a syntax error is found in the reference format, an error is thrown.
:; {type = function(val) ... end}
:: `type` may be set to a function (or callable table), which must take the argument value as its sole argument, and must
  output one of the other recognized types. This is particularly useful for lists (see below), where certain values need
  to be interpreted differently to others.
; {list =}
; {list =}
: Treat the parameter as a list of values, each having its own parameter name, rather than a single value. The
: Treat the parameter as a list of values, each having its own parameter name, rather than a single value. The
Line 166: Line 339:
: The value of the parameter is a delimiter-separated list of individual raw values. The resulting field in `args` will
: The value of the parameter is a delimiter-separated list of individual raw values. The resulting field in `args` will
   be a Lua list (i.e. a table with numeric indices) of the converted values. If {sublist = true} is given, the values
   be a Lua list (i.e. a table with numeric indices) of the converted values. If {sublist = true} is given, the values
   will be split on comma (possibly with whitespace on one or both sides of the comma, which is ignored). Otherwise, the
   will be split on commas (possibly with whitespace on one or both sides of the comma, which is ignored). If
   value of `sublist` should be either a Lua pattern specifying the delimiter(s) to split on or a function to do the
  {sublist = "comma without whitespace"} is given, the values will be split on commas which are not followed by whitespace,
  splitting, which is passed two values (the value to split and a function to signal an error) and should return a list
   and which aren't preceded by an escaping backslash. Otherwise, the value of `sublist` should be either a Lua pattern
  of the split values. A function `split_on_comma_without_whitespace()` is provided in [[Module:parameters]] to split on
  specifying the delimiter(s) to split on or a function (or callable table) to do the splitting, which is passed two values
  commas not followed by whitespace, while considering commas followed by whitespace part of the argument.
  (the value to split and a function to signal an error) and should return a list of the split values.
; {convert =}
; {convert =}
: If given, this specifies a function to convert the raw parameter value into the Lua object used during further
: If given, this specifies a function (or callable table) to convert the raw parameter value into the Lua object used
  processing. The function is passed two arguments, the raw parameter value itself and a function used to signal an
  during further processing. The function is passed two arguments, the raw parameter value itself and a function used to
  error during parsing or conversion, and should return one value, the converted parameter. The error-signaling function
  signal an error during parsing or conversion, and should return one value, the converted parameter. The error-signaling
   contains the name and raw value of the parameter embedded into the message it generates, so these do not need to
   function contains the name and raw value of the parameter embedded into the message it generates, so these do not need to
   specified in the message passed into it. If `type` is specified in conjunction with `convert`, the processing by
   specified in the message passed into it. If `type` is specified in conjunction with `convert`, the processing by
   `type` happens first. If `sublist` is given in conjunction with `convert`, the raw parameter value will be split
   `type` happens first. If `sublist` is given in conjunction with `convert`, the raw parameter value will be split
   appropriately and `convert` called on each resulting item.
   appropriately and `convert` called on each resulting item.
; {allow_hex = true}
: When used in conjunction with {type = "number"}, allows hexadecimal numbers as inputs, in the format {"0x100"} (which is
  not case-sensitive).
; {family = true}
; {family = true}
: When used in conjunction with {type = "language"}, allows [[Wiktionary:Language families|language family codes]] to be
: When used in conjunction with {type = "language"}, allows [[Wiktionary:Language families|language family codes]] to be
Line 215: Line 391:
   indicate that it is in a different language). When this is used, the resulting table will contain an additional named
   indicate that it is in a different language). When this is used, the resulting table will contain an additional named
   value, `default`, which contains the value for the indexless argument.
   value, `default`, which contains the value for the indexless argument.
; {demo = true}
: This is used as a way to ensure that the parameter is only enabled on the template's own page (and its documentation page), and in the User: namespace; otherwise, it will be treated as an unknown parameter. This should only be used if special settings are required to showcase a template in its documentation (e.g. adjusting the pagename or disabling categorization). In most cases, it should be possible to do this without using demo parameters, but they may be required if a template/documentation page also contains real uses of the same template as well (e.g. {{tl|shortcut}}), as a way to distinguish them.
]==]
]==]


-------------------------------------- Some splitting functions -----------------------------
-- Returns true if the current page is a template or module containing the current {{#invoke}}.
 
-- If the include_documentation argument is given, also returns true if the current page is either page's docuemntation page.
--[==[
local own_page, own_page_or_documentation
Split an argument on comma, but not comma followed by whitespace. Can be used e.g. as the value of the `sublist` field
local function is_own_page(include_documentation)
in the `params` structure.
if own_page == nil then
]==]
if current_namespace == nil then
function export.split_on_comma_without_whitespace(val)
local current_title = mw_title.getCurrentTitle()
if val:find(",%s") or val:find("\\") then
current_title_text, current_namespace = current_title.prefixedText, current_title.namespace
return require(parse_utilities_module).split_on_comma(val)
end
else
local frame = current_namespace == 828 and mw.getCurrentFrame() or
return rsplit(val, ",")
current_namespace == 10 and mw.getCurrentFrame():getParent()
if frame then
local frame_title_text = frame:getTitle()
own_page = current_title_text == frame_title_text
own_page_or_documentation = own_page or current_title_text == frame_title_text .. "/documentation"
else
own_page, own_page_or_documentation = false, false
end
end
end
return include_documentation and own_page_or_documentation or own_page
end
end


-------------------------------------- Value conversion -----------------------------
-------------------------------------- Some helper functions -----------------------------
 
-- For a list parameter `name` and corresponding value `list_name` of the `list` field (which should have the same value
-- as `name` if `list = true` was given), generate a pattern to match parameters of the list and store the pattern as a
-- key in `patterns`, with corresponding value set to `name`. For example, if `list_name` is "tr", the pattern will
-- match "tr" as well as "tr1", "tr2", ..., "tr10", "tr11", etc. If the `list_name` contains a \1 in it, the numeric
-- portion goes in place of the \1. For example, if `list_name` is "f\1accel", the pattern will match "faccel",
-- "f1accel", "f2accel", etc. Any \1 in `name` is removed before storing into `patterns`.
local function save_pattern(name, list_name, patterns)
name = type(name) == "string" and gsub(name, "\1", "") or name
if match(list_name, "\1") then
patterns["^" .. gsub(pattern_escape(list_name), "\1", "([1-9]%%d*)") .. "$"] = name
else
patterns["^" .. pattern_escape(list_name) .. "([1-9]%d*)$"] = name
end
end


-- Convert a list in `list` to a string, separating the final element from the preceding one(s) by `conjunction`. If
-- Convert a list in `list` to a string, separating the final element from the preceding one(s) by `conjunction`. If
Line 254: Line 425:
local function concat_list(list, conjunction, dump_vals)
local function concat_list(list, conjunction, dump_vals)
if dump_vals then
if dump_vals then
for i = 1, #list do
for k, v in pairs(list) do
list[i] = dump(list[i])
list[k] = dump(v)
end
end
end
end
Line 263: Line 434:
-- Helper function for use with convert_val_error(). Format a list of possible choices using `concat_list` and
-- Helper function for use with convert_val_error(). Format a list of possible choices using `concat_list` and
-- conjunction "or", displaying "either " before the choices if there's more than one.
-- conjunction "or", displaying "either " before the choices if there's more than one.
local function format_choice_list(typ)
local function format_choice_list(param_type)
return (#typ > 1 and "either " or "") .. concat_list(typ, " or ")
return (#param_type > 1 and "either " or "") .. concat_list(param_type, " or ")
end
end


-- Signal an error for a value `val` that is not of the right typ `typ` (which is either a string specifying a type or
-- Split an argument on comma, but not comma followed by whitespace.
-- a list of possible values, in the case where `set` was used). `name` is the name of the parameter and can be a
local function split_on_comma_without_whitespace(val)
-- function to signal an error (which is assumed to automatically display the parameter's name and value). `seetext` is
if find(val, "\\", 1, true) or match(val, ",%s") then
-- an optional additional explanatory link to display (e.g. [[WT:LOL]], the list of possible languages and codes).
return split_on_comma(val)
local function convert_val_error(val, name, typ, seetext)
if type(name) == "function" then
if type(typ) == "table" then
typ = "choice, must be " .. format_choice_list(typ)
end
name(("Invalid %s; the value %s is not valid%s"):format(typ, val, seetext and "; see " .. seetext or ""))
else
if type(typ) == "table" then
typ = "must be " .. format_choice_list(typ)
else
typ = "should be a valid " .. typ
end
error(("Parameter %s %s; the value %s is not valid.%s"):format(dump(name), typ, dump(val),
seetext and " See " .. seetext .. "." or ""))
end
end
return split(val, ",")
end
end


-- Convert a value that is not a string or number to a string using mw.dumpObject(), for debugging purposes.
-- Convert a value that is not a string or number to a string using mw.dumpObject(), for debugging purposes.
local function dump_if_unusual(val)
local function dump_if_unusual(val)
return (type(val) == "string" or type(val) == "number") and val or dump(val)
local val_type = type(val)
return (val_type == "string" or val_type == "number") and val or dump(val)
end
end


Line 299: Line 458:
if rawval == processed then
if rawval == processed then
return msg
return msg
end
return format("%s (processed value %s)", msg, dump_if_unusual(processed))
end
-------------------------------------- Error handling -----------------------------
local function process_error(fmt, ...)
local args = {...}
for i, val in ipairs(args) do
args[i] = dump(val)
end
if type(fmt) == "table" then
-- hacky signal that we're called from internal_process_error(), and not to omit stack frames
return error(format(fmt[1], unpack(args)))
end
return error(format(fmt, unpack(args)), 3)
end
local function internal_process_error(fmt, ...)
process_error({"Internal error in `params` table: " .. fmt}, ...)
end
-- Check that a parameter or argument is in the form form Scribunto normalizes input argument keys into (e.g. 1 not "1", "foo" not " foo "). Otherwise, it won't be possible to normalize inputs in the expected way. Unless is_argument is set, also check that the name only contains one placeholder at most, and that strings don't resolve to numeric keys once the placeholder has been substituted.
local function validate_name(name, desc, extra_name, is_argument)
local normalized = scribunto_param_key(name)
if name and name == normalized then
if is_argument or type(name) ~= "string" then
return
end
local placeholder = find(name, "\1", 1, true)
if not placeholder then
return
elseif find(name, "\1", placeholder + 1, true) then
error(format(
'Expected %s to only contain one placeholder, but saw %s',
extra_name and (desc .. dump(extra_name)) or desc, dump(name)
))
end
local first_name = gsub(name, "\1", "1")
normalized = scribunto_param_key(first_name)
if first_name == normalized then
return
end
error(format(
'%s cannot resolve to numeric parameters once any placeholder has been substituted, but %s resolves to %s',
extra_name and (desc .. dump(extra_name)) or desc, dump(name), dump(normalized)
))
elseif normalized == nil then
error(format(
'Expected %s to be of type string or number, but saw %s',
extra_name and (desc .. dump(extra_name)) or desc, type(name)
))
end
error(format(
"Expected %s to be Scribunto-compatible: %s (a %s) should be %s (a %s)",
extra_name and (desc .. dump(extra_name)) or desc, dump(name), type(name), dump(normalized), type(normalized)
))
end
-- TODO: give ranges instead of long lists, if possible.
local function params_list_error(params, msg)
local list, n = {}, 0
for name in sorted_pairs(params) do
n = n + 1
list[n] = name
end
error(format(
"Parameter%s %s.",
format(n == 1 and " %s is" or "s %s are", concat_list(list, " and ", true)),
msg
), 3)
end
-- Signal an error for a value `val` that is not of the right type `param_type` (which is either a string specifying a type or
-- a list of possible values, in the case where `set` was used). `name` is the name of the parameter and can be a
-- function to signal an error (which is assumed to automatically display the parameter's name and value). `seetext` is
-- an optional additional explanatory link to display (e.g. [[WT:LOL]], the list of possible languages and codes).
local function convert_val_error(val, name, param_type, seetext)
if is_callable(name) then
if type(param_type) == "table" then
param_type = "choice, must be " .. format_choice_list(param_type)
end
name(format("Invalid %s; the value %s is not valid%s", param_type, val, seetext and "; see " .. seetext or ""))
else
else
return ("%s (processed value %s)"):format(msg, dump_if_unusual(processed))
if type(param_type) == "table" then
param_type = "must be " .. format_choice_list(param_type)
else
param_type = "should be a valid " .. param_type
end
error(format("Parameter %s %s; the value %s is not valid.%s", dump(name), param_type, dump(val),
seetext and " See " .. seetext .. "." or ""))
end
end
end
end
Line 308: Line 556:
-- along with the parameter's name and value.
-- along with the parameter's name and value.
local function make_parse_err(val, name)
local function make_parse_err(val, name)
if type(name) == "function" then
if is_callable(name) then
return name
return name
else
end
return function(msg)
return function(msg)
error(("%s: parameter %s=%s"):format(msg, name, val))
error(format("%s: parameter %s=%s", msg, name, val))
end
end
end
end
end


-- A reimplementation of ipairs() for use in a single-variable for-loop (like with gsplit()) instead of a two-variable
-------------------------------------- Value conversion -----------------------------
-- for-loop (like with ipairs()). If we changed the return statement below to `return index, list[index]`, we'd get
 
-- ipairs() directly.
-- For a list parameter `name` and corresponding value `list_name` of the `list` field (which should have the same value
local function iterate_over_list(list)
-- as `name` if `list = true` was given), generate a pattern to match parameters of the list and store the pattern as a
  local index, len = 0, #list
-- key in `patterns`, with corresponding value set to `name`. For example, if `list_name` is "tr", the pattern will
  return function()
-- match "tr" as well as "tr1", "tr2", ..., "tr10", "tr11", etc. If the `list_name` contains a \1 in it, the numeric
      index = index + 1
-- portion goes in place of the \1. For example, if `list_name` is "f\1accel", the pattern will match "faccel",
      if index <= len then
-- "f1accel", "f2accel", etc. Any \1 in `name` is removed before storing into `patterns`.
        return list[index]
local function save_pattern(name, list_name, patterns)
      end
name = type(name) == "string" and gsub(name, "\1", "") or name
  end
if find(list_name, "\1", 1, true) then
patterns["^" .. gsub(pattern_escape(list_name), "\1", "([1-9]%%d*)") .. "$"] = name
else
patterns["^" .. pattern_escape(list_name) .. "([1-9]%d*)$"] = name
list_name = list_name .. "\1"
end
validate_name(list_name, "the list field of parameter ", name)
return patterns
end
end


-- A helper function for use with `sublist`. It is an iterator function for use in a for-loop that returns split
-- A helper function for use with `sublist`. It is an iterator function for use in a for-loop that returns split
-- elements of `val` using `sublist` (a Lua split pattern; boolean `true` to split on commas optionally surrounded by
-- elements of `val` using `sublist` (a Lua split pattern; boolean `true` to split on commas optionally surrounded by
-- whitespace; or a function to do the splitting, which is passed two values, the value to split and a function to
-- whitespace; "comma without whitespace" to split only on commas not followed by whitespace which have not been escaped
-- by a backslash; or a function to do the splitting, which is passed two values, the value to split and a function to
-- signal an error, and should return a list of the split elements). `name` is the parameter name or error-signaling
-- signal an error, and should return a list of the split elements). `name` is the parameter name or error-signaling
-- function passed into convert_val().
-- function passed into convert_val().
local function split_sublist(val, name, sublist)
local function split_sublist(val, name, sublist)
sublist = sublist == true and "%s*,%s*" or sublist
if sublist == true then
if type(sublist) == "string" then
return gsplit(val, "%s*,%s*")
elseif sublist == "comma without whitespace" then
sublist = split_on_comma_without_whitespace
elseif type(sublist) == "string" then
return gsplit(val, sublist)
return gsplit(val, sublist)
elseif type(sublist) == "function" then
elseif not is_callable(sublist) then
local retval = sublist(val, make_parse_err(val, name))
error(format('Internal error: Expected `sublist` to be of type "string" or "function" or boolean `true`, but saw %s', dump(sublist)))
return iterate_over_list(retval)
else
error(('Internal error: Expected `sublist` to be of type "string" or "function" or boolean `true`, but saw %s'):
format(dump(sublist)))
end
end
return iterate_list(sublist(val, make_parse_err(val, name)))
end
end


-- For parameter named `name` with value `val` and param spec `param`, if the `set` field is specified, verify that the
-- For parameter named `name` with value `val` and param spec `param`, if the `set` field is specified, verify that the
-- value is one of the one specified in `set`, and throw an error otherwise. `name` is taken directly from the
-- value is one of the one specified in `set`, and throw an error otherwise. `name` is taken directly from the
-- corresponding parameter passed into convert_val() and may be a function to signal an error. Optional `typ` is a
-- corresponding parameter passed into convert_val() and may be a function to signal an error. Optional `param_type` is a
-- string specifying the conversion type of `val` and is used for special-casing: If `typ` is "boolean", an internal
-- string specifying the conversion type of `val` and is used for special-casing: If `param_type` is "boolean", an internal
-- error is thrown (since `set` cannot be used in conjunction with booleans) and if `typ` is "number", no checking
-- error is thrown (since `set` cannot be used in conjunction with booleans) and if `param_type` is "number", no checking
-- happens because in this case `set` contains numbers and is checked inside the number conversion function itself,
-- happens because in this case `set` contains numbers and is checked inside the number conversion function itself,
-- after converting `val` to a number.
-- after converting `val` to a number.
local function check_set(val, name, param, typ)
local function check_set(val, name, param, param_type)
if typ == "boolean" then
if param_type == "boolean" then
error(('Internal error: Cannot use `set` with `type = "%s"`'):format(typ))
error(format('Internal error: Cannot use `set` with `type = "%s"`', param_type))
end
elseif param_type == "number" then
if typ == "number" then
-- Needs to be special cased because the check happens after conversion to numbers.
-- Needs to be special cased because the check happens after conversion to numbers.
return
return
Line 378: Line 632:


local function convert_language(val, name, param, allow_etym)
local function convert_language(val, name, param, allow_etym)
local lang = require(languages_module)[param.method == "name" and "getByCanonicalName" or "getByCode"](val, nil, allow_etym, param.family)
local method, func = param.method
if method == nil or method == "code" then
func, method = get_language_by_code, "code"
elseif method == "name" then
func, method = get_language_by_name, "name"
else
error(format('Internal error: Expected `method` for type `language` to be "code", "name" or undefined, but saw %s', dump(method)))
end
local lang = func(val, nil, allow_etym, param.family)
if lang then
if lang then
return lang
return lang
end
end
local list = {"language"}
local list, links = {"language"}, {"[[WT:LOL]]"}
local links = {"[[WT:LOL]]"}
if allow_etym then
if allow_etym then
insert(list, "etymology language")
insert(list, "etymology language")
Line 392: Line 653:
insert(links, "[[WT:LOF]]")
insert(links, "[[WT:LOF]]")
end
end
convert_val_error(val, name, concat_list(list, " or ") .. " " .. (param.method == "name" and "name" or "code"),
convert_val_error(val, name, concat_list(list, " or ") .. " " .. (method == "name" and "name" or "code"), concat_list(links, " and "))
concat_list(links, " and "))
end
end
 
--[==[ func: export.convert_val(val, name, param)
-- TODO: validate parameter specs separately, as it's making the handler code really messy at the moment.
Convert a parameter value according to the associated specs listed in the `params` table passed to
local type_handlers = setmetatable({
[[Module:parameters]]. `val` is the value to convert for a parameter whose name is `name` (used only in error messages).
`param` is the spec (the value part of the `params` table for the parameter). In place of passing in the parameter name,
`name` can be a function that throws an error, displaying the specified message along with the parameter name and value.
This function processes all the conversion-related fields in `param`, including `type`, `set`, `sublist`, `convert`,
etc. It returns the converted value.
]==]
local convert_val = setmetatable({
["boolean"] = function(val)
["boolean"] = function(val)
return yesno(val, true)
return yesno(val, true)
end,
end,
 
["family"] = function(val, name, param)
["family"] = function(val, name, param)
return require(families_module)[param.method == "name" and "getByCanonicalName" or "getByCode"](val) or
local method, func = param.method
convert_val_error(val, name, "family " .. (param.method == "name" and "name" or "code"), "[[WT:LOF]]")
if method == nil or method == "code" then
func, method = get_family_by_code, "code"
elseif method == "name" then
func, method = get_family_by_name, "name"
else
error(format('Internal error: Expected `method` for type `family` to be "code", "name" or undefined, but saw %s', dump(method)))
end
return func(val) or convert_val_error(val, name, "family " .. method, "[[WT:LOF]]")
end,
end,
 
["labels"] = function(val, name, param)
["labels"] = function(val, name, param)
-- FIXME: Should be able to pass in a parse_err function.
-- FIXME: Should be able to pass in a parse_err function.
return require(labels_module).split_labels_on_comma(val)
return split_labels_on_comma(val)
end,
 
["references"] = function(val, name, param)
return require(references_module).parse_references(val, make_parse_err(val, name))
end,
 
["qualifier"] = function(val, name, param)
return {val}
end,
end,


Line 430: Line 682:
return convert_language(val, name, param, true)
return convert_language(val, name, param, true)
end,
end,
 
["full language"] = function(val, name, param)
["full language"] = function(val, name, param)
return convert_language(val, name, param, false)
return convert_language(val, name, param)
end,
end,
 
["number"] = function(val, name, param)
["number"] = function(val, name, param)
if type(val) == "number" then
local allow_hex = param.allow_hex
return val
if allow_hex and allow_hex ~= true then
error(format('Internal error: Expected `allow_hex` for type `number` to be of type "boolean" or undefined, but saw %s', dump(allow_hex)))
end
local num = tonumber(val)
-- Avoid converting inputs like "nan" or "inf", and disallow 0x hex inputs unless explicitly enabled
-- with `allow_hex`.
if not (num and is_finite_real_number(num) and (allow_hex or not match(val, "^[+-]?0[Xx]%x*%.?%x*$"))) then
convert_val_error(val, name, (allow_hex and "decimal or hexadecimal " or "") .. "number")
end
end
-- Avoid converting inputs like "nan" or "inf".
val = tonumber(val:match("^[+%-]?%d+%.?%d*")) or convert_val_error(val, name, "number")
if param.set then
if param.set then
-- Don't pass in "number" here; otherwise no checking will happen.
-- Don't pass in "number" here; otherwise no checking will happen.
check_set(val, name, param)
check_set(num, name, param)
end
end
return val
return num
end,
["qualifier"] = function(val, name, param)
return {val}
end,
end,
["references"] = function(val, name, param)
return parse_references(val, make_parse_err(val, name))
end,
["script"] = function(val, name, param)
["script"] = function(val, name, param)
return require(scripts_module)[param.method == "name" and "getByCanonicalName" or "getByCode"](val) or
local method, func = param.method
convert_val_error(val, name, "script " .. (param.method == "name" and "name" or "code"), "[[WT:LOS]]")
if method == nil or method == "code" then
func, method = get_script_by_code, "code"
elseif method == "name" then
func, method = get_script_by_name, "name"
else
error(format('Internal error: Expected `method` for type `script` to be "code", "name" or undefined, but saw %s', dump(method)))
end
return func(val) or convert_val_error(val, name, "script " .. method, "[[WT:LOS]]")
end,
end,
 
["string"] = function(val, name, param)
["string"] = function(val, name, param) -- To be removed as unnecessary.
return val
return val
end,
end,
 
["wikimedia language"] = function(val, name, param)
-- TODO: add support for resolving to unsupported titles.
local fallback = param.method == "fallback"
-- TODO: split this into "page name" (i.e. internal) and "link target" (i.e. external as well), which is more intuitive.
local lang = require(wikimedia_languages_module)[fallback and "getByCodeWithFallback" or "getByCode"](val)
["title"] = function(val, name, param)
if lang then
local namespace = param.namespace
return lang
if namespace == nil then
namespace = 0
else
local valid_type = type(namespace) ~= "number" and 'of type "number" or undefined' or
not namespaces[namespace] and "a valid namespace number" or
nil
if valid_type then
error(format('Internal error: Expected `namespace` for type `title` to be %s, but saw %s', valid_type, dump(namespace)))
end
end
-- Decode entities. WARNING: mw.title.makeTitle must be called with `decoded` (as it doesn't decode) and mw.title.new must be called with `val` (as it does decode, so double-decoding needs to be avoided).
local decoded, prefix, title = decode_entities(val), param.prefix
-- If the input is a fragment, treat the title as the current title with the input fragment.
if sub(decoded, 1, 1) == "#" then
-- If prefix is "force", only get the current title if it's in the specified namespace. current_title includes the namespace prefix.
if current_namespace == nil then
local current_title = mw_title.getCurrentTitle()
current_title_text, current_namespace = current_title.prefixedText, current_title.namespace
end
if not (prefix == "force" and namespace ~= current_namespace) then
title = new_title(current_title_text .. val)
end
elseif prefix == "force" then
-- Unconditionally add the namespace prefix (mw.title.makeTitle).
title = make_title(namespace, decoded)
elseif prefix == "full override" then
-- The first input prefix will be used as an override (mw.title.new). This can be a namespace or interwiki prefix.
title = new_title(val, namespace)
elseif prefix == nil or prefix == "namespace override" then
-- Only allow namespace prefixes to override. Interwiki prefixes therefore need to be treated as plaintext (e.g. "el:All topics" with namespace 14 returns "el:Category:All topics", but we want "Category:el:All topics" instead; if the former is really needed, then the input ":el:Category:All topics" will work, as the initial colon overrides the namespace). mw.title.new can take namespace names as well as numbers in the second argument, and will throw an error if the input isn't a valid namespace, so this can be used to determine if a prefix is for a namespace, since mw.title.new will return successfully only if there's either no prefix or the prefix is for a valid namespace (in which case we want the override).
local success
success, title = pcall(new_title, val, match(decoded, ".-%f[:]") or namespace)
-- Otherwise, get the title with mw.title.makeTitle, which unconditionally adds the namespace prefix, but behaves like mw.title.new if the namespace is 0.
if not success then
title = make_title(namespace, decoded)
end
else
error(format('Internal error: Expected `prefix` for type `title` to be "force", "full override", "namespace override" or undefined, but saw %s', dump(prefix)))
end
local allow_external = param.allow_external
if allow_external == true then
return title or convert_val_error(val, name, "Wiktionary or external page title")
elseif not allow_external then
return title and is_internal_title(title) and title or convert_val_error(val, name, "Wiktionary page title")
end
end
local list = {"wikimedia language"}
error(format('Internal error: Expected `allow_external` for type `title` to be of type "boolean" or undefined, but saw %s', dump(allow_external)))
if fallback then
end,
insert(list, "language")
 
["Wikimedia language"] = function(val, name, param)
local fallback = param.fallback
if fallback == true then
return get_wm_lang_by_code_with_fallback(val) or convert_val_error(val, name, "Wikimedia language or language code")
elseif not fallback then
return get_wm_lang_by_code(val) or convert_val_error(val, name, "Wikimedia language code")
end
end
convert_val_error(val, name, concat_list(list, " or ") .. " code")
error(format('Internal error: Expected `fallback` for type `Wikimedia language` to be of type "boolean" or undefined, but saw %s', dump(fallback)))
end,
end,
}, {
}, {
__call = function(self, val, name, param)
-- TODO: decode HTML entities in all input values. Non-trivial to implement, because we need to avoid any downstream functions decoding the output from this module, which would be double-decoding. Note that "title" has this implemented already, and it needs to have both the raw input and the decoded input to avoid double-decoding by me.title.new, so any implementation can't be as simple as decoding in __call then passing the result to the handler.
local typ = param.type or "string"
__call = function(self, val, name, param, param_type)
local func, sublist = self[typ], param.sublist
local val_type = type(val)
if not func then
-- TODO: check this for all possible parameter types.
error("Internal error: " .. dump(typ) .. " is not a recognized parameter type.")
if val_type == param_type then
elseif sublist then
return val
local retlist = {}
-- TODO: throw an internal error.
if type(val) ~= "string" then
end
error("Internal error: " .. dump(val) .. " is not a string.")
local func = self[param_type]
end
if func == nil then
if param.convert then
error(format("Internal error: %s is not a recognized parameter type.", dump(param_type)))
local thisval, insval
end
local thisindex = 0
return func(val, name, param)
local parse_err
end
if type(name) == "function" then
})
-- We assume the passed-in error function in `name` already shows the parameter name and raw value.
 
parse_err = function(msg)
--[==[ func: export.convert_val(val, name, param)
name(("%s: item #%s=%s"):format(msg_with_processed(msg, thisval, insval), thisindex,
Convert a parameter value according to the associated specs listed in the `params` table passed to
thisval))
[[Module:parameters]]. `val` is the value to convert for a parameter whose name is `name` (used only in error messages).
end
`param` is the spec (the value part of the `params` table for the parameter). In place of passing in the parameter name,
else
`name` can be a function that throws an error, displaying the specified message along with the parameter name and value.
parse_err = function(msg)
This function processes all the conversion-related fields in `param`, including `type`, `set`, `sublist`, `convert`,
error(("%s: item #%s=%s of parameter %s=%s"):format(msg_with_processed(msg, thisval, insval),
etc. It returns the converted value.
thisindex, thisval, name, val))
]==]
end
local function convert_val(val, name, param)
local param_type = param.type or "string"
-- If param.type is a function, resolve it to a recognized type.
if is_callable(param_type) then
param_type = param_type(val)
end
local sublist = param.sublist
if sublist then
local retlist = {}
if type(val) ~= "string" then
error(format("Internal error: %s is not a string.", dump(val)))
end
if param.convert then
local thisval, insval
local thisindex = 0
local parse_err
if is_callable(name) then
-- We assume the passed-in error function in `name` already shows the parameter name and raw value.
parse_err = function(msg)
name(format("%s: item #%s=%s", msg_with_processed(msg, thisval, insval), thisindex,
thisval))
end
end
for v in split_sublist(val, name, sublist) do
else
thisval = v
parse_err = function(msg)
thisindex = thisindex + 1
error(format("%s: item #%s=%s of parameter %s=%s", msg_with_processed(msg, thisval, insval),
if param.set then
thisindex, thisval, name, val))
check_set(v, name, param, typ)
end
insval = func(v, name, param)
insert(retlist, param.convert(insval, parse_err))
end
end
else
end
for v in split_sublist(val, name, sublist) do
for v in split_sublist(val, name, sublist) do
if param.set then
thisval = v
check_set(v, name, param, typ)
thisindex = thisindex + 1
end
if param.set then
insert(retlist, func(v, name, param))
check_set(v, name, param, param_type)
end
end
insert(retlist, param.convert(type_handlers(v, name, param, param_type), parse_err))
end
end
return retlist
else
else
if param.set then
for v in split_sublist(val, name, sublist) do
check_set(val, name, param, typ)
if param.set then
check_set(v, name, param, param_type)
end
insert(retlist, type_handlers(v, name, param, param_type))
end
end
local retval = func(val, name, param)
end
if param.convert then
return retlist
local parse_err
else
if type(name) == "function" then
if param.set then
-- We assume the passed-in error function in `name` already shows the parameter name and raw value.
check_set(val, name, param, param_type)
if retval == val then
end
-- This is an optimization to avoid creating a closure. The second arm works correctly even
local retval = type_handlers(val, name, param, param_type)
-- when retval == val.
if param.convert then
parse_err = name
local parse_err
else
if is_callable(name) then
parse_err = function(msg)
-- We assume the passed-in error function in `name` already shows the parameter name and raw value.
name(msg_with_processed(msg, val, retval))
if retval == val then
end
-- This is an optimization to avoid creating a closure. The second arm works correctly even
end
-- when retval == val.
parse_err = name
else
else
parse_err = function(msg)
parse_err = function(msg)
error(("%s: parameter %s=%s"):format(msg_with_processed(msg, val, retval), name, val))
name(msg_with_processed(msg, val, retval))
end
end
end
end
retval = param.convert(retval, parse_err)
else
parse_err = function(msg)
error(format("%s: parameter %s=%s", msg_with_processed(msg, val, retval), name, val))
end
end
end
return retval
retval = param.convert(retval, parse_err)
end
end
return retval
end
end
})
end
export.convert_val = convert_val -- used by [[Module:parameter utilities]]
export.convert_val = convert_val -- used by [[Module:parameter utilities]]


local function process_error(fmt, ...)
local function unknown_param(name, val, args_unknown)
local args = {...}
args_unknown[name] = val
for i, val in ipairs(args) do
return args_unknown
args[i] = dump(val)
end
 
local function check_string_param(param_type, name, tag)
if param_type and param_type ~= "string" then
internal_process_error(
"%s cannot be set unless the parameter has the type %s (the default): parameter %s has the type %s.",
tag, "string", name, param_type
)
end
end
if type(fmt) == "table" then
end
-- hacky signal that we're called from internal_process_error(), and not to omit stack frames
 
return error(fmt[1]:format(unpack(args)))
local function handle_holes(param, val, name)
-- Iterate up the list, and throw an error if a hole is found.
if param.disallow_holes then
for i = 1, val.maxindex do
if val[i] == nil then
local listname = param.list
if type(listname) == "string" then
listname = dump(listname)
elseif type(name) == "number" then
i = i + name - 1 -- Absolute index.
listname = "numeric"
else
listname = dump(name)
end
process_error(
"Item %d in the list of %s parameters cannot be empty, because the list must be contiguous.",
i, listname
)
end
end
-- If `allow_holes` is set, there's nothing to do. This is placed after
-- `disallow_holes`, so that the latter takes priority.
elseif param.allow_holes then
return
-- Otherwise, remove any holes. Use num_keys to get a list of numerical keys
-- instead of iterating from 1 to `maxindex`, as it could be enormous if
-- there is a huge hole in the list.
else
else
return error(fmt:format(unpack(args)), 3)
local keys, i = num_keys(val), 0
while true do
i = i + 1
local key = keys[i]
if key == nil then
break
elseif i ~= key then
val[i], val[key] = val[key], nil
end
end
end
end
-- Some code depends on only numeric params being present when no holes are
-- allowed (e.g. by checking for the presence of arguments using next()), so
-- remove `maxindex`.
val.maxindex = nil
end
end


local function internal_process_error(fmt, ...)
-- If both `template_default` and `default` are given, `template_default` takes precedence, but only on the template or
fmt = "Internal error in `params` table: " .. fmt
-- module page. This means a different default can be specified for the template or module page example. However,
process_error({fmt}, ...)
-- `template_default` doesn't apply if any args are set, which helps (somewhat) with examples on documentation pages
-- transcluded into the template page. HACK: We still run into problems on documentation pages transcluded into the
-- template page when pagename= is set. Check this on the assumption that pagename= is fairly standard.
local function convert_default_val(name, param, pagename_set, any_args_set)
if not pagename_set then
local val = param.template_default
if val ~= nil and not any_args_set and is_own_page() then
return convert_val(val, name, param)
end
end
local val = param.default
if val ~= nil then
return convert_val(val, name, param)
end
end
end


Line 584: Line 988:
function export.process(args, params, return_unknown)
function export.process(args, params, return_unknown)
-- Process parameters for specific properties
-- Process parameters for specific properties
local args_new = {}
local args_new, args_unknown, any_args_set, param_types, required, patterns, list_args, index_list,
local required = {}
args_placeholders, n_ph = {}
local seen = {}
 
local patterns = {}
local names_with_equal_sign = {}
local list_from_index
for name, param in pairs(params) do
for name, param in pairs(params) do
-- Populate required table, and make sure aliases aren't set to required.
validate_name(name, "parameter names")
if param.required then
local param_type = type(param)
if param.alias_of then
if param_types then
internal_process_error(
param_types[param] = param_type
"Parameter %s is an alias of %s, but is also set as a required parameter. Only %s should be set as required.",
else
name, param.alias_of, name)
param_types = {[param] = param_type}
end
required[name] = true
end
end
if param_type == "table" then
-- Convert param.set from a list into a set.
-- Populate required table, and make sure aliases aren't set to required.
-- `seen` prevents double-conversion if multiple parameter keys share the same param table.
if param.required then
local set = param.set
if param.alias_of then
if set and not seen[param] then
internal_process_error(
param.set = list_to_set(set)
"Parameter %s is an alias of %s, but is also set as a required parameter. Only %s should be set as required.",
seen[param] = true
name, param.alias_of, name
end
)
elseif required then
local alias = param.alias_of
required[name] = true
if alias then
else
-- Check that the alias_of is set to a valid parameter.
required = {[name] = true}
if not params[alias] then
end
internal_process_error("Parameter %s is an alias of an invalid parameter.", name)
end
end
-- Check that all the parameters in params are in the form Scribunto normalizes input argument keys into (e.g. 1 not "1", "foo" not " foo "). Otherwise, this function won't be able to normalize the input arguments in the expected way.
 
local normalized = scribunto_param_key(alias)
-- Convert param.set from a list into a set.
if alias ~= normalized then
-- `converted_set` prevents double-conversion if multiple parameter keys share the same param table.
internal_process_error(
-- rawset avoids errors if param has been loaded via mw.loadData; however, it's probably more efficient to preconvert them, and set the `converted_set` key in advance.
"Parameter %s (a " .. type(alias) .. ") given in the alias_of field of parameter %s is not a normalized Scribunto parameter. Should be %s (a " .. type(normalized) .. ").",
local set = param.set
alias, name, normalized)
if set and not param.converted_set then
-- Aliases can't be lists unless the canonical parameter is also a list.
rawset(param, "set", list_to_set(set))
elseif param.list and not params[alias].list then
rawset(param, "converted_set", true)
internal_process_error(
"The list parameter %s is set as an alias of %s, which is not a list parameter.", name, alias)
-- Aliases can't be aliases of other aliases.
elseif params[alias].alias_of then
internal_process_error(
"Alias_of cannot be set to another alias: parameter %s is set as an alias of %s, which is in turn an alias of %s. Set alias_of for %s to %s.",
name, alias, params[alias].alias_of, name, params[alias].alias_of)
end
end
end
 
local listname, alias = param.list, param.alias_of
local normalized = scribunto_param_key(name)
if alias then
if name ~= normalized then
validate_name(alias, "the alias_of field of parameter ", name)
internal_process_error(
-- Check that the alias_of is set to a valid parameter.
"Parameter %s (a " .. type(name) .. ") is not a normalized Scribunto parameter. Should be %s (a " ..
if not params[alias] then
type(normalized) .. ").",
internal_process_error(
name, normalized)
"Parameter %s is an alias of an invalid parameter.",
end
name
)
if param.list then
elseif alias == name then
if not param.alias_of then
internal_process_error(
local key = name
"Parameter %s cannot be an alias of itself.",
if type(name) == "string" then
name
key = gsub(name, "\1", "")
)
end
local main_param = params[alias]
local main_type = param_types[main_param] or type(main_param) -- Might not yet be memoized.
-- Aliases can't be lists unless the canonical parameter is also a list.
if listname and not (main_type == "table" and main_param.list) then
internal_process_error(
"The list parameter %s is set as an alias of %s, which is not a list parameter.", name, alias
)
-- Aliases can't be aliases of other aliases.
elseif main_type == "table" and main_param.alias_of then
internal_process_error(
"alias_of cannot be set to another alias: parameter %s is set as an alias of %s, which is in turn an alias of %s. Set alias_of for %s to %s.",
name, alias, params[alias].alias_of, name, params[alias].alias_of
)
end
end
-- _list is used as a temporary flag.
args_new[key] = {maxindex = 0, _list = param.list}
end
end
 
if type(param.list) == "string" then
if listname then
-- If the list property is a string, then it represents the name
if not alias then
-- to be used as the prefix for list items. This is for use with lists
local key = name
-- where the first item is a numbered parameter and the
if type(name) == "string" then
-- subsequent ones are named, such as 1, pl2, pl3.
key = gsub(name, "\1", "")
save_pattern(name, param.list, patterns)
end
elseif type(name) == "number" then
local list_arg = {maxindex = 0}
if list_from_index then
args_new[key] = list_arg
if list_args == nil then
list_args = {[key] = list_arg}
else
list_args[key] = list_arg
end
end
local list_type = type(listname)
if list_type == "string" then
-- If the list property is a string, then it represents the name
-- to be used as the prefix for list items. This is for use with lists
-- where the first item is a numbered parameter and the
-- subsequent ones are named, such as 1, pl2, pl3.
patterns = save_pattern(name, listname, patterns or {})
elseif listname ~= true then
internal_process_error(
internal_process_error(
"Only one numeric parameter can be a list, unless the list property is a string.")
"The list field for parameter %s must be a boolean, string or undefined, but saw a %s.",
name, list_type
)
elseif type(name) == "number" then
if index_list ~= nil then
internal_process_error(
"Only one numeric parameter can be a list, unless the list property is a string."
)
end
-- If the name is a number, then all indexed parameters from
-- this number onwards go in the list.
index_list = name
else
patterns = save_pattern(name, name, patterns or {})
end
if find(name, "\1", 1, true) then
if args_placeholders then
n_ph = n_ph + 1
args_placeholders[n_ph] = name
else
args_placeholders, n_ph = {name}, 1
end
end
end
-- If the name is a number, then all indexed parameters from
-- this number onwards go in the list.
list_from_index = name
else
save_pattern(name, name, patterns)
end
if match(name, "\1") then
insert(names_with_equal_sign, name)
end
end
elseif param ~= true then
internal_process_error(
"Spec for parameter %s must be a table of specs or the value true, but found %s.",
name, param_type ~= "boolean" and param_type or param
)
end
end
end
end
 
--Process required changes to `params`.
--Process required changes to `params`.
for i = 1, #names_with_equal_sign do
if args_placeholders then
local name = names_with_equal_sign[i]
for i = 1, n_ph do
params[gsub(name, "\1", "")] = params[name]
local name = args_placeholders[i]
params[name] = nil
params[gsub(name, "\1", "")], params[name] = params[name], nil
end
end
end
 
-- Process the arguments
-- Process the arguments
local args_unknown = {}
local max_index
for name, val in pairs(args) do
for name, val in pairs(args) do
local orig_name, raw_type, index, normalized = name, type(name)
any_args_set = true
validate_name(name, "argument names", nil, true)
local orig_name, raw_type, index, canonical = name, type(name)
if raw_type == "number" then
if raw_type == "number" then
if list_from_index ~= nil and name >= list_from_index then
if index_list and name >= index_list then
index = name - list_from_index + 1
index = name - index_list + 1
name = list_from_index
name = index_list
end
end
else
elseif patterns then
-- Does this argument name match a pattern?
-- Does this argument name match a pattern?
for pattern, pname in pairs(patterns) do
for pattern, pname in next, patterns do
index = match(name, pattern)
index = match(name, pattern)
-- It matches, so store the parameter name and the
-- It matches, so store the parameter name and the
Line 708: Line 1,142:
end
end
end
end
 
local param = params[name]
local param = params[name]
 
if param and param.require_index then
-- If the argument is not in the list of parameters, store it in a separate list.
-- Disallow require_index for numeric parameter names, as this doesn't make sense.
if raw_type == "number" then
internal_process_error("Cannot set require_index for numeric parameter %s.", name)
-- If a parameter without the trailing index was found, and
-- require_index is set on the param, set the param to nil to treat it
-- as if it isn't recognized.
elseif not index then
param = nil
end
end
-- If the argument is not in the list of parameters, trigger an error.
-- return_unknown suppresses the error, and stores it in a separate list instead.
if not param then
if not param then
if return_unknown then
args_unknown = unknown_param(name, val, args_unknown or {})
args_unknown[name] = val
elseif param == true then
else
canonical = orig_name
process_error("Parameter %s is not used by this template.", name)
val = trim(val)
if val ~= "" then
-- If the parameter is duplicated, throw an error.
if args_new[name] ~= nil then
process_error(
"Parameter %s has been entered more than once. This is probably because a parameter alias has been used.",
canonical
)
end
args_new[name] = val
end
end
else
else
if param.require_index then
-- Disallow require_index for numeric parameter names, as this doesn't make sense.
if raw_type == "number" then
internal_process_error(
"Cannot set require_index for numeric parameter %s.",
name
)
-- If a parameter without the trailing index was found, and
-- require_index is set on the param, treat it
-- as if it isn't recognized.
elseif not index then
args_unknown = unknown_param(name, val, args_unknown or {})
end
end
-- Check that separate_no_index is not being used with a numeric parameter.
-- Check that separate_no_index is not being used with a numeric parameter.
if param.separate_no_index then
if param.separate_no_index then
if raw_type == "number" then
if raw_type == "number" then
internal_process_error("Cannot set separate_no_index for numeric parameter %s.", name)
internal_process_error(
"Cannot set separate_no_index for numeric parameter %s.",
name
)
elseif type(param.alias_of) == "number" then
elseif type(param.alias_of) == "number" then
internal_process_error(
internal_process_error(
"Cannot set separate_no_index for parameter %s, as it is an alias of numeric parameter %s.",
"Cannot set separate_no_index for parameter %s, as it is an alias of numeric parameter %s.",
name, param.alias_of)
name, param.alias_of
)
end
end
end
end
 
-- If no index was found, use 1 as the default index.
-- If no index was found, use 1 as the default index.
-- This makes list parameters like g, g2, g3 put g at index 1.
-- This makes list parameters like g, g2, g3 put g at index 1.
Line 749: Line 1,198:
index = index or param.separate_no_index and 0 or 1
index = index or param.separate_no_index and 0 or 1
end
end
 
-- Normalize to the canonical parameter name. If it's a list, but the alias is not, then determine the index.
-- Normalize to the canonical parameter name. If it's a list, but the alias is not, then determine the index.
local raw_name = param.alias_of
local raw_name = param.alias_of
Line 755: Line 1,204:
raw_type = type(raw_name)
raw_type = type(raw_name)
if raw_type == "number" then
if raw_type == "number" then
if params[raw_name].list then
local main_param = params[raw_name]
if param_types and param_types[main_param] == "table" and main_param.list then
index = index or param.separate_no_index and 0 or 1
index = index or param.separate_no_index and 0 or 1
normalized = raw_name + index - 1
canonical = raw_name + index - 1
else
else
normalized = raw_name
canonical = raw_name
end
end
name = raw_name
name = raw_name
else
else
name = gsub(raw_name, "\1", "")
name = gsub(raw_name, "\1", "")
if params[name].list then
local main_param = params[name]
if param_types and param_types[main_param] == "table" and main_param.list then
index = index or param.separate_no_index and 0 or 1
index = index or param.separate_no_index and 0 or 1
end
end
if not index or index == 0 then
if not index or index == 0 then
normalized = name
canonical = name
elseif name == raw_name then
elseif name == raw_name then
normalized = name .. index
canonical = name .. index
else
else
normalized = gsub(raw_name, "\1", index)
canonical = gsub(raw_name, "\1", index)
end
end
end
end
else
else
normalized = orig_name
canonical = orig_name
end
end
 
-- Remove leading and trailing whitespace unless allow_whitespace is true.
-- Only recognize demo parameters if this is the current template or module's
if not param.allow_whitespace then
-- page, or its documentation page.
if param.demo and not is_own_page("include_documentation") then
args_unknown = unknown_param(name, val, args_unknown or {})
end
 
-- Remove leading and trailing whitespace unless no_trim is true.
if param.no_trim then
check_string_param(param.type, name, "no_trim")
else
val = trim(val)
val = trim(val)
end
end
 
-- Empty string is equivalent to nil unless allow_empty is true.
-- Empty string is equivalent to nil unless allow_empty is true.
if val == "" and not param.allow_empty then
if param.allow_empty then
check_string_param(param.type, name, "allow_empty")
elseif val == "" then
val = nil
val = nil
-- Track empty parameters, unless (1) allow_empty is set or (2) they're numbered parameters where a higher numbered parameter is also in use (e.g. track {{l|en|term|}}, but not {{l|en||term}}).
if raw_type == "number" and not max_index then
-- Find the highest numbered parameter that's in use/an empty string, as we don't want parameters like 500= to mean we can't track any empty parameters with a lower index than 500.
local n = 0
while args[n + 1] do
n = n + 1
end
max_index = 0
for n = n, 1, -1 do
if args[n] ~= "" then
max_index = n
break
end
end
end
if raw_type ~= "number" or name > max_index then
-- Disable this for now as it causes slowdowns on large pages like [[a]].
-- track("empty parameter")
end
end
end
 
-- Can't use "if val" alone, because val may be a boolean false.
-- Can't use "if val" alone, because val may be a boolean false.
if val ~= nil then
if val ~= nil then
-- Convert to proper type if necessary.
-- Convert to proper type if necessary.
val = convert_val(val, orig_name, params[raw_name] or param)
local main_param = params[raw_name]
if not main_param or (param_types and param_types[main_param] == "table") then
val = convert_val(val, orig_name, main_param or param)
end
 
-- Mark it as no longer required, as it is present.
-- Mark it as no longer required, as it is present.
required[name] = nil
if required then
required[name] = nil
end
 
-- Store the argument value.
-- Store the argument value.
if index then
if index then
Line 822: Line 1,269:
process_error(
process_error(
"Parameter %s has been entered more than once. This is probably because a list parameter has been entered without an index and with index 1 at the same time, or because a parameter alias has been used.",
"Parameter %s has been entered more than once. This is probably because a list parameter has been entered without an index and with index 1 at the same time, or because a parameter alias has been used.",
normalized)
canonical
)
end
end
args_new[name][index] = val
args_new[name][index] = val
 
-- Store the highest index we find.
-- Store the highest index we find.
args_new[name].maxindex = max(index, args_new[name].maxindex)
args_new[name].maxindex = max(index, args_new[name].maxindex)
Line 834: Line 1,282:
end
end
args_new[name][0] = nil
args_new[name][0] = nil
end
end
 
if params[name].list then
if params[name].list then
-- Don't store index 0, as it's a proxy for the default.
-- Don't store index 0, as it's a proxy for the default.
Line 852: Line 1,299:
process_error(
process_error(
"Parameter %s has been entered more than once. This is probably because a parameter alias has been used.",
"Parameter %s has been entered more than once. This is probably because a parameter alias has been used.",
normalized)
canonical
)
end
end
 
if not param.alias_of then
if not param.alias_of then
args_new[name] = val
args_new[name] = val
else
else
if params[param.alias_of].list then
local main_param = params[param.alias_of]
if param_types and param_types[main_param] == "table" and main_param.list then
args_new[param.alias_of][1] = val
args_new[param.alias_of][1] = val
-- Store the highest index we find.
-- Store the highest index we find.
args_new[param.alias_of].maxindex = max(1, args_new[param.alias_of].maxindex)
args_new[param.alias_of].maxindex = max(1, args_new[param.alias_of].maxindex)
Line 871: Line 1,319:
end
end
end
end
 
-- Remove holes in any list parameters if needed.
-- Remove holes in any list parameters if needed.
for name, val in pairs(args_new) do
if list_args then
if type(val) == "table" then
for name, val in next, list_args do
local listname = val._list
handle_holes(params[name], val, name)
if listname then
if params[name].disallow_holes then
local highest = 0
for num, _ in pairs(val) do
if type(num) == "number" and num > 0 and num < huge and floor(num) == num then
highest = max(highest, num)
end
end
for i = 1, highest do
if val[i] == nil then
if type(listname) == "string" then
listname = dump(listname)
elseif type(name) == "number" then
i = i + name - 1 -- Absolute index.
listname = "numeric"
else
listname = dump(name)
end
process_error(
"Item %s in the list of " .. listname .. " parameters cannot be empty, because the list must be contiguous.",
i)
end
end
-- Some code depends on only numeric params being present
-- when no holes are allowed (e.g. by checking for the
-- presence of arguments using next()), so remove
-- `maxindex`.
val.maxindex = nil
elseif not params[name].allow_holes then
args_new[name] = remove_holes(val)
end
end
end
end
end
end


-- Determine whether this is a template page. For these pages, normally required params aren't required, and the
-- If the current page is the template which invoked this Lua instance, then ignore the `require` flag, as it
-- `template_default` key supplies the default value only for these pages. Template documentation pages don't count
-- means we're viewing the template directly. Required parameters sometimes have a `template_default` key set,
-- because we want template invocations on those pages to behave like mainspace template invocations.
-- which gets used in such cases as a demo.
local title_obj = mw.title.getCurrentTitle()
-- Note: this won't work on other pages in the Template: namespace (including the /documentation subpage),
local is_template_page = title_obj.namespace == 10 and not title_obj.text:find("/documentation$")
-- or if the #invoke: is on a page in another namespace.
local pagename_set = args_new.pagename


-- Handle defaults.
-- Handle defaults.
for name, param in pairs(params) do
for name, param in pairs(params) do
local default_val
if param_types and param_types[param] == "table" then
-- If both `template_default` and `default` are given, `template_default` takes precedence, but only on
-- template pages. This lets you specify a different default as the template page example.
if is_template_page then
default_val = param.template_default
end
if default_val == nil then
default_val = param.default
end
if default_val ~= nil then
local arg_new = args_new[name]
local arg_new = args_new[name]
if type(arg_new) == "table" and arg_new._list then
if arg_new == nil then
if arg_new[1] == nil then
args_new[name] = convert_default_val(name, param, pagename_set, any_args_set)
arg_new[1] = convert_val(default_val, name, param)
elseif param.list and arg_new[1] == nil then
local default_val = convert_default_val(name, param, pagename_set, any_args_set)
if default_val ~= nil then
arg_new[1] = default_val
if arg_new.maxindex == 0 then
arg_new.maxindex = 1
end
end
end
if arg_new.maxindex == 0 then
arg_new.maxindex = 1
end
arg_new._list = nil
elseif arg_new == nil then
args_new[name] = convert_val(default_val, name, param)
end
end
end
end
Line 945: Line 1,353:
-- The required table should now be empty.
-- The required table should now be empty.
-- If any entry remains, trigger an error, unless we're on a template page.
-- If any parameters remain, throw an error, unless we're on the current template or module's page.
if not is_template_page then
if required and next(required) ~= nil and not is_own_page() then
local list = {}
params_list_error(required, "required")
for name in pairs(required) do
-- Return the arguments table.
insert(list, dump(name))
-- If there are any unknown parameters, throw an error, unless return_unknown is set, in which case return args_unknown as a second return value.
end
elseif return_unknown then
local n = #list
return args_new, args_unknown or {}
if n > 0 then
elseif args_unknown and next(args_unknown) ~= nil then
process_error("Parameter" .. (
params_list_error(args_unknown, "not used by this template")
n == 1 and (" " .. list[1] .. " is") or
("s " .. concat_list(list, " and ", true) .. " are")
) .. " required.")
end
end
-- Remove the temporary _list flag.
for _, arg_new in pairs(args_new) do
if type(arg_new) == "table" then
arg_new._list = nil
end
end
if return_unknown then
return args_new, args_unknown
else
return args_new
end
end
return args_new
end
end


return export
return export

Revision as of 11:27, 8 January 2025



local export = {}

local families_module = "Module:families"
local function_module = "Module:fun"
local labels_module = "Module:labels"
local languages_module = "Module:languages"
local math_module = "Module:math"
local pages_module = "Module:pages"
local parse_utilities_module = "Module:parse utilities"
local references_module = "Module:references"
local scripts_module = "Module:scripts"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local wikimedia_languages_module = "Module:wikimedia languages"
local yesno_module = "Module:yesno"

local mw = mw
local mw_title = mw.title
local string = string
local table = table

local dump = mw.dumpObject
local find = string.find
local format = string.format
local gmatch = string.gmatch
local gsub = string.gsub
local insert = table.insert
local ipairs = ipairs
local list_to_text = mw.text.listToText
local make_title = mw_title.makeTitle
local match = string.match
local max = math.max
local new_title = mw_title.new
local next = next
local pairs = pairs
local pcall = pcall
local rawset = rawset
local require = require
local sort = table.sort
local sub = string.sub
local tonumber = tonumber
local traceback = debug.traceback
local type = type

local current_title_text, current_namespace -- Defined when needed.
local namespaces = mw.site.namespaces

local function decode_entities(...)
	decode_entities = require(string_utilities_module).decode_entities
	return decode_entities(...)
end

local function get_family_by_code(...)
	get_family_by_code = require(families_module).getByCode
	return get_family_by_code(...)
end

local function get_family_by_name(...)
	get_family_by_name = require(families_module).getByCanonicalName
	return get_family_by_name(...)
end

local function get_language_by_code(...)
	get_language_by_code = require(languages_module).getByCode
	return get_language_by_code(...)
end

local function get_language_by_name(...)
	get_language_by_name = require(languages_module).getByCanonicalName
	return get_language_by_name(...)
end

local function get_script_by_code(...)
	get_script_by_code = require(scripts_module).getByCode
	return get_script_by_code(...)
end

local function get_script_by_name(...)
	get_script_by_name = require(scripts_module).getByCanonicalName
	return get_script_by_name(...)
end

local function get_wm_lang_by_code(...)
	get_wm_lang_by_code = require(wikimedia_languages_module).getByCode
	return get_wm_lang_by_code(...)
end

local function get_wm_lang_by_code_with_fallback(...)
	get_wm_lang_by_code_with_fallback = require(wikimedia_languages_module).getByCodeWithFallback
	return get_wm_lang_by_code_with_fallback(...)
end

local function gsplit(...)
	gsplit = require(string_utilities_module).gsplit
	return gsplit(...)
end

local function is_callable(...)
	is_callable = require(function_module).is_callable
	return is_callable(...)
end

local function is_finite_real_number(...)
	is_finite_real_number = require(math_module).is_finite_real_number
	return is_finite_real_number(...)
end

local function is_integer(...)
	is_integer = require(math_module).is_integer
	return is_integer(...)
end

local function is_internal_title(...)
	is_internal_title = require(pages_module).is_internal_title
	return is_internal_title(...)
end

local function is_positive_integer(...)
	is_positive_integer = require(math_module).is_positive_integer
	return is_positive_integer(...)
end

local function iterate_list(...)
	iterate_list = require(table_module).iterateList
	return iterate_list(...)
end

local function list_to_set(...)
	list_to_set = require(table_module).listToSet
	return list_to_set(...)
end

local function num_keys(...)
	num_keys = require(table_module).numKeys
	return num_keys(...)
end

local function parse_references(...)
	parse_references = require(references_module).parse_references
	return parse_references(...)
end

local function pattern_escape(...)
	pattern_escape = require(string_utilities_module).pattern_escape
	return pattern_escape(...)
end

local function scribunto_param_key(...)
	scribunto_param_key = require(string_utilities_module).scribunto_param_key
	return scribunto_param_key(...)
end

local function sorted_pairs(...)
	sorted_pairs = require(table_module).sortedPairs
	return sorted_pairs(...)
end

local function split(...)
	split = require(string_utilities_module).split
	return split(...)
end

local function split_labels_on_comma(...)
	split_labels_on_comma = require(labels_module).split_labels_on_comma
	return split_labels_on_comma(...)
end

local function split_on_comma(...)
	split_on_comma = require(parse_utilities_module).split_on_comma
	return split_on_comma(...)
end

local function trim(...)
	trim = require(string_utilities_module).trim
	return trim(...)
end

local function yesno(...)
	yesno = require(yesno_module)
	return yesno(...)
end

--[==[ intro:
This module is used to standardize template argument processing and checking. A typical workflow is as follows (based
on [[Module:translations]]):

{
	...
	local parent_args = frame:getParent().args

	local params = {
		[1] = {required = true, type = "language", default = "und"},
		[2] = true,
		[3] = {list = true},
		["alt"] = true,
		["id"] = true,
		["sc"] = {type = "script"},
		["tr"] = true,
		["ts"] = true,
		["lit"] = true,
	}

	local args = require("Module:parameters").process(parent_args, params)

	-- Do further processing of the parsed arguments in `args`.
	...
}

The `params` table should have the parameter names as the keys, and a (possibly empty) table of parameter tags as the
value. An empty table as the value merely states that the parameter exists, but should not receive any special
treatment; if desired, empty tables can be replaced with the value `true` as a perforamnce optimization.

Possible parameter tags are listed below:

; {required = true}
: The parameter is required; an error is shown if it is not present. The template's page itself is an exception; no
  error is shown there.
; {default =}
: Specifies a default input value for the parameter, if it is absent or empty. This will be processed as though it were
  the input instead, so (for example) {default = "und"} with the type {"language"} will return a language object for
  [[:Category:Undetermined language|Undetermined language]] if no language code is provided. When used on list
  parameters, this specifies a default value for the first item in the list only. Note that it is not possible to
  generate a default that depends on the value of other parameters. If used together with {required = true}, the default
  applies only to template pages (see the following entry), as a side effect of the fact that "required" parameters
  aren't actually required on template pages. This can be used to show an example of the template in action when the
  template page is visited; however, it is preferred to use `template_default` for this purpose, for clarity.
; {template_default =}
: Specifies a default input value for absent or empty parameters only on template pages. Template pages are any page in
  the template space (beginning with `Template:`) except for documentation pages (those ending in `.../documentation`).
  This can be used to provide an example value for a non-required parameter when the template page is visited, without
  interfering with other uses of the template. Both `template_default` and `default` can be specified for the same
  parameter. If this is done, `template_default` applies on template pages, and `default` on other pages. As an example,
  {{tl|cs-IPA}} uses the equivalent of {[1] = {default = "+", template_default = "příklad"}} to supply a default of
  {"+"} for mainspace and documentation pages (which tells the module to use the value of the {{para|pagename}}
  parameter, falling back to the actual pagename), but {"příklad"} (which means "example"), on [[Template:cs-IPA]].
; {alias_of =}
: Treat the parameter as an alias of another. When arguments are specified for this parameter, they will automatically
  be renamed and stored under the alias name. This allows for parameters with multiple alternative names, while still
  treating them as if they had only one name. The conversion-related properties of an aliased parameter (e.g. `type`,
  `set`, `convert`, `sublist`) are taken from the aliasee, and the corrresponding properties set on the alias itself
  are ignored; but other properties on the alias are taken from the alias's spec and not from the aliasee's spec. This
  means, for example, that if you create an alias of a list parameter, the alias must also specify the `list` property
  or it is not a list. (In such a case, a value specified for the alias goes into the first item of the aliasee's list.
  You cannot make a list alias of a non-list parameter; this causes an error to be thrown.) Similarly, if you specify
  `separate_no_index` on an aliasee but not on the alias, uses of the unindexed aliasee parameter are stored into the
  `.default` key, but uses of the unindexed alias are stored into the first numbered key of the aliasee's list.
  Aliases cannot be required, as this prevents the other name or names of the parameter from being used. Parameters
  that are aliases and required at the same time cause an error to be thrown.
; {allow_empty = true}
: If the argument is an empty string value, it is not converted to {nil}, but kept as-is. The use of `allow_empty` is
  disallowed if a type has been specified, and causes an error to be thrown.
; {no_trim = true}
: Spacing characters such as spaces and newlines at the beginning and end of a positional parameter are not removed.
  (MediaWiki itself automatically trims spaces and newlines at the edge of named parameters.) The use of `no_trim` is
  disallowed if a type has been specified, and causes an error to be thrown.
; {type =}
: Specifies what value type to convert the argument into. The default is to leave it as a text string. Alternatives are:
:; {type = "boolean"}
:: The value is treated as a boolean value, either true or false. No value, the empty string, and the strings {"0"},
   {"no"}, {"n"}, {"false"}, {"f"} and {"off"} are treated as {false}, all other values are considered {true}.
:; {type = "number"}
:: The value is converted into a number, and throws an error if the value is not parsable as a number. Input values may
   be signed (`+` or `-`), and may contain decimal points and leading zeroes. If {allow_hex = true}, then hexadecimal
   values in the form {"0x100"} may optionally be used instead, which otherwise have the same syntax restrictions
   (including signs, decimal digits, and leading zeroes after {"0x"}). Hexadecimal inputs are not case-sensitive. Lua's
   special number values (`inf` and `nan`) are not possible inputs.
:; {type = "language"}
:: The value is interpreted as a full or [[Wiktionary:Languages#Etymology-only languages|etymology-only language]] code
   language code (or name, if {method = "name"}) and converted into the corresponding object (see [[Module:languages]]).
   If the code or name is invalid, then an error is thrown. The additional setting {family = true} can be given to allow
   [[Wiktionary:Language families|language family codes]] to be considered valid and the corresponding object returned.
   Note that to distinguish an etymology-only language object from a full language object, use
   {object:hasType("language", "etymology-only")}.
:; {type = "full language"}
:: The value is interpreted as a full language code (or name, if {method = "name"}) and converted into the corresponding
   object (see [[Module:languages]]). If the code or name is invalid, then an error is thrown. Etymology-only languages
   are not allowed. The additional setting {family = true} can be given to allow
   [[Wiktionary:Language families|language family codes]] to be considered valid and the corresponding object returned.
:; {type = "Wikimedia language"}
:: The value is interpreted as a code and converted into a Wikimedia language object. If the code is invalid, then an
   error is thrown. If {fallback = true} is specified, conventional language codes which are different from their
   Wikimedia equivalent will also be accepted as a fallback.
:; {type = "family"}
:: The value is interpreted as a language family code (or name, if {method = "name"}) and converted into the
   corresponding object (see [[Module:families]]). If the code or name is invalid, then an error is thrown.
:; {type = "script"}
:: The value is interpreted as a script code (or name, if {method = "name"}) and converted into the corresponding object
   (see [[Module:scripts]]). If the code or name is invalid, then an error is thrown.
:; {type = "title"}
:: The value is interpreted as a page title and converted into the corresponding object (see the [[mw:Extension:Scribunto/Lua_reference_manual#Title_library|Title library]]). If the page title is invalid, then an error is thrown; by default, external titles (i.e. those on other wikis) are not treated as valid. Options are:
::; {namespace = n}
::: The default namespace, where {n} is a namespace number; this is treated as {0} (the mainspace) if not specified.
::; {allow_external = true}
::: External titles are treated as valid.
::; {prefix = "namespace override"} (default)
::: The default namespace prefix will be prefixed to the value is already prefixed by a namespace prefix. For instance, the input {"Foo"} with namespace {10} returns {"Template:Foo"}, {"Wiktionary:Foo"} returns {"Wiktionary:Foo"}, and {"Template:Foo"} returns {"Template:Foo"}. Interwiki prefixes cannot act as overrides, however: the input {"fr:Foo"} returns {"Template:fr:Foo"}.
::; {prefix = "force"}
::: The default namespace prefix will be prefixed unconditionally, even if the value already appears to be prefixed. This is the way that {{tl|#invoke:}} works when calling modules from the module namespace ({828}): the input {"Foo"} returns {"Module:Foo"}, {"Wiktionary:Foo"} returns {"Module:Wiktionary:Foo"}, and {"Module:Foo"} returns {"Module:Module:Foo"}.
::; {prefix = "full override"}
::: The same as {prefix = "namespace override"}, except that interwiki prefixes can also act as overrides. For instance, {"el:All topics"} with namespace {14} returns {"el:Category:All topics"}. Due to the limitations of MediaWiki, only the first prefix in the value may act as an override, so the namespace cannot be overridden if the first prefix is an interwiki prefix: e.g. {"el:Template:All topics"} with namespace {14} returns {"el:Category:Template:All topics"}.
:; {type = "qualifier"}
:: The value is interpreted as a qualifier and converted into the correct format for passing into `format_qualifiers()`
   in [[Module:qualifier]] (which currently just means converting it to a one-item list).
:; {type = "labels"}
:: The value is interpreted as a comma-separated list of labels and converted into the correct format for passing into
   `show_labels()` in [[Module:labels]] (which is currently a list of strings). Splitting is done on commas not followed
   by whitespace, except that commas inside of double angle brackets do not count even if not followed by whitespace.
   This type should be used by for normal labels (typically specified using {{para|l}} or {{para|ll}}) and accent
   qualifiers (typically specified using {{para|a}} and {{para|aa}}).
:; {type = "references"}
:: The value is interpreted as one or more references, in the format prescribed by `parse_references()` in
   [[Module:references]], and converted into a list of objects of the form accepted by `format_references()` in the same
   module. If a syntax error is found in the reference format, an error is thrown.
:; {type = function(val) ... end}
:: `type` may be set to a function (or callable table), which must take the argument value as its sole argument, and must
   output one of the other recognized types. This is particularly useful for lists (see below), where certain values need
   to be interpreted differently to others.
; {list =}
: Treat the parameter as a list of values, each having its own parameter name, rather than a single value. The
  parameters will have a number at the end, except optionally for the first (but see also {require_index = true}). For
  example, {list = true} on a parameter named "head" will include the parameters {{para|head}} (or {{para|head1}}),
  {{para|head2}}, {{para|head3}} and so on. If the parameter name is a number, another number doesn't get appended, but
  the counting simply continues, e.g. for parameter {3} the sequence is {{para|3}}, {{para|4}}, {{para|5}} etc. List
  parameters are returned as numbered lists, so for a template that is given the parameters `|head=a|head2=b|head3=c`,
  the processed value of the parameter {"head"} will be { { "a", "b", "c" }}}.
: The value for {list =} can also be a string. This tells the module that parameters other than the first should have a
  different name, which is useful when the first parameter in a list is a number, but the remainder is named. An example
  would be for genders: {list = "g"} on a parameter named {1} would have parameters {{para|1}}, {{para|g2}}, {{para|g3}}
  etc.
: If the number is not located at the end, it can be specified by putting {"\1"} at the number position. For example,
  parameters {{para|f1accel}}, {{para|f2accel}}, ... can be captured by using the parameter name {"f\1accel"}, as is
  done in [[Module:headword/templates]].
; {set =}
: Require that the value of the parameter be one of the specified list of values (or omitted, if {required = true} isn't
  given). The values in the specified list should be strings corresponding to the raw parameter values except when
  {type = "number"}, in which case they should be numbers. The use of `set` is disallowed if {type = "boolean"} and
  causes an error to be thrown.
; {sublist =}
: The value of the parameter is a delimiter-separated list of individual raw values. The resulting field in `args` will
  be a Lua list (i.e. a table with numeric indices) of the converted values. If {sublist = true} is given, the values
  will be split on commas (possibly with whitespace on one or both sides of the comma, which is ignored). If
  {sublist = "comma without whitespace"} is given, the values will be split on commas which are not followed by whitespace,
  and which aren't preceded by an escaping backslash. Otherwise, the value of `sublist` should be either a Lua pattern
  specifying the delimiter(s) to split on or a function (or callable table) to do the splitting, which is passed two values
  (the value to split and a function to signal an error) and should return a list of the split values.
; {convert =}
: If given, this specifies a function (or callable table) to convert the raw parameter value into the Lua object used
  during further processing. The function is passed two arguments, the raw parameter value itself and a function used to
  signal an error during parsing or conversion, and should return one value, the converted parameter. The error-signaling
  function contains the name and raw value of the parameter embedded into the message it generates, so these do not need to
  specified in the message passed into it. If `type` is specified in conjunction with `convert`, the processing by
  `type` happens first. If `sublist` is given in conjunction with `convert`, the raw parameter value will be split
  appropriately and `convert` called on each resulting item.
; {allow_hex = true}
: When used in conjunction with {type = "number"}, allows hexadecimal numbers as inputs, in the format {"0x100"} (which is
  not case-sensitive).
; {family = true}
: When used in conjunction with {type = "language"}, allows [[Wiktionary:Language families|language family codes]] to be
  returned. To check if a given object refers to a language family, use {object:hasType("family")}.
; {method = "name"}
: When used in conjunction with {type = "language"}, {type = "family"} or {type = "script"}, checks for and parses a
  language, family or script name instead of a code.
; {allow_holes = true}
: This is used in conjunction with list-type parameters. By default, the values are tightly packed in the resulting
  list. This means that if, for example, an entry specified `head=a|head3=c` but not {{para|head2}}, the returned list
  will be { {"a", "c"}}}, with the values stored at the indices {1} and {2}, not {1} and {3}. If it is desirable to keep
  the numbering intact, for example if the numbers of several list parameters correlate with each other (like those of
  {{tl|affix}}), then this tag should be specified.
: If {allow_holes = true} is given, there may be {nil} values in between two real values, which makes many of Lua's
  table processing functions no longer work, like {#} or {ipairs()}. To remedy this, the resulting table will contain an
  additional named value, `maxindex`, which tells you the highest numeric index that is present in the table. In the
  example above, the resulting table will now be { { "a", nil, "c", maxindex = 3}}}. That way, you can iterate over the
  values from {1} to `maxindex`, while skipping {nil} values in between.
; {disallow_holes = true}
: This is used in conjunction with list-type parameters. As mentioned above, normally if there is a hole in the source
  arguments, e.g. `head=a|head3=c` but not {{para|head2}}, it will be removed in the returned list. If
  {disallow_holes = true} is specified, however, an error is thrown in such a case. This should be used whenever there
  are multiple list-type parameters that need to line up (e.g. both {{para|head}} and {{para|tr}} are available and
  {{para|head3}} lines up with {{para|tr3}}), unless {allow_holes = true} is given and you are prepared to handle the
  holes in the returned lists.
; {require_index = true}
: This is used in conjunction with list-type parameters. By default, the first parameter can have its index omitted.
  For example, a list parameter named `head` can have its first parameter specified as either {{para|head}} or
  {{para|head1}}. If {require_index = true} is specified, however, only {{para|head1}} is recognized, and {{para|head}}
  will be treated as an unknown parameter. {{tl|affixusex}} (and variants {{tl|suffixusex}}, {{tl|prefixusex}}) use
  this, for example, on all list parameters.
; {separate_no_index = true}
: This is used to distinguish between {{para|head}} and {{para|head1}} as different parameters. For example, in
  {{tl|affixusex}}, to distinguish between {{para|sc}} (a script code for all elements in the usex's language) and
  {{para|sc1}} (the script code of the first element, used when the first element is prefixed with a language code to
  indicate that it is in a different language). When this is used, the resulting table will contain an additional named
  value, `default`, which contains the value for the indexless argument.
; {demo = true}
: This is used as a way to ensure that the parameter is only enabled on the template's own page (and its documentation page), and in the User: namespace; otherwise, it will be treated as an unknown parameter. This should only be used if special settings are required to showcase a template in its documentation (e.g. adjusting the pagename or disabling categorization). In most cases, it should be possible to do this without using demo parameters, but they may be required if a template/documentation page also contains real uses of the same template as well (e.g. {{tl|shortcut}}), as a way to distinguish them.
]==]

-- Returns true if the current page is a template or module containing the current {{#invoke}}.
-- If the include_documentation argument is given, also returns true if the current page is either page's docuemntation page.
local own_page, own_page_or_documentation
local function is_own_page(include_documentation)
	if own_page == nil then
		if current_namespace == nil then
			local current_title = mw_title.getCurrentTitle()
			current_title_text, current_namespace = current_title.prefixedText, current_title.namespace
		end
		local frame = current_namespace == 828 and mw.getCurrentFrame() or
			current_namespace == 10 and mw.getCurrentFrame():getParent()
		if frame then
			local frame_title_text = frame:getTitle()
			own_page = current_title_text == frame_title_text
			own_page_or_documentation = own_page or current_title_text == frame_title_text .. "/documentation"
		else
			own_page, own_page_or_documentation = false, false
		end
	end
	return include_documentation and own_page_or_documentation or own_page
end

-------------------------------------- Some helper functions -----------------------------

-- Convert a list in `list` to a string, separating the final element from the preceding one(s) by `conjunction`. If
-- `dump_vals` is given, pass all values in `list` through mw.dumpObject() (WARNING: this destructively modifies
-- `list`). This is similar to serialCommaJoin() in [[Module:table]] when used with the `dontTag = true` option, but
-- internally uses mw.text.listToText().
local function concat_list(list, conjunction, dump_vals)
	if dump_vals then
		for k, v in pairs(list) do
			list[k] = dump(v)
		end
	end
	return list_to_text(list, nil, conjunction)
end

-- Helper function for use with convert_val_error(). Format a list of possible choices using `concat_list` and
-- conjunction "or", displaying "either " before the choices if there's more than one.
local function format_choice_list(param_type)
	return (#param_type > 1 and "either " or "") .. concat_list(param_type, " or ")
end

-- Split an argument on comma, but not comma followed by whitespace.
local function split_on_comma_without_whitespace(val)
	if find(val, "\\", 1, true) or match(val, ",%s") then
		return split_on_comma(val)
	end
	return split(val, ",")
end

-- Convert a value that is not a string or number to a string using mw.dumpObject(), for debugging purposes.
local function dump_if_unusual(val)
	local val_type = type(val)
	return (val_type == "string" or val_type == "number") and val or dump(val)
end

-- A helper function for use with generating error-signaling functions in the presence of raw value conversion. Format a
-- message `msg`, including the processed value `processed` if it is different from the raw value `rawval`; otherwise,
-- just return `msg`.
local function msg_with_processed(msg, rawval, processed)
	if rawval == processed then
		return msg
	end
	return format("%s (processed value %s)", msg, dump_if_unusual(processed))
end

-------------------------------------- Error handling -----------------------------

local function process_error(fmt, ...)
	local args = {...}
	for i, val in ipairs(args) do
		args[i] = dump(val)
	end
	if type(fmt) == "table" then
		-- hacky signal that we're called from internal_process_error(), and not to omit stack frames
		return error(format(fmt[1], unpack(args)))
	end
	return error(format(fmt, unpack(args)), 3)
end

local function internal_process_error(fmt, ...)
	process_error({"Internal error in `params` table: " .. fmt}, ...)
end

-- Check that a parameter or argument is in the form form Scribunto normalizes input argument keys into (e.g. 1 not "1", "foo" not " foo "). Otherwise, it won't be possible to normalize inputs in the expected way. Unless is_argument is set, also check that the name only contains one placeholder at most, and that strings don't resolve to numeric keys once the placeholder has been substituted.
local function validate_name(name, desc, extra_name, is_argument)
	local normalized = scribunto_param_key(name)
	if name and name == normalized then
		if is_argument or type(name) ~= "string" then
			return
		end
		local placeholder = find(name, "\1", 1, true)
		if not placeholder then
			return
		elseif find(name, "\1", placeholder + 1, true) then
			error(format(
				'Expected %s to only contain one placeholder, but saw %s',
				extra_name and (desc .. dump(extra_name)) or desc, dump(name)
			))
		end
		local first_name = gsub(name, "\1", "1")
		normalized = scribunto_param_key(first_name)
		if first_name == normalized then
			return
		end
		error(format(
			'%s cannot resolve to numeric parameters once any placeholder has been substituted, but %s resolves to %s',
			extra_name and (desc .. dump(extra_name)) or desc, dump(name), dump(normalized)
		))
	elseif normalized == nil then
		error(format(
			'Expected %s to be of type string or number, but saw %s',
			extra_name and (desc .. dump(extra_name)) or desc, type(name)
		))
	end
	error(format(
		"Expected %s to be Scribunto-compatible: %s (a %s) should be %s (a %s)",
		extra_name and (desc .. dump(extra_name)) or desc, dump(name), type(name), dump(normalized), type(normalized)
	))
end

-- TODO: give ranges instead of long lists, if possible.
local function params_list_error(params, msg)
	local list, n = {}, 0
	for name in sorted_pairs(params) do
		n = n + 1
		list[n] = name
	end
	error(format(
		"Parameter%s %s.",
		format(n == 1 and " %s is" or "s %s are", concat_list(list, " and ", true)),
		msg
	), 3)
end

-- Signal an error for a value `val` that is not of the right type `param_type` (which is either a string specifying a type or
-- a list of possible values, in the case where `set` was used). `name` is the name of the parameter and can be a
-- function to signal an error (which is assumed to automatically display the parameter's name and value). `seetext` is
-- an optional additional explanatory link to display (e.g. [[WT:LOL]], the list of possible languages and codes).
local function convert_val_error(val, name, param_type, seetext)
	if is_callable(name) then
		if type(param_type) == "table" then
			param_type = "choice, must be " .. format_choice_list(param_type)
		end
		name(format("Invalid %s; the value %s is not valid%s", param_type, val, seetext and "; see " .. seetext or ""))
	else
		if type(param_type) == "table" then
			param_type = "must be " .. format_choice_list(param_type)
		else
			param_type = "should be a valid " .. param_type
		end
		error(format("Parameter %s %s; the value %s is not valid.%s", dump(name), param_type, dump(val),
			seetext and " See " .. seetext .. "." or ""))
	end
end

-- Generate the appropriate error-signaling function given parameter value `val` and name `name`. If `name` is already
-- a function, it is just returned; otherwise a function is generated and returned that displays the passed-in messaeg
-- along with the parameter's name and value.
local function make_parse_err(val, name)
	if is_callable(name) then
		return name
	end
	return function(msg)
		error(format("%s: parameter %s=%s", msg, name, val))
	end
end

-------------------------------------- Value conversion -----------------------------

-- For a list parameter `name` and corresponding value `list_name` of the `list` field (which should have the same value
-- as `name` if `list = true` was given), generate a pattern to match parameters of the list and store the pattern as a
-- key in `patterns`, with corresponding value set to `name`. For example, if `list_name` is "tr", the pattern will
-- match "tr" as well as "tr1", "tr2", ..., "tr10", "tr11", etc. If the `list_name` contains a \1 in it, the numeric
-- portion goes in place of the \1. For example, if `list_name` is "f\1accel", the pattern will match "faccel",
-- "f1accel", "f2accel", etc. Any \1 in `name` is removed before storing into `patterns`.
local function save_pattern(name, list_name, patterns)
	name = type(name) == "string" and gsub(name, "\1", "") or name
	if find(list_name, "\1", 1, true) then
		patterns["^" .. gsub(pattern_escape(list_name), "\1", "([1-9]%%d*)") .. "$"] = name
	else
		patterns["^" .. pattern_escape(list_name) .. "([1-9]%d*)$"] = name
		list_name = list_name .. "\1"
	end
	validate_name(list_name, "the list field of parameter ", name)
	return patterns
end

-- A helper function for use with `sublist`. It is an iterator function for use in a for-loop that returns split
-- elements of `val` using `sublist` (a Lua split pattern; boolean `true` to split on commas optionally surrounded by
-- whitespace; "comma without whitespace" to split only on commas not followed by whitespace which have not been escaped
-- by a backslash; or a function to do the splitting, which is passed two values, the value to split and a function to
-- signal an error, and should return a list of the split elements). `name` is the parameter name or error-signaling
-- function passed into convert_val().
local function split_sublist(val, name, sublist)
	if sublist == true then
		return gsplit(val, "%s*,%s*")
	elseif sublist == "comma without whitespace" then
		sublist = split_on_comma_without_whitespace
	elseif type(sublist) == "string" then
		return gsplit(val, sublist)
	elseif not is_callable(sublist) then
		error(format('Internal error: Expected `sublist` to be of type "string" or "function" or boolean `true`, but saw %s', dump(sublist)))
	end
	return iterate_list(sublist(val, make_parse_err(val, name)))
end

-- For parameter named `name` with value `val` and param spec `param`, if the `set` field is specified, verify that the
-- value is one of the one specified in `set`, and throw an error otherwise. `name` is taken directly from the
-- corresponding parameter passed into convert_val() and may be a function to signal an error. Optional `param_type` is a
-- string specifying the conversion type of `val` and is used for special-casing: If `param_type` is "boolean", an internal
-- error is thrown (since `set` cannot be used in conjunction with booleans) and if `param_type` is "number", no checking
-- happens because in this case `set` contains numbers and is checked inside the number conversion function itself,
-- after converting `val` to a number.
local function check_set(val, name, param, param_type)
	if param_type == "boolean" then
		error(format('Internal error: Cannot use `set` with `type = "%s"`', param_type))
	elseif param_type == "number" then
		-- Needs to be special cased because the check happens after conversion to numbers.
		return
	end
	if not param.set[val] then
		local list = {}
		for k in pairs(param.set) do
			insert(list, dump(k))
		end
		sort(list)
		-- If the parameter is not required then put "or empty" at the end of the list, to avoid implying the parameter is actually required.
		if not param.required then
			insert(list, "empty")
		end
		convert_val_error(val, name, list)
	end
end

local function convert_language(val, name, param, allow_etym)
	local method, func = param.method
	if method == nil or method == "code" then
		func, method = get_language_by_code, "code"
	elseif method == "name" then
		func, method = get_language_by_name, "name"
	else
		error(format('Internal error: Expected `method` for type `language` to be "code", "name" or undefined, but saw %s', dump(method)))
	end
	local lang = func(val, nil, allow_etym, param.family)
	if lang then
		return lang
	end
	local list, links = {"language"}, {"[[WT:LOL]]"}
	if allow_etym then
		insert(list, "etymology language")
		insert(links, "[[WT:LOL/E]]")
	end
	if param.family then
		insert(list, "family")
		insert(links, "[[WT:LOF]]")
	end
	convert_val_error(val, name, concat_list(list, " or ") .. " " .. (method == "name" and "name" or "code"), concat_list(links, " and "))
end

-- TODO: validate parameter specs separately, as it's making the handler code really messy at the moment.
local type_handlers = setmetatable({
	["boolean"] = function(val)
		return yesno(val, true)
	end,

	["family"] = function(val, name, param)
		local method, func = param.method
		if method == nil or method == "code" then
			func, method = get_family_by_code, "code"
		elseif method == "name" then
			func, method = get_family_by_name, "name"
		else
			error(format('Internal error: Expected `method` for type `family` to be "code", "name" or undefined, but saw %s', dump(method)))
		end
		return func(val) or convert_val_error(val, name, "family " .. method, "[[WT:LOF]]")
	end,

	["labels"] = function(val, name, param)
		-- FIXME: Should be able to pass in a parse_err function.
		return split_labels_on_comma(val)
	end,

	["language"] = function(val, name, param)
		return convert_language(val, name, param, true)
	end,

	["full language"] = function(val, name, param)
		return convert_language(val, name, param)
	end,

	["number"] = function(val, name, param)
		local allow_hex = param.allow_hex
		if allow_hex and allow_hex ~= true then
			error(format('Internal error: Expected `allow_hex` for type `number` to be of type "boolean" or undefined, but saw %s', dump(allow_hex)))
		end
		local num = tonumber(val)
		-- Avoid converting inputs like "nan" or "inf", and disallow 0x hex inputs unless explicitly enabled
		-- with `allow_hex`.
		if not (num and is_finite_real_number(num) and (allow_hex or not match(val, "^[+-]?0[Xx]%x*%.?%x*$"))) then
			convert_val_error(val, name, (allow_hex and "decimal or hexadecimal " or "") .. "number")
		end
		if param.set then
			-- Don't pass in "number" here; otherwise no checking will happen.
			check_set(num, name, param)
		end
		return num
	end,
	
	["qualifier"] = function(val, name, param)
		return {val}
	end,
	
	["references"] = function(val, name, param)
		return parse_references(val, make_parse_err(val, name))
	end,

	["script"] = function(val, name, param)
		local method, func = param.method
		if method == nil or method == "code" then
			func, method = get_script_by_code, "code"
		elseif method == "name" then
			func, method = get_script_by_name, "name"
		else
			error(format('Internal error: Expected `method` for type `script` to be "code", "name" or undefined, but saw %s', dump(method)))
		end
		return func(val) or convert_val_error(val, name, "script " .. method, "[[WT:LOS]]")
	end,

	["string"] = function(val, name, param) -- To be removed as unnecessary.
		return val
	end,

	-- TODO: add support for resolving to unsupported titles.
	-- TODO: split this into "page name" (i.e. internal) and "link target" (i.e. external as well), which is more intuitive.
	["title"] = function(val, name, param)
		local namespace = param.namespace
		if namespace == nil then
			namespace = 0
		else
			local valid_type = type(namespace) ~= "number" and 'of type "number" or undefined' or
				not namespaces[namespace] and "a valid namespace number" or
				nil
			if valid_type then
				error(format('Internal error: Expected `namespace` for type `title` to be %s, but saw %s', valid_type, dump(namespace)))
			end
		end
		-- Decode entities. WARNING: mw.title.makeTitle must be called with `decoded` (as it doesn't decode) and mw.title.new must be called with `val` (as it does decode, so double-decoding needs to be avoided).
		local decoded, prefix, title = decode_entities(val), param.prefix
		-- If the input is a fragment, treat the title as the current title with the input fragment.
		if sub(decoded, 1, 1) == "#" then
			-- If prefix is "force", only get the current title if it's in the specified namespace. current_title includes the namespace prefix.
			if current_namespace == nil then
				local current_title = mw_title.getCurrentTitle()
				current_title_text, current_namespace = current_title.prefixedText, current_title.namespace
			end
			if not (prefix == "force" and namespace ~= current_namespace) then
				title = new_title(current_title_text .. val)
			end
		elseif prefix == "force" then
			-- Unconditionally add the namespace prefix (mw.title.makeTitle).
			title = make_title(namespace, decoded)
		elseif prefix == "full override" then
			-- The first input prefix will be used as an override (mw.title.new). This can be a namespace or interwiki prefix.
			title = new_title(val, namespace)
		elseif prefix == nil or prefix == "namespace override" then
			-- Only allow namespace prefixes to override. Interwiki prefixes therefore need to be treated as plaintext (e.g. "el:All topics" with namespace 14 returns "el:Category:All topics", but we want "Category:el:All topics" instead; if the former is really needed, then the input ":el:Category:All topics" will work, as the initial colon overrides the namespace). mw.title.new can take namespace names as well as numbers in the second argument, and will throw an error if the input isn't a valid namespace, so this can be used to determine if a prefix is for a namespace, since mw.title.new will return successfully only if there's either no prefix or the prefix is for a valid namespace (in which case we want the override).
			local success
			success, title = pcall(new_title, val, match(decoded, ".-%f[:]") or namespace)
			-- Otherwise, get the title with mw.title.makeTitle, which unconditionally adds the namespace prefix, but behaves like mw.title.new if the namespace is 0.
			if not success then
				title = make_title(namespace, decoded)
			end
		else
			error(format('Internal error: Expected `prefix` for type `title` to be "force", "full override", "namespace override" or undefined, but saw %s', dump(prefix)))
		end
		local allow_external = param.allow_external
		if allow_external == true then
			return title or convert_val_error(val, name, "Wiktionary or external page title")
		elseif not allow_external then
			return title and is_internal_title(title) and title or convert_val_error(val, name, "Wiktionary page title")
		end
		error(format('Internal error: Expected `allow_external` for type `title` to be of type "boolean" or undefined, but saw %s', dump(allow_external)))
	end,

	["Wikimedia language"] = function(val, name, param)
		local fallback = param.fallback
		if fallback == true then
			return get_wm_lang_by_code_with_fallback(val) or convert_val_error(val, name, "Wikimedia language or language code")
		elseif not fallback then
			return get_wm_lang_by_code(val) or convert_val_error(val, name, "Wikimedia language code")
		end
		error(format('Internal error: Expected `fallback` for type `Wikimedia language` to be of type "boolean" or undefined, but saw %s', dump(fallback)))
	end,
}, {
	-- TODO: decode HTML entities in all input values. Non-trivial to implement, because we need to avoid any downstream functions decoding the output from this module, which would be double-decoding. Note that "title" has this implemented already, and it needs to have both the raw input and the decoded input to avoid double-decoding by me.title.new, so any implementation can't be as simple as decoding in __call then passing the result to the handler.
	__call = function(self, val, name, param, param_type)
		local val_type = type(val)
		-- TODO: check this for all possible parameter types.
		if val_type == param_type then
			return val
		-- TODO: throw an internal error.
		end
		local func = self[param_type]
		if func == nil then
			error(format("Internal error: %s is not a recognized parameter type.", dump(param_type)))
		end
		return func(val, name, param)
	end
})

--[==[ func: export.convert_val(val, name, param)
Convert a parameter value according to the associated specs listed in the `params` table passed to
[[Module:parameters]]. `val` is the value to convert for a parameter whose name is `name` (used only in error messages).
`param` is the spec (the value part of the `params` table for the parameter). In place of passing in the parameter name,
`name` can be a function that throws an error, displaying the specified message along with the parameter name and value.
This function processes all the conversion-related fields in `param`, including `type`, `set`, `sublist`, `convert`,
etc. It returns the converted value.
]==]
local function convert_val(val, name, param)
	local param_type = param.type or "string"
	-- If param.type is a function, resolve it to a recognized type.
	if is_callable(param_type) then
		param_type = param_type(val)
	end
	local sublist = param.sublist
	if sublist then
		local retlist = {}
		if type(val) ~= "string" then
			error(format("Internal error: %s is not a string.", dump(val)))
		end
		if param.convert then
			local thisval, insval
			local thisindex = 0
			local parse_err
			if is_callable(name) then
				-- We assume the passed-in error function in `name` already shows the parameter name and raw value.
				parse_err = function(msg)
					name(format("%s: item #%s=%s", msg_with_processed(msg, thisval, insval), thisindex,
						thisval))
				end
			else
				parse_err = function(msg)
					error(format("%s: item #%s=%s of parameter %s=%s", msg_with_processed(msg, thisval, insval),
						thisindex, thisval, name, val))
				end
			end
			for v in split_sublist(val, name, sublist) do
				thisval = v
				thisindex = thisindex + 1
				if param.set then
					check_set(v, name, param, param_type)
				end
				insert(retlist, param.convert(type_handlers(v, name, param, param_type), parse_err))
			end
		else
			for v in split_sublist(val, name, sublist) do
				if param.set then
					check_set(v, name, param, param_type)
				end
				insert(retlist, type_handlers(v, name, param, param_type))
			end
		end
		return retlist
	else
		if param.set then
			check_set(val, name, param, param_type)
		end
		local retval = type_handlers(val, name, param, param_type)
		if param.convert then
			local parse_err
			if is_callable(name) then
				-- We assume the passed-in error function in `name` already shows the parameter name and raw value.
				if retval == val then
					-- This is an optimization to avoid creating a closure. The second arm works correctly even
					-- when retval == val.
					parse_err = name
				else
					parse_err = function(msg)
						name(msg_with_processed(msg, val, retval))
					end
				end
			else
				parse_err = function(msg)
					error(format("%s: parameter %s=%s", msg_with_processed(msg, val, retval), name, val))
				end
			end
			retval = param.convert(retval, parse_err)
		end
		return retval
	end
end
export.convert_val = convert_val -- used by [[Module:parameter utilities]]

local function unknown_param(name, val, args_unknown)
	args_unknown[name] = val
	return args_unknown
end

local function check_string_param(param_type, name, tag)
	if param_type and param_type ~= "string" then
		internal_process_error(
			"%s cannot be set unless the parameter has the type %s (the default): parameter %s has the type %s.",
			tag, "string", name, param_type
		)
	end
end

local function handle_holes(param, val, name)
	-- Iterate up the list, and throw an error if a hole is found.
	if param.disallow_holes then
		for i = 1, val.maxindex do
			if val[i] == nil then
				local listname = param.list
				if type(listname) == "string" then
					listname = dump(listname)
				elseif type(name) == "number" then
					i = i + name - 1 -- Absolute index.
					listname = "numeric"
				else
					listname = dump(name)
				end
				process_error(
					"Item %d in the list of %s parameters cannot be empty, because the list must be contiguous.",
					i, listname
				)
			end
		end
	-- If `allow_holes` is set, there's nothing to do. This is placed after
	-- `disallow_holes`, so that the latter takes priority.
	elseif param.allow_holes then
		return
	-- Otherwise, remove any holes. Use num_keys to get a list of numerical keys
	-- instead of iterating from 1 to `maxindex`, as it could be enormous if
	-- there is a huge hole in the list.
	else
		local keys, i = num_keys(val), 0
		while true do
			i = i + 1
			local key = keys[i]
			if key == nil then
				break
			elseif i ~= key then
				val[i], val[key] = val[key], nil
			end
		end
	end
	-- Some code depends on only numeric params being present when no holes are
	-- allowed (e.g. by checking for the presence of arguments using next()), so
	-- remove `maxindex`.
	val.maxindex = nil
end

-- If both `template_default` and `default` are given, `template_default` takes precedence, but only on the template or
-- module page. This means a different default can be specified for the template or module page example. However,
-- `template_default` doesn't apply if any args are set, which helps (somewhat) with examples on documentation pages
-- transcluded into the template page. HACK: We still run into problems on documentation pages transcluded into the
-- template page when pagename= is set. Check this on the assumption that pagename= is fairly standard.
local function convert_default_val(name, param, pagename_set, any_args_set)
	if not pagename_set then
		local val = param.template_default
		if val ~= nil and not any_args_set and is_own_page() then
			return convert_val(val, name, param)
		end
	end
	local val = param.default
	if val ~= nil then
		return convert_val(val, name, param)
	end
end

--[==[
Process arguments with a given list of parameters. Return a table containing the processed arguments. The `args`
parameter specifies the arguments to be processed; they are the arguments you might retrieve from
{frame:getParent().args} (the template arguments) or in some cases {frame.args} (the invocation arguments). The `params`
parameter specifies a list of valid parameters, and consists of a table. If an argument is encountered that is not in
the parameter table, an error is thrown.

The structure of the `params` table is as described above in the intro comment.

'''WARNING:''' The `params` table is destructively modified to save memory. Nonetheless, different keys can share the
same value objects in memory without causing problems.

The `return_unknown` parameter, if set to {true}, prevents the function from triggering an error when it comes across an
argument with a name that it doesn't recognise. Instead, the return value is a pair of values: the first is the
processed arguments as usual, while the second contains all the unrecognised arguments that were left unprocessed. This
allows you to do multi-stage processing, where the entire set of arguments that a template should accept is not known at
once. For example, an inflection-table might do some generic processing on some arguments, but then defer processing of
the remainder to the function that handles a specific inflectional type.
]==]
function export.process(args, params, return_unknown)
	-- Process parameters for specific properties
	local args_new, args_unknown, any_args_set, param_types, required, patterns, list_args, index_list,
		args_placeholders, n_ph = {}

	for name, param in pairs(params) do
		validate_name(name, "parameter names")
		local param_type = type(param)
		if param_types then
			param_types[param] = param_type
		else
			param_types = {[param] = param_type}
		end
		if param_type == "table" then
			-- Populate required table, and make sure aliases aren't set to required.
			if param.required then
				if param.alias_of then
					internal_process_error(
						"Parameter %s is an alias of %s, but is also set as a required parameter. Only %s should be set as required.",
						name, param.alias_of, name
					)
				elseif required then
					required[name] = true
				else
					required = {[name] = true}
				end
			end

			-- Convert param.set from a list into a set.
			-- `converted_set` prevents double-conversion if multiple parameter keys share the same param table.
			-- rawset avoids errors if param has been loaded via mw.loadData; however, it's probably more efficient to preconvert them, and set the `converted_set` key in advance.
			local set = param.set
			if set and not param.converted_set then
				rawset(param, "set", list_to_set(set))
				rawset(param, "converted_set", true)
			end

			local listname, alias = param.list, param.alias_of
			if alias then
				validate_name(alias, "the alias_of field of parameter ", name)
				-- Check that the alias_of is set to a valid parameter.
				if not params[alias] then
					internal_process_error(
						"Parameter %s is an alias of an invalid parameter.",
						name
					)
				elseif alias == name then
					internal_process_error(
						"Parameter %s cannot be an alias of itself.",
						name
					)
				end
				local main_param = params[alias]
				local main_type = param_types[main_param] or type(main_param) -- Might not yet be memoized.
				-- Aliases can't be lists unless the canonical parameter is also a list.
				if listname and not (main_type == "table" and main_param.list) then
					internal_process_error(
						"The list parameter %s is set as an alias of %s, which is not a list parameter.", name, alias
					)
				-- Aliases can't be aliases of other aliases.
				elseif main_type == "table" and main_param.alias_of then
					internal_process_error(
						"alias_of cannot be set to another alias: parameter %s is set as an alias of %s, which is in turn an alias of %s. Set alias_of for %s to %s.",
						name, alias, params[alias].alias_of, name, params[alias].alias_of
					)
				end
			end

			if listname then
				if not alias then
					local key = name
					if type(name) == "string" then
						key = gsub(name, "\1", "")
					end
					local list_arg = {maxindex = 0}
					args_new[key] = list_arg
					if list_args == nil then
						list_args = {[key] = list_arg}
					else
						list_args[key] = list_arg
					end
				end
				local list_type = type(listname)
				if list_type == "string" then
					-- If the list property is a string, then it represents the name
					-- to be used as the prefix for list items. This is for use with lists
					-- where the first item is a numbered parameter and the
					-- subsequent ones are named, such as 1, pl2, pl3.
					patterns = save_pattern(name, listname, patterns or {})
				elseif listname ~= true then
					internal_process_error(
						"The list field for parameter %s must be a boolean, string or undefined, but saw a %s.",
						name, list_type
					)
				elseif type(name) == "number" then
					if index_list ~= nil then
						internal_process_error(
							"Only one numeric parameter can be a list, unless the list property is a string."
						)
					end
					-- If the name is a number, then all indexed parameters from
					-- this number onwards go in the list.
					index_list = name
				else
					patterns = save_pattern(name, name, patterns or {})
				end
				if find(name, "\1", 1, true) then
					if args_placeholders then
						n_ph = n_ph + 1
						args_placeholders[n_ph] = name
					else
						args_placeholders, n_ph = {name}, 1
					end
				end
			end
		elseif param ~= true then
			internal_process_error(
				"Spec for parameter %s must be a table of specs or the value true, but found %s.",
				name, param_type ~= "boolean" and param_type or param
			)
		end
	end

	--Process required changes to `params`.
	if args_placeholders then
		for i = 1, n_ph do
			local name = args_placeholders[i]
			params[gsub(name, "\1", "")], params[name] = params[name], nil
		end
	end

	-- Process the arguments
	for name, val in pairs(args) do
		any_args_set = true
		validate_name(name, "argument names", nil, true)
		
		local orig_name, raw_type, index, canonical = name, type(name)

		if raw_type == "number" then
			if index_list and name >= index_list then
				index = name - index_list + 1
				name = index_list
			end
		elseif patterns then
			-- Does this argument name match a pattern?
			for pattern, pname in next, patterns do
				index = match(name, pattern)
				-- It matches, so store the parameter name and the
				-- numeric index extracted from the argument name.
				if index then
					index = tonumber(index)
					name = pname
					break
				end
			end
		end

		local param = params[name]

		-- If the argument is not in the list of parameters, store it in a separate list.
		if not param then
			args_unknown = unknown_param(name, val, args_unknown or {})
		elseif param == true then
			canonical = orig_name
			val = trim(val)
			if val ~= "" then
				-- If the parameter is duplicated, throw an error.
				if args_new[name] ~= nil then
					process_error(
						"Parameter %s has been entered more than once. This is probably because a parameter alias has been used.",
						canonical
					)
				end
				args_new[name] = val
			end
		else
			if param.require_index then
				-- Disallow require_index for numeric parameter names, as this doesn't make sense.
				if raw_type == "number" then
					internal_process_error(
						"Cannot set require_index for numeric parameter %s.",
						name
					)
				-- If a parameter without the trailing index was found, and
				-- require_index is set on the param, treat it
				-- as if it isn't recognized.
				elseif not index then
					args_unknown = unknown_param(name, val, args_unknown or {})
				end
			end

			-- Check that separate_no_index is not being used with a numeric parameter.
			if param.separate_no_index then
				if raw_type == "number" then
					internal_process_error(
						"Cannot set separate_no_index for numeric parameter %s.",
						name
					)
				elseif type(param.alias_of) == "number" then
					internal_process_error(
						"Cannot set separate_no_index for parameter %s, as it is an alias of numeric parameter %s.",
						name, param.alias_of
					)
				end
			end

			-- If no index was found, use 1 as the default index.
			-- This makes list parameters like g, g2, g3 put g at index 1.
			-- If `separate_no_index` is set, then use 0 as the default instead.
			if param.list then
				index = index or param.separate_no_index and 0 or 1
			end

			-- Normalize to the canonical parameter name. If it's a list, but the alias is not, then determine the index.
			local raw_name = param.alias_of
			if param.alias_of then
				raw_type = type(raw_name)
				if raw_type == "number" then
					local main_param = params[raw_name]
					if param_types and param_types[main_param] == "table" and main_param.list then
						index = index or param.separate_no_index and 0 or 1
						canonical = raw_name + index - 1
					else
						canonical = raw_name
					end
					name = raw_name
				else
					name = gsub(raw_name, "\1", "")
					local main_param = params[name]
					if param_types and param_types[main_param] == "table" and main_param.list then
						index = index or param.separate_no_index and 0 or 1
					end
					if not index or index == 0 then
						canonical = name
					elseif name == raw_name then
						canonical = name .. index
					else
						canonical = gsub(raw_name, "\1", index)
					end
				end
			else
				canonical = orig_name
			end

			-- Only recognize demo parameters if this is the current template or module's
			-- page, or its documentation page.
			if param.demo and not is_own_page("include_documentation") then
				args_unknown = unknown_param(name, val, args_unknown or {})
			end

			-- Remove leading and trailing whitespace unless no_trim is true.
			if param.no_trim then
				check_string_param(param.type, name, "no_trim")
			else
				val = trim(val)
			end

			-- Empty string is equivalent to nil unless allow_empty is true.
			if param.allow_empty then
				check_string_param(param.type, name, "allow_empty")
			elseif val == "" then
				val = nil
			end

			-- Can't use "if val" alone, because val may be a boolean false.
			if val ~= nil then
				-- Convert to proper type if necessary.
				local main_param = params[raw_name]
				if not main_param or (param_types and param_types[main_param] == "table") then
					val = convert_val(val, orig_name, main_param or param)
				end

				-- Mark it as no longer required, as it is present.
				if required then
					required[name] = nil
				end

				-- Store the argument value.
				if index then
					-- If the parameter is duplicated, throw an error.
					if args_new[name][index] ~= nil then
						process_error(
							"Parameter %s has been entered more than once. This is probably because a list parameter has been entered without an index and with index 1 at the same time, or because a parameter alias has been used.",
							canonical
						)
					end
					args_new[name][index] = val

					-- Store the highest index we find.
					args_new[name].maxindex = max(index, args_new[name].maxindex)
					if args_new[name][0] ~= nil then
						args_new[name].default = args_new[name][0]
						if args_new[name].maxindex == 0 then
							args_new[name].maxindex = 1
						end
						args_new[name][0] = nil
					end

					if params[name].list then
						-- Don't store index 0, as it's a proxy for the default.
						if index > 0 then
							args_new[name][index] = val
							-- Store the highest index we find.
							args_new[name].maxindex = max(index, args_new[name].maxindex)
						end
					else
						args_new[name] = val
					end
				else
					-- If the parameter is duplicated, throw an error.
					if args_new[name] ~= nil then
						process_error(
							"Parameter %s has been entered more than once. This is probably because a parameter alias has been used.",
							canonical
						)
					end

					if not param.alias_of then
						args_new[name] = val
					else
						local main_param = params[param.alias_of]
						if param_types and param_types[main_param] == "table" and main_param.list then
							args_new[param.alias_of][1] = val
							-- Store the highest index we find.
							args_new[param.alias_of].maxindex = max(1, args_new[param.alias_of].maxindex)
						else
							args_new[param.alias_of] = val
						end
					end
				end
			end
		end
	end

	-- Remove holes in any list parameters if needed.
	if list_args then
		for name, val in next, list_args do
			handle_holes(params[name], val, name)
		end
	end

	-- If the current page is the template which invoked this Lua instance, then ignore the `require` flag, as it
	-- means we're viewing the template directly. Required parameters sometimes have a `template_default` key set,
	-- which gets used in such cases as a demo.
	-- Note: this won't work on other pages in the Template: namespace (including the /documentation subpage),
	-- or if the #invoke: is on a page in another namespace.
	local pagename_set = args_new.pagename

	-- Handle defaults.
	for name, param in pairs(params) do
		if param_types and param_types[param] == "table" then
			local arg_new = args_new[name]
			if arg_new == nil then
				args_new[name] = convert_default_val(name, param, pagename_set, any_args_set)
			elseif param.list and arg_new[1] == nil then
				local default_val = convert_default_val(name, param, pagename_set, any_args_set)
				if default_val ~= nil then
					arg_new[1] = default_val
					if arg_new.maxindex == 0 then
						arg_new.maxindex = 1
					end
				end
			end
		end
	end
	
	-- The required table should now be empty.
	-- If any parameters remain, throw an error, unless we're on the current template or module's page.
	if required and next(required) ~= nil and not is_own_page() then
		params_list_error(required, "required")
	-- Return the arguments table.
	-- If there are any unknown parameters, throw an error, unless return_unknown is set, in which case return args_unknown as a second return value.
	elseif return_unknown then
		return args_new, args_unknown or {}
	elseif args_unknown and next(args_unknown) ~= nil then
		params_list_error(args_unknown, "not used by this template")
	end
	return args_new
end

return export