Module:parameters: Difference between revisions

no edit summary
No edit summary
No edit summary
Line 1: Line 1:
--[==[TODO:
* Change certain flag names, as some are misnomers:
* Change `allow_holes` to `keep_holes`, because it's not the inverse of `disallow_holes`.
* Change `allow_empty` to `keep_empty`, as it causes them to be kept as "" instead of deleted.
* Sort out all the internal error calls. Manual error(format()) calls are used when certain parameters shouldn't be dumped, so find a way to avoid that.
]==]
local export = {}
local export = {}


local collation_module = "Module:collation"
local families_module = "Module:families"
local families_module = "Module:families"
local function_module = "Module:fun"
local functions_module = "Module:fun"
local gender_and_number_utilities_module = "Module:gender and number utilities"
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 math_module = "Module:math"
local pages_module = "Module:pages"
local pages_module = "Module:pages"
local parameters_finalize_set_module = "Module:parameters/finalizeSet"
local parameters_track_module = "Module:parameters/track"
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 scribunto_module = "Module:Scribunto"
local scripts_module = "Module:scripts"
local scripts_module = "Module:scripts"
local string_utilities_module = "Module:string utilities"
local string_utilities_module = "Module:string utilities"
Line 23: Line 35:
local find = string.find
local find = string.find
local format = string.format
local format = string.format
local gmatch = string.gmatch
local gsub = string.gsub
local gsub = string.gsub
local insert = table.insert
local insert = table.insert
Line 35: Line 46:
local pairs = pairs
local pairs = pairs
local pcall = pcall
local pcall = pcall
local rawset = rawset
local require = require
local require = require
local sort = table.sort
local sub = string.sub
local sub = string.sub
local tonumber = tonumber
local tonumber = tonumber
local traceback = debug.traceback
local type = type
local type = type
local unpack = unpack or table.unpack -- Lua 5.2 compatibility


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


--[==[
Loaders for functions in other modules, which overwrite themselves with the target function when called. This ensures modules are only loaded when needed, retains the speed/convenience of locally-declared pre-loaded functions, and has no overhead after the first call, since the target functions are called directly in any subsequent calls.]==]
local function decode_entities(...)
local function decode_entities(...)
decode_entities = require(string_utilities_module).decode_entities
decode_entities = require(string_utilities_module).decode_entities
return decode_entities(...)
return decode_entities(...)
end
local function extend(...)
extend = require(table_module).extend
return extend(...)
end
local function finalize_set(...)
finalize_set = require(parameters_finalize_set_module)
return finalize_set(...)
end
end


Line 97: Line 118:


local function is_callable(...)
local function is_callable(...)
is_callable = require(function_module).is_callable
is_callable = require(functions_module).is_callable
return 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
end


Line 124: Line 140:
iterate_list = require(table_module).iterateList
iterate_list = require(table_module).iterateList
return iterate_list(...)
return iterate_list(...)
end
local function list_to_set(...)
list_to_set = require(table_module).listToSet
return list_to_set(...)
end
end


Line 134: Line 145:
num_keys = require(table_module).numKeys
num_keys = require(table_module).numKeys
return num_keys(...)
return num_keys(...)
end
local function parse_gender_and_number_spec(...)
parse_gender_and_number_spec = require(gender_and_number_utilities_module).parse_gender_and_number_spec
return parse_gender_and_number_spec(...)
end
end


Line 146: Line 162:
end
end


local function scribunto_param_key(...)
local function php_trim(...)
scribunto_param_key = require(string_utilities_module).scribunto_param_key
php_trim = require(scribunto_module).php_trim
return scribunto_param_key(...)
return php_trim(...)
end
 
local function scribunto_parameter_key(...)
scribunto_parameter_key = require(scribunto_module).scribunto_parameter_key
return scribunto_parameter_key(...)
end
 
local function sort(...)
sort = require(collation_module).sort
return sort(...)
end
end


Line 154: Line 180:
sorted_pairs = require(table_module).sortedPairs
sorted_pairs = require(table_module).sortedPairs
return sorted_pairs(...)
return sorted_pairs(...)
end
local function split(...)
split = require(string_utilities_module).split
return split(...)
end
end


Line 171: Line 192:
end
end


local function trim(...)
local function tonumber_extended(...)
trim = require(string_utilities_module).trim
tonumber_extended = require(math_module).tonumber_extended
return trim(...)
return tonumber_extended(...)
end
 
local function track(...)
track = require(parameters_track_module)
return track(...)
end
end


Line 265: Line 291:
   (including signs, decimal digits, and leading zeroes after {"0x"}). Hexadecimal inputs are not case-sensitive. Lua's
   (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.
   special number values (`inf` and `nan`) are not possible inputs.
:; {type = "range"}
:: The value is interpreted as a hyphen-separated range of two numbers (e.g. {"2-4"} is interpreted as the range from
  {2} to {4}). A number input without a hyphen is interpreted as a range from that number to itself (e.g. the input {"1"} is interpreted as the range from {1} to {1}). Any optional flags which are available for numbers will also work for ranges.
:; {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 288: Line 317:
   (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"}
:; {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:
:: 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}
::; {namespace = n}
::: The default namespace, where {n} is a namespace number; this is treated as {0} (the mainspace) if not specified.
::: The default namespace, where {n} is a namespace number; this is treated as {0} (the mainspace) if not specified.
Line 294: Line 325:
::: External titles are treated as valid.
::: External titles are treated as valid.
::; {prefix = "namespace override"} (default)
::; {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"}.
::: 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"}
::; {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"}.
::: 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"}
::; {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"}.
::: 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 = "parameter"}
:: The value is interpreted as the name of a parameter, and will be normalized using the method that Scribunto uses when
  constructing a {frame.args} table of arguments. This means that integers will be converted to numbers, but all other
  arguments will remain as strings (e.g. {"1"} will be normalized to {1}, but {"foo"} and {"1.5"} will remain
  unchanged). Note that Scribunto also trims parmeter names, following the same trimming method that this module
  applies by default to all parameter types.
:: This type is useful when one set of input arguments is used to construct a {params} table for use in a subsequent
  {export.process()} call with another set of input arguments; for instance, the set of valid parameters for a template
  might be defined as {{tl|#invoke:[some module]|args=}} in the template, where {args} is a sublist of valid parameters
  for the template.
:; {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()`
Line 312: Line 362:
   [[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 = "genders"}
:: The value is interpreted as one or more comma-separated gender/number specs, in the format prescribed by
  [[Module:gender and number]]. Inline modifiers (`<q:...>`, `<qq:...>`, `<l:...>`, `<ll:...>` or `<ref:...>`) may be
  attached to a gender/number spec.
:; {type = function(val) ... end}
:; {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
:: `type` may be set to a function (or callable table), which must take the argument value as its sole argument, and must
Line 334: Line 388:
: Require that the value of the parameter be one of the specified list of values (or omitted, if {required = true} isn't
: 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
   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
   {type = "number"}, in which case they should be numbers. A individual value in the list can also be an ''alias list'',
  causes an error to be thrown.
  which is a list where the first value is the "canonical" value and the remainder are aliases. When one of the aliases
  is used, the resulting parameter field in the returned arguments structure will have the canonical value. The use of
  `set` is disallowed if {type = "boolean"} and causes an error to be thrown.
; {sublist =}
; {sublist =}
: 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
Line 379: Line 435:
   {{para|head3}} lines up with {{para|tr3}}), unless {allow_holes = true} is given and you are prepared to handle the
   {{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.
   holes in the returned lists.
; {disallow_missing = true}
: This is similar to {disallow_holes = true}, but an error will not be thrown if an argument is blank, rather than
  completely missing. This may be used to tolerate intermediate blank numerical parameters, which sometimes occur in list
  templates. For instance, `head=a|head2=|head3=c` will not throw an error, but `head=a|head3=c` will.
; {require_index = true}
; {require_index = true}
: This is used in conjunction with list-type parameters. By default, the first parameter can have its index omitted.
: This is used in conjunction with list-type parameters. By default, the first parameter can have its index omitted.
Line 391: Line 451:
   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.
; {flatten = true}
: This is used in conjunction with list-type parameters when `sublist` or a list-generating type such as {"labels"} or
  {"genders"} is also specified, and causes the resulting list to be flattened. Not currently compatible with
  {allow_holes = true}.
; {demo = true}
; {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.
: 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.
; {deprecated = true}
: This is for tracking the use of deprecated parameters, including any aliases that are being brought out of use. See
  [[Wiktionary:Tracking]] for more information.
]==]
]==]


Line 430: Line 502:
end
end
return list_to_text(list, nil, conjunction)
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
end


Line 459: Line 511:
return msg
return msg
end
end
return format("%s (processed value %s)", msg, dump_if_unusual(processed))
local processed_type = type(processed)
return format("%s (processed value %s)",
msg, (processed_type == "string" or processed_type == "number") and processed or dump(processed)
)
end
end


Line 482: Line 537:
-- 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.
-- 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 function validate_name(name, desc, extra_name, is_argument)
local normalized = scribunto_param_key(name)
local normalized = scribunto_parameter_key(name)
if name and name == normalized then
if name and name == normalized then
if is_argument or type(name) ~= "string" then
if is_argument or type(name) ~= "string" then
return
return
end
end
local placeholder = find(name, "\1", 1, true)
local placeholder = find(name, "\1", nil, true)
if not placeholder then
if not placeholder then
return
return
elseif find(name, "\1", placeholder + 1, true) then
elseif find(name, "\1", placeholder + 1, true) then
error(format(
error(format(
'Expected %s to only contain one placeholder, but saw %s',
"Internal error: expected %s to only contain one placeholder, but saw %s",
extra_name and (desc .. dump(extra_name)) or desc, dump(name)
extra_name and (desc .. dump(extra_name)) or desc, dump(name)
))
))
end
end
local first_name = gsub(name, "\1", "1")
local first_name = gsub(name, "\1", "1")
normalized = scribunto_param_key(first_name)
normalized = scribunto_parameter_key(first_name)
if first_name == normalized then
if first_name == normalized then
return
return
end
end
error(format(
error(format(
'%s cannot resolve to numeric parameters once any placeholder has been substituted, but %s resolves to %s',
"Internal error: %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)
extra_name and (desc .. dump(extra_name)) or desc, dump(name), dump(normalized)
))
))
elseif normalized == nil then
elseif normalized == nil then
error(format(
error(format(
'Expected %s to be of type string or number, but saw %s',
"Internal error: expected %s to be of type string or number, but saw %s",
extra_name and (desc .. dump(extra_name)) or desc, type(name)
extra_name and (desc .. dump(extra_name)) or desc, type(name)
))
))
end
end
error(format(
error(format(
"Expected %s to be Scribunto-compatible: %s (a %s) should be %s (a %s)",
"Internal error: 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)
extra_name and (desc .. dump(extra_name)) or desc, dump(name), type(name), dump(normalized), type(normalized)
))
))
end
local function validate_alias_options(...)
local invalid = {
required = true,
default = true,
template_default = true,
allow_holes = true,
disallow_holes = true,
disallow_missing = true,
}
function validate_alias_options(param, name, main_param, alias_of)
for k in pairs(param) do
if invalid[k] then
track("bad alias option")
-- internal_process_error(
-- "parameter %s cannot have the option %s, as it is an alias of parameter %s.",
-- name, option, alias_of
-- )
end
end
-- Soon, aliases will inherit options from the main parameter via __index. Track cases where this would happen.
if main_param ~= true then
for k in pairs(main_param) do
if param[k] == nil and not invalid[k] then
if k == "list" then -- these need to be changed to list = false to retain current behaviour
track("mismatched list alias option")
elseif not (k == "type" or k == "set" or k == "sublist") then -- rarely specified on aliases, as they're effectively inherited already
track("mismatched alias option")
end
end
end
end
end
validate_alias_options(...)
end
end


Line 531: Line 623:
end
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
-- 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(valid)
return (#valid > 1 and "either " or "") .. concat_list(valid, " or ")
end
 
-- Signal an error for a value `val` that is not of the right type `valid` (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
-- 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
-- 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).
-- 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)
local function convert_val_error(val, name, valid, seetext)
if is_callable(name) then
if is_callable(name) then
if type(param_type) == "table" then
if type(valid) == "table" then
param_type = "choice, must be " .. format_choice_list(param_type)
valid = "choice, must be " .. format_choice_list(valid)
end
end
name(format("Invalid %s; the value %s is not valid%s", param_type, val, seetext and "; see " .. seetext or ""))
name(format("Invalid %s; the value %s is not valid%s", valid, val, seetext and "; see " .. seetext or ""))
else
else
if type(param_type) == "table" then
if type(valid) == "table" then
param_type = "must be " .. format_choice_list(param_type)
valid = format_choice_list(valid)
else
else
param_type = "should be a valid " .. param_type
valid = "a valid " .. valid
end
end
error(format("Parameter %s %s; the value %s is not valid.%s", dump(name), param_type, dump(val),
error(format("Parameter %s must be %s; the value %s is not valid.%s", dump(name), valid, dump(val),
seetext and " See " .. seetext .. "." or ""))
seetext and " See " .. seetext .. "." or ""))
end
end
Line 574: Line 672:
local function save_pattern(name, list_name, patterns)
local function save_pattern(name, list_name, patterns)
name = type(name) == "string" and gsub(name, "\1", "") or name
name = type(name) == "string" and gsub(name, "\1", "") or name
if find(list_name, "\1", 1, true) then
if find(list_name, "\1", nil, true) then
patterns["^" .. gsub(pattern_escape(list_name), "\1", "([1-9]%%d*)") .. "$"] = name
patterns["^" .. gsub(pattern_escape(list_name), "\1", "([1-9]%%d*)") .. "$"] = name
else
else
Line 593: Line 691:
if sublist == true then
if sublist == true then
return gsplit(val, "%s*,%s*")
return gsplit(val, "%s*,%s*")
-- Split an argument on comma, but not comma followed by whitespace.
elseif sublist == "comma without whitespace" then
elseif sublist == "comma without whitespace" then
sublist = split_on_comma_without_whitespace
-- If difficult cases, use split_on_comma.
if find(val, "\\", nil, true) or match(val, ",%s") then
return iterate_list(split_on_comma(val))
end
-- Otherwise, use gsplit.
return gsplit(val, ",")
elseif type(sublist) == "string" then
elseif type(sublist) == "string" then
return gsplit(val, sublist)
return gsplit(val, sublist)
elseif not is_callable(sublist) then
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)))
error(format('Internal error: expected `sublist` to be of type "string" or "function" or boolean `true`, but saw %s', dump(sublist)))
end
end
return iterate_list(sublist(val, make_parse_err(val, name)))
return iterate_list(sublist(val, make_parse_err(val, name)))
Line 605: Line 709:
-- 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 `param_type` is a
-- corresponding parameter passed into convert_val() and may be a function to signal an error. Optional `param_type` is
-- string specifying the conversion type of `val` and is used for special-casing: If `param_type` is "boolean", an internal
-- a string specifying the conversion type of `val` and is used for special-casing: If `param_type` is "boolean", an
-- error is thrown (since `set` cannot be used in conjunction with booleans) and if `param_type` is "number", no checking
-- internal error is thrown (since `set` cannot be used in conjunction with booleans) and if `param_type` is "number",
-- happens because in this case `set` contains numbers and is checked inside the number conversion function itself,
-- no checking happens because in this case `set` contains numbers and is checked inside the number conversion function
-- after converting `val` to a number.
-- itself, after converting `val` to a number. Return the canonical value of `val` (which may be different from `val`
-- if an alias map is given).
local function check_set(val, name, param, param_type)
local function check_set(val, name, param, param_type)
if param_type == "boolean" then
if param_type == "boolean" then
error(format('Internal error: Cannot use `set` with `type = "%s"`', param_type))
error(format('Internal error: cannot use `set` with `type = "%s"`', param_type))
-- Needs to be special cased because the check happens after conversion to numbers.
elseif param_type == "number" then
elseif param_type == "number" then
-- Needs to be special cased because the check happens after conversion to numbers.
return val
return
end
 
local set, map = param.set
if sets == nil then
map = finalize_set(set, name)
sets = {[set] = map}
else
map = sets[set]
if map == nil then
map = finalize_set(set, name)
sets[set] = map
end
end
 
local newval = map[val]
if newval == true then
return val
elseif newval ~= nil then
return newval
end
end
if not param.set[val] then
 
local list = {}
local list = {}
for k in pairs(param.set) do
for k, v in sorted_pairs(map) do
if v == true then
insert(list, dump(k))
insert(list, dump(k))
else
insert(list, ("%s (alias of %s)"):format(dump(k), dump(v)))
end
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
-- 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


Line 638: Line 764:
func, method = get_language_by_name, "name"
func, method = get_language_by_name, "name"
else
else
error(format('Internal error: Expected `method` for type `language` to be "code", "name" or undefined, but saw %s', dump(method)))
error(format('Internal error: expected `method` for type `language` to be "code", "name" or undefined, but saw %s', dump(method)))
end
end
local lang = func(val, nil, allow_etym, param.family)
local lang = func(val, nil, allow_etym, param.family)
Line 654: Line 780:
end
end
convert_val_error(val, name, concat_list(list, " or ") .. " " .. (method == "name" and "name" or "code"), concat_list(links, " and "))
convert_val_error(val, name, concat_list(list, " or ") .. " " .. (method == "name" and "name" or "code"), concat_list(links, " and "))
end
local function convert_number(val, allow_hex)
-- Call tonumber_extended with the `real_finite` flag, which filters out ±infinity and NaN.
-- By default, specify base 10, which prevents 0x hex inputs from being converted.
-- If `allow_hex` is set, then don't give a base, which means 0x hex inputs will work.
local num = tonumber_extended(val, not allow_hex and 10 or nil, "finite_real")
if not num then
return num
end
if match(val, "[eEpP.]") then -- float
track("number not an integer")
end
if find(val, "+", nil, true) then
track("number with +")
end
-- Track various unusual number inputs to determine if it should be restricted to positive integers by default (possibly including 0).
if not is_positive_integer(num) then
track("number not a positive integer")
if num == 0 then
track("number is 0")
elseif not is_integer(num) then
track("number not an integer")
end
end
return num
end
end


Line 669: Line 821:
func, method = get_family_by_name, "name"
func, method = get_family_by_name, "name"
else
else
error(format('Internal error: Expected `method` for type `family` to be "code", "name" or undefined, but saw %s', dump(method)))
error(format('Internal error: expected `method` for type `family` to be "code", "name" or undefined, but saw %s', dump(method)))
end
end
return func(val) or convert_val_error(val, name, "family " .. method, "[[WT:LOF]]")
return func(val) or convert_val_error(val, name, "family " .. method, "[[WT:LOF]]")
Line 683: Line 835:
end,
end,


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


["number"] = function(val, name, param)
["number"] = function(val, name, param)
local allow_hex = param.allow_hex
local allow_hex = param.allow_hex
if allow_hex and allow_hex ~= true then
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)))
error(format(
end
'Internal error: expected `allow_hex` for type `number` to be of type "boolean" or undefined, but saw %s',
local num = tonumber(val)
dump(allow_hex)
-- 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
local num = convert_number(val, allow_hex)
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(num, name, param)
num = check_set(num, name, param)
end
if num then
return num
end
convert_val_error(val, name, (allow_hex and "decimal or hexadecimal " or "") .. "number")
end,
 
["range"] = 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 `range` to be of type "boolean" or undefined, but saw %s',
dump(allow_hex)
))
end
end
return num
-- Pattern ensures leading minus signs are accounted for.
local m1, m2 = match(val, "^(%s*%S.-)%-(%s*%S.*)")
if m1 then
m1 = convert_number(m1, allow_hex)
if m1 then
m2 = convert_number(m2, allow_hex)
if m2 then
return {m1, m2}
end
end
end
-- Try `val` if it couldn't be split into a range, and return a range of `val` to `val` if possible.
local num = convert_number(val, allow_hex)
if num then
return {num, num}
end
convert_val_error(val, name, (allow_hex and "decimal or hexadecimal " or "") .. "number or a hyphen-separated range of two numbers")
end,
 
["parameter"] = function(val, name, param)
-- Use the `no_trim` option, as any trimming will have already been done.
return scribunto_parameter_key(val, true)
end,
end,
 
["qualifier"] = function(val, name, param)
["qualifier"] = function(val, name, param)
return {val}
return {val}
end,
end,
 
["references"] = function(val, name, param)
["references"] = function(val, name, param)
return parse_references(val, make_parse_err(val, name))
return parse_references(val, make_parse_err(val, name))
end,
["genders"] = function(val, name, param)
if not val:find("[,<]") then
return {{spec = val}}
end
-- NOTE: We don't pass in allow_space_around_comma. Consistent with other comma-separated types, there shouldn't
-- be spaces around the comma.
return parse_gender_and_number_spec {
spec = val,
parse_err = make_parse_err(val, name),
allow_multiple = true,
}
end,
end,


Line 720: Line 917:
func, method = get_script_by_name, "name"
func, method = get_script_by_name, "name"
else
else
error(format('Internal error: Expected `method` for type `script` to be "code", "name" or undefined, but saw %s', dump(method)))
error(format('Internal error: expected `method` for type `script` to be "code", "name" or undefined, but saw %s', dump(method)))
end
end
return func(val) or convert_val_error(val, name, "script " .. method, "[[WT:LOS]]")
return func(val) or convert_val_error(val, name, "script " .. method, "[[WT:LOS]]")
Line 726: Line 923:


["string"] = function(val, name, param) -- To be removed as unnecessary.
["string"] = function(val, name, param) -- To be removed as unnecessary.
track("string")
return val
return val
end,
end,
Line 740: Line 938:
nil
nil
if valid_type then
if valid_type then
error(format('Internal error: Expected `namespace` for type `title` to be %s, but saw %s', valid_type, dump(namespace)))
error(format('Internal error: expected `namespace` for type `title` to be %s, but saw %s', valid_type, dump(namespace)))
end
end
end
end
Line 764: Line 962:
-- 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).
-- 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
local success
success, title = pcall(new_title, val, match(decoded, ".-%f[:]") or namespace)
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.
-- 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
if not success then
Line 770: Line 968:
end
end
else
else
error(format('Internal error: Expected `prefix` for type `title` to be "force", "full override", "namespace override" or undefined, but saw %s', dump(prefix)))
error(format('Internal error: expected `prefix` for type `title` to be "force", "full override", "namespace override" or undefined, but saw %s', dump(prefix)))
end
end
local allow_external = param.allow_external
local allow_external = param.allow_external
Line 778: Line 976:
return title and is_internal_title(title) and title or convert_val_error(val, name, "Wiktionary page title")
return title and is_internal_title(title) and title or convert_val_error(val, name, "Wiktionary page title")
end
end
error(format('Internal error: Expected `allow_external` for type `title` to be of type "boolean" or undefined, but saw %s', dump(allow_external)))
error(format('Internal error: expected `allow_external` for type `title` to be of type "boolean" or undefined, but saw %s', dump(allow_external)))
end,
end,


Line 788: Line 986:
return get_wm_lang_by_code(val) or convert_val_error(val, name, "Wikimedia language code")
return get_wm_lang_by_code(val) or convert_val_error(val, name, "Wikimedia language code")
end
end
error(format('Internal error: Expected `fallback` for type `Wikimedia language` to be of type "boolean" or undefined, but saw %s', dump(fallback)))
error(format('Internal error: expected `fallback` for type `Wikimedia language` to be of type "boolean" or undefined, but saw %s', dump(fallback)))
end,
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.
-- 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)
__call = function(self, val, name, param, param_type, default)
local val_type = type(val)
local val_type = type(val)
-- TODO: check this for all possible parameter types.
-- TODO: check this for all possible parameter types.
if val_type == param_type then
if val_type == param_type then
return val
return val
-- TODO: throw an internal error.
elseif val_type ~= "string" then
local expected = "string"
if default and (param_type == "boolean" or param_type == "number") then
expected = param_type .. " or " .. expected
end
error(format(
"Internal error: %sargument %s has the type %s; expected a %s.",
default and (default .. " for ") or "", name, dump(val_type), expected
))
end
end
local func = self[param_type]
local func = self[param_type]
Line 815: Line 1,021:
etc. It returns the converted value.
etc. It returns the converted value.
]==]
]==]
local function convert_val(val, name, param)
local function convert_val(val, name, param, default)
local param_type = param.type or "string"
local param_type = param.type or "string"
-- If param.type is a function, resolve it to a recognized type.
-- If param.type is a function, resolve it to a recognized type.
Line 821: Line 1,027:
param_type = param_type(val)
param_type = param_type(val)
end
end
local sublist = param.sublist
local convert, sublist = param.convert, param.sublist
if sublist then
-- `val` might not be a string if it's the default value.
local retlist = {}
if sublist and type(val) == "string" then
if type(val) ~= "string" then
local retlist, set = {}, param.set
error(format("Internal error: %s is not a string.", dump(val)))
if convert then
end
local thisindex, thisval, insval, parse_err = 0
if param.convert then
local thisval, insval
local thisindex = 0
local parse_err
if is_callable(name) then
if is_callable(name) then
-- We assume the passed-in error function in `name` already shows the parameter name and raw value.
-- We assume the passed-in error function in `name` already shows the parameter name and raw value.
parse_err = function(msg)
function parse_err(msg)
name(format("%s: item #%s=%s", msg_with_processed(msg, thisval, insval), thisindex,
name(format("%s: item #%s=%s",
thisval))
msg_with_processed(msg, thisval, insval), thisindex, thisval)
)
end
end
else
else
parse_err = function(msg)
function parse_err(msg)
error(format("%s: item #%s=%s of parameter %s=%s", msg_with_processed(msg, thisval, insval),
error(format("%s: item #%s=%s of parameter %s=%s",
thisindex, thisval, name, val))
msg_with_processed(msg, thisval, insval), thisindex, thisval, name, val)
)
end
end
end
end
for v in split_sublist(val, name, sublist) do
for v in split_sublist(val, name, sublist) do
thisval = v
thisindex, thisval = thisindex + 1, v
thisindex = thisindex + 1
if set then
if param.set then
v = check_set(v, name, param, param_type)
check_set(v, name, param, param_type)
end
end
insert(retlist, param.convert(type_handlers(v, name, param, param_type), parse_err))
insert(retlist, convert(type_handlers(v, name, param, param_type, default), parse_err))
end
end
else
else
for v in split_sublist(val, name, sublist) do
for v in split_sublist(val, name, sublist) do
if param.set then
if set then
check_set(v, name, param, param_type)
v = check_set(v, name, param, param_type)
end
end
insert(retlist, type_handlers(v, name, param, param_type))
insert(retlist, type_handlers(v, name, param, param_type, default))
end
end
end
end
return retlist
return retlist
else
elseif param.set then
if param.set then
val = check_set(val, name, param, param_type)
check_set(val, name, param, param_type)
end
end
local retval = type_handlers(val, name, param, param_type, default)
local retval = type_handlers(val, name, param, param_type)
if convert then
if param.convert then
local parse_err
local parse_err
if is_callable(name) then
if is_callable(name) then
-- We assume the passed-in error function in `name` already shows the parameter name and raw value.
-- We assume the passed-in error function in `name` already shows the parameter name and raw value.
if retval == val then
if retval == val then
-- This is an optimization to avoid creating a closure. The second arm works correctly even
-- This is an optimization to avoid creating a closure. The second arm works correctly even
-- when retval == val.
-- when retval == val.
parse_err = name
parse_err = name
else
parse_err = function(msg)
name(msg_with_processed(msg, val, retval))
end
end
else
else
parse_err = function(msg)
function parse_err(msg)
error(format("%s: parameter %s=%s", 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
function parse_err(msg)
error(format("%s: parameter %s=%s", msg_with_processed(msg, val, retval), name, val))
end
end
end
return retval
retval = convert(retval, parse_err)
end
end
-- If `sublist` is set but the input wasn't a string, return `retval` as a one-item list.
if sublist then
retval = {retval}
end
return retval
end
end
export.convert_val = convert_val -- used by [[Module:parameter utilities]]
export.convert_val = convert_val -- used by [[Module:parameter utilities]]


local function unknown_param(name, val, args_unknown)
local function unknown_param(name, val, args_unknown)
track("unknown parameters")
args_unknown[name] = val
args_unknown[name] = val
return args_unknown
return args_unknown
end
end


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


local function handle_holes(param, val, name)
local function hole_error(params, name, listname, this, nxt, extra)
-- `process_error` calls `dump` on values to be inserted into
-- error messages, but with numeric lists this causes "numeric"
-- to look like the name of the list rather than a description,
-- as `dump` adds quote marks. Insert it early to avoid this,
-- but add another %s specifier in all other cases, so that
-- actual list names will be displayed properly.
local offset, specifier, starting_from = 0, "%s", ""
local msg = "Item %%d in the list of %s parameters must be given if item %%d is given, because %sthere shouldn't be any gaps due to missing%s parameters."
local specs = {}
if type(listname) == "string" then
specs[2] = listname
elseif type(name) == "number" then
offset = name - 1 -- To get the original parameter.
specifier = "numeric"
-- If the list doesn't start at parameter 1, avoid implying
-- there can't be any gaps in the numeric parameters if
-- some parameter with a lower key is optional.
for j = name - 1, 1, -1 do
local _param = params[j]
if not (_param and _param.required) then
starting_from = format("(starting from parameter %d) ", dump(j + 1))
break
end
end
else
specs[2] = name
end
specs[1] = this + offset -- Absolute index for this item.
insert(specs, nxt + offset) -- Absolute index for the next item.
process_error(format(msg, specifier, starting_from, extra or ""), unpack(specs))
end
 
local function check_disallow_holes(params, val, name, listname, extra)
for i = 1, val.maxindex do
if val[i] == nil then
hole_error(params, name, listname, i, num_keys(val)[i], extra)
end
end
end
 
local function handle_holes(params, val, name)
local param = params[name]
local disallow_holes = param.disallow_holes
-- Iterate up the list, and throw an error if a hole is found.
-- Iterate up the list, and throw an error if a hole is found.
if param.disallow_holes then
if disallow_holes then
for i = 1, val.maxindex do
check_disallow_holes(params, val, name, param.list, " or empty")
if val[i] == nil then
end
local listname = param.list
-- Iterate up the list, and throw an error if a hole is found due to a
if type(listname) == "string" then
-- missing parameter, treating empty parameters as part of the list. This
listname = dump(listname)
-- applies beyond maxindex if blank arguments are supplied beyond it, so
elseif type(name) == "number" then
-- isn't mutually exclusive with `disallow_holes`.
i = i + name - 1 -- Absolute index.
local empty = val.empty
listname = "numeric"
if param.disallow_missing then
else
if empty then
listname = dump(name)
-- Remove `empty` from `val`, so it doesn't get returned.
val.empty = nil
for i = 1, max(val.maxindex, empty.maxindex) do
if val[i] == nil and not empty[i] then
local keys = extend(num_keys(val), num_keys(empty))
sort(keys)
hole_error(params, name, param.list, i, keys[i])
end
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 there's no table of empty parameters, the check is identical to
-- `disallow_holes`, except that the error message only refers to
-- missing parameters, not missing or empty ones. If `disallow_holes` is
-- also set, there's no point checking again.
elseif not disallow_holes then
check_disallow_holes(params, val, name, param.list)
end
end
-- If `allow_holes` is set, there's nothing to do. This is placed after
end
-- `disallow_holes`, so that the latter takes priority.
-- If `allow_holes` is set, there's nothing left to do.
elseif param.allow_holes then
if param.allow_holes then
return
-- do nothing
-- Otherwise, remove any holes. Use num_keys to get a list of numerical keys
-- Otherwise, remove any holes: `pairs` won't work, as it's unsorted, and
-- instead of iterating from 1 to `maxindex`, as it could be enormous if
-- iterating from 1 to `maxindex` times out with inputs like |100000000000=,
-- there is a huge hole in the list.
-- so use num_keys to get a list of numerical keys sorted from lowest to
-- highest, then iterate up the list, moving each value in `val` to the
-- lowest unused positive integer key. This also avoids the need to create a
-- new table. If `disallow_holes` is specified, then there can't be any
-- holes in the list, so there's no reason to check again; this doesn't
-- apply to `disallow_missing`, however.
else
else
local keys, i = num_keys(val), 0
if not disallow_holes then
while true do
local keys, i = num_keys(val), 0
i = i + 1
while true do
local key = keys[i]
i = i + 1
if key == nil then
local key = keys[i]
break
if key == nil then
elseif i ~= key then
break
val[i], val[key] = val[key], nil
elseif i ~= key then
track("holes compressed")
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
end
 
local function maybe_flatten(params, val, name)
local param = params[name]
if param.flatten then
if param.allow_holes then
process_error("For parameter %s, can't set both `allow_holes` and `flatten`", name)
end
if not param.sublist and param.type ~= "genders" and param.type ~= "labels" and
param.type ~= "references" and param.type ~= "qualifier" then
process_error("For parameter %s, can only set `flatten` along with `sublist` or a list-generating type", name)
end
-- Do the flattening ourselves rather than calling flatten() in [[Module:table]], which will attempt to
-- flatten non-list objects like title objects, and cause an error in the process.
-- FIXME: We should do this in-place if possible.
local newlist = {}
for _, sublist in ipairs(val) do
for _, item in ipairs(sublist) do
insert(newlist, item)
end
end
end
end
val = newlist
end
end
-- Some code depends on only numeric params being present when no holes are
return val
-- allowed (e.g. by checking for the presence of arguments using next()), so
-- remove `maxindex`.
val.maxindex = nil
end
end


Line 954: Line 1,243:
-- transcluded into the template page. HACK: We still run into problems on documentation pages transcluded into the
-- 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.
-- 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)
local function convert_default_val(name, param, pagename_set, any_args_set, add_empty_sublist)
if not pagename_set then
if not pagename_set then
local val = param.template_default
local val = param.template_default
if val ~= nil and not any_args_set and is_own_page() then
if val ~= nil and not any_args_set and is_own_page() then
return convert_val(val, name, param)
return convert_val(val, name, param, "template default")
end
end
end
end
local val = param.default
local val = param.default
if val ~= nil then
if val ~= nil then
return convert_val(val, name, param)
return convert_val(val, name, param, "default")
-- Sublist parameters should return an empty table if not given, but only do
-- this if the parameter isn't also a list (in which case it will already
-- be an empty table).
-- FIXME: do this once all modules that pass in a sublist parameter treat an empty sublist identically to a nil argument; some currently do things based on the fact an argument exists at all.
-- elseif add_empty_sublist and param.sublist then
--return {}
end
end
end
end
Line 988: Line 1,283:
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, args_unknown, any_args_set, param_types, required, patterns, list_args, index_list,
local args_new, args_unknown, any_args_set, required, patterns, list_args, index_list, args_placeholders, placeholders_n = {}
args_placeholders, n_ph = {}


-- TODO: memoize the processing of each unique `param` value, since it's common for the same value to be used for many parameter names.
for name, param in pairs(params) do
for name, param in pairs(params) do
validate_name(name, "parameter names")
validate_name(name, "parameter names")
local param_type = type(param)
if param ~= true then
if param_types then
local spec_type = type(param)
param_types[param] = param_type
if type(param) ~= "table" then
else
internal_process_error(
param_types = {[param] = param_type}
"spec for parameter %s must be a table of specs or the value true, but found %s.",
end
name, spec_type ~= "boolean" and spec_type or param
if param_type == "table" then
)
end
-- Populate required table, and make sure aliases aren't set to required.
-- Populate required table, and make sure aliases aren't set to required.
if param.required then
if param.required then
if param.alias_of then
if required == nil then
internal_process_error(
required = {}
"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
required[name] = true
end
end


-- Convert param.set from a list into a set.
local listname, alias_of = param.list, param.alias_of
-- `converted_set` prevents double-conversion if multiple parameter keys share the same param table.
if alias_of then
-- 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.
validate_name(alias_of, "the alias_of field of parameter ", name)
local set = param.set
if alias_of == name then
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(
internal_process_error(
"Parameter %s is an alias of an invalid parameter.",
"parameter %s cannot be an alias of itself.",
name
name
)
)
elseif alias == name then
end
local main_param = params[alias_of]
-- Check that the alias_of is set to a valid parameter.
if not (main_param == true or type(main_param) == "table") then
internal_process_error(
internal_process_error(
"Parameter %s cannot be an alias of itself.",
"parameter %s is an alias of an invalid parameter.",
name
name
)
)
end
end
local main_param = params[alias]
validate_alias_options(param, name, main_param, alias_of)
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.
-- 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
if listname and (main_param == true or not main_param.list) then
internal_process_error(
internal_process_error(
"The list parameter %s is set as an alias of %s, which is not a list parameter.", name, alias
"list parameter %s is set as an alias of %s, which is not a list parameter.", name, alias_of
)
-- 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
)
)
-- Can't be an alias of an alias.
elseif main_param ~= true then
local main_alias_of = main_param.alias_of
if main_alias_of ~= nil 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_of, main_alias_of, name, main_alias_of
)
end
end
end
end
end


if listname then
if listname then
if not alias then
if not alias_of then
local key = name
local key = name
if type(name) == "string" then
if type(name) == "string" then
Line 1,063: Line 1,348:
args_new[key] = list_arg
args_new[key] = list_arg
if list_args == nil then
if list_args == nil then
list_args = {[key] = list_arg}
list_args = {}
else
list_args[key] = list_arg
end
end
list_args[key] = list_arg
end
end
local list_type = type(listname)
local list_type = type(listname)
Line 1,077: Line 1,361:
elseif listname ~= true then
elseif listname ~= true then
internal_process_error(
internal_process_error(
"The list field for parameter %s must be a boolean, string or undefined, but saw a %s.",
"list field for parameter %s must be a boolean, string or undefined, but saw a %s.",
name, list_type
name, list_type
)
)
Line 1,083: Line 1,367:
if index_list ~= nil then
if index_list ~= nil then
internal_process_error(
internal_process_error(
"Only one numeric parameter can be a list, unless the list property is a string."
"only one numeric parameter can be a list, unless the list property is a string."
)
)
end
end
Line 1,092: Line 1,376:
patterns = save_pattern(name, name, patterns or {})
patterns = save_pattern(name, name, patterns or {})
end
end
if find(name, "\1", 1, true) then
if find(name, "\1", nil, true) then
if args_placeholders then
if args_placeholders then
n_ph = n_ph + 1
placeholders_n = placeholders_n + 1
args_placeholders[n_ph] = name
args_placeholders[placeholders_n] = name
else
else
args_placeholders, n_ph = {name}, 1
args_placeholders, placeholders_n = {name}, 1
end
end
end
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
end
end
Line 1,111: Line 1,390:
--Process required changes to `params`.
--Process required changes to `params`.
if args_placeholders then
if args_placeholders then
for i = 1, n_ph do
for i = 1, placeholders_n do
local name = args_placeholders[i]
local name = args_placeholders[i]
params[gsub(name, "\1", "")], params[name] = params[name], nil
params[gsub(name, "\1", "")], params[name] = params[name], nil
Line 1,121: Line 1,400:
any_args_set = true
any_args_set = true
validate_name(name, "argument names", nil, true)
validate_name(name, "argument names", nil, true)
-- Guaranteeing that all values are strings avoids issues with type coercion being inconsistent between functions.
local val_type = type(val)
if val_type ~= "string" then
internal_process_error(
"argument %s has the type %s; all arguments must be strings.",
name, val_type
)
end
local orig_name, raw_type, index, canonical = name, type(name)
local orig_name, raw_type, index, canonical = name, type(name)
Line 1,150: Line 1,437:
elseif param == true then
elseif param == true then
canonical = orig_name
canonical = orig_name
val = trim(val)
val = php_trim(val)
if val ~= "" then
if val ~= "" then
-- If the parameter is duplicated, throw an error.
-- If the parameter is duplicated, throw an error.
Line 1,162: Line 1,449:
end
end
else
else
if param.deprecated then
track("deprecated parameter", name)
end
if param.require_index then
if param.require_index then
-- Disallow require_index for numeric parameter names, as this doesn't make sense.
-- Disallow require_index for numeric parameter names, as this doesn't make sense.
if raw_type == "number" then
if raw_type == "number" then
internal_process_error(
internal_process_error(
"Cannot set require_index for numeric parameter %s.",
"cannot set require_index for numeric parameter %s.",
name
name
)
)
Line 1,181: Line 1,471:
if raw_type == "number" then
if raw_type == "number" then
internal_process_error(
internal_process_error(
"Cannot set separate_no_index for numeric parameter %s.",
"cannot set separate_no_index for numeric parameter %s.",
name
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
)
)
Line 1,195: Line 1,485:
-- 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.
-- If `separate_no_index` is set, then use 0 as the default instead.
-- If `separate_no_index` is set, then use 0 as the default instead.
if param.list then
if not index and param.list then
index = index or param.separate_no_index and 0 or 1
index = 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
if param.alias_of then
if raw_name then
raw_type = type(raw_name)
raw_type = type(raw_name)
if raw_type == "number" then
if raw_type == "number" then
name = raw_name
local main_param = params[raw_name]
local main_param = params[raw_name]
if param_types and param_types[main_param] == "table" and main_param.list then
if main_param ~= true and main_param.list then
index = index or param.separate_no_index and 0 or 1
if not index then
index = param.separate_no_index and 0 or 1
end
canonical = raw_name + index - 1
canonical = raw_name + index - 1
else
else
canonical = raw_name
canonical = raw_name
end
end
name = raw_name
else
else
name = gsub(raw_name, "\1", "")
name = gsub(raw_name, "\1", "")
local main_param = params[name]
local main_param = params[name]
if param_types and param_types[main_param] == "table" and main_param.list then
if not index and main_param ~= true and main_param.list then
index = index or param.separate_no_index and 0 or 1
index = param.separate_no_index and 0 or 1
end
end
if not index or index == 0 then
if not index or index == 0 then
Line 1,238: Line 1,530:
-- Remove leading and trailing whitespace unless no_trim is true.
-- Remove leading and trailing whitespace unless no_trim is true.
if param.no_trim then
if param.no_trim then
check_string_param(param.type, name, "no_trim")
check_string_param_modifier(param.type, name, "no_trim")
else
else
val = trim(val)
val = php_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 param.allow_empty then
if param.allow_empty then
check_string_param(param.type, name, "allow_empty")
check_string_param_modifier(param.type, name, "allow_empty")
elseif val == "" then
elseif val == "" then
-- If `disallow_missing` is set, keep track of empty parameters
-- via the `empty` field in `arg`, which will be used by the
-- `disallow_missing` check. This will be deleted before
-- returning.
if index and param.disallow_missing then
local arg = args_new[name]
local empty = arg.empty
if empty == nil then
empty = {maxindex = 0}
arg.empty = empty
end
empty[index] = true
if index > empty.maxindex then
empty.maxindex = index
end
end
val = nil
val = nil
end
end


-- Can't use "if val" alone, because val may be a boolean false.
-- Allow boolean false.
if val ~= nil then
if val ~= nil then
-- Convert to proper type if necessary.
-- Convert to proper type if necessary.
local main_param = params[raw_name]
local main_param = params[raw_name]
if not main_param or (param_types and param_types[main_param] == "table") then
if main_param ~= true then
val = convert_val(val, orig_name, main_param or param)
val = convert_val(val, orig_name, main_param or param)
end
end
Line 1,265: Line 1,573:
-- Store the argument value.
-- Store the argument value.
if index then
if index then
local arg = args_new[name]
-- If the parameter is duplicated, throw an error.
-- If the parameter is duplicated, throw an error.
if args_new[name][index] ~= nil then
if arg[index] ~= nil then
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.",
Line 1,272: Line 1,581:
)
)
end
end
args_new[name][index] = val
arg[index] = val
 
-- Store the highest index we find.
-- Store the highest index we find.
args_new[name].maxindex = max(index, args_new[name].maxindex)
local maxindex = arg.maxindex
if args_new[name][0] ~= nil then
if index > maxindex then
args_new[name].default = args_new[name][0]
maxindex = index
if args_new[name].maxindex == 0 then
end
args_new[name].maxindex = 1
if arg[0] ~= nil then
arg.default, arg[0] = arg[0], nil
if maxindex < 1 then
maxindex = 1
end
end
args_new[name][0] = nil
end
end
 
arg.maxindex = maxindex
if params[name].list then
if not 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
args_new[name] = val
-- Don't store index 0, as it's a proxy for the default.
elseif index > 0 then
arg[index] = val
end
end
else
else
Line 1,303: Line 1,609:
end
end


if not param.alias_of then
if not raw_name then
args_new[name] = val
args_new[name] = val
else
else
local main_param = params[param.alias_of]
local main_param = params[raw_name]
if param_types and param_types[main_param] == "table" and main_param.list then
if main_param ~= true and main_param.list then
args_new[param.alias_of][1] = val
local main_arg = args_new[raw_name]
main_arg[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)
if main_arg.maxindex < 1 then
main_arg.maxindex = 1
end
else
else
args_new[param.alias_of] = val
args_new[raw_name] = val
end
end
end
end
Line 1,320: Line 1,629:
end
end


-- Remove holes in any list parameters if needed.
-- Remove holes in any list parameters if needed. This must be handled
-- straight after the previous loop, as any instances of `empty` need to be
-- converted to nil.
if list_args then
if list_args then
for name, val in next, list_args do
for name, val in next, list_args do
handle_holes(params[name], val, name)
handle_holes(params, val, name)
end
end
end
end
Line 1,336: Line 1,647:
-- Handle defaults.
-- Handle defaults.
for name, param in pairs(params) do
for name, param in pairs(params) do
if param_types and param_types[param] == "table" then
if param ~= true then
local arg_new = args_new[name]
local arg_new = args_new[name]
if arg_new == nil then
if arg_new == nil then
args_new[name] = convert_default_val(name, param, pagename_set, any_args_set)
args_new[name] = convert_default_val(name, param, pagename_set, any_args_set, true)
elseif param.list and arg_new[1] == nil then
elseif param.list and arg_new[1] == nil then
local default_val = convert_default_val(name, param, pagename_set, any_args_set)
local default_val = convert_default_val(name, param, pagename_set, any_args_set)
Line 1,351: Line 1,662:
end
end
end
end
 
-- Flatten nested lists if called for. This must come after setting the default.
if list_args then
for name, val in next, list_args do
args_new[name] = maybe_flatten(params, val, name)
end
end
 
-- The required table should now be empty.
-- 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 any parameters remain, throw an error, unless we're on the current template or module's page.