48,355
edits
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 | 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 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 require = require | local require = require | ||
local sub = string.sub | local sub = string.sub | ||
local tonumber = tonumber | local tonumber = tonumber | ||
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( | is_callable = require(functions_module).is_callable | ||
return is_callable(...) | return is_callable(...) | ||
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 | 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 | local function php_trim(...) | ||
php_trim = require(scribunto_module).php_trim | |||
return | 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 | end | ||
| Line 171: | Line 192: | ||
end | end | ||
local function | local function tonumber_extended(...) | ||
tonumber_extended = require(math_module).tonumber_extended | |||
return | 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'', | ||
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 | end | ||
| Line 459: | Line 511: | ||
return msg | return msg | ||
end | end | ||
return format("%s (processed value %s)", msg, | 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 = | 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", | 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( | ||
"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 = | normalized = scribunto_parameter_key(first_name) | ||
if first_name == normalized then | if first_name == normalized then | ||
return | return | ||
end | end | ||
error(format( | error(format( | ||
"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( | ||
"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( | ||
" | "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 ` | -- 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, | local function convert_val_error(val, name, valid, seetext) | ||
if is_callable(name) then | if is_callable(name) then | ||
if type( | if type(valid) == "table" then | ||
valid = "choice, must be " .. format_choice_list(valid) | |||
end | end | ||
name(format("Invalid %s; the value %s is not valid%s", | name(format("Invalid %s; the value %s is not valid%s", valid, val, seetext and "; see " .. seetext or "")) | ||
else | else | ||
if type( | if type(valid) == "table" then | ||
valid = format_choice_list(valid) | |||
else | else | ||
valid = "a valid " .. valid | |||
end | end | ||
error(format("Parameter %s %s; the value %s is not valid.%s", dump(name), | 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", | 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 | ||
-- 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: | 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 | -- 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 | -- 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", | -- 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 | -- 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: | 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 | ||
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 | ||
local list = {} | |||
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 | ||
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: | 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: | 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"] = | ["full language"] = convert_language, | ||
["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: | error(format( | ||
'Internal error: expected `allow_hex` for type `number` to be of type "boolean" or undefined, but saw %s', | |||
dump(allow_hex) | |||
)) | |||
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: | 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: | 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: | 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: | 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: | 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 | ||
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 | -- `val` might not be a string if it's the default value. | ||
if sublist and type(val) == "string" then | |||
local retlist, set = {}, param.set | |||
if convert then | |||
local thisindex, thisval, insval, parse_err = 0 | |||
if | |||
local thisval, insval | |||
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 parse_err(msg) | ||
name(format("%s: item #%s=%s", msg_with_processed(msg, thisval, insval), thisindex, | name(format("%s: item #%s=%s", | ||
msg_with_processed(msg, thisval, insval), thisindex, thisval) | |||
) | |||
end | end | ||
else | else | ||
parse_err | 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", | ||
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 | thisindex, thisval = thisindex + 1, v | ||
if set then | |||
if | v = check_set(v, name, param, param_type) | ||
check_set(v, name, param, param_type) | |||
end | end | ||
insert(retlist, | 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 | 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 | ||
elseif param.set then | |||
val = check_set(val, name, param, param_type) | |||
end | |||
local retval = type_handlers(val, name, param, param_type, default) | |||
if 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 | else | ||
parse_err | function parse_err(msg) | ||
name(msg_with_processed(msg, val, retval)) | |||
end | end | ||
end | end | ||
else | |||
function parse_err(msg) | |||
error(format("%s: parameter %s=%s", msg_with_processed(msg, val, retval), name, val)) | |||
end | |||
end | end | ||
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 | local function check_string_param_modifier(param_type, name, tag) | ||
if param_type and param_type | 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 | "%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( | 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") | ||
end | |||
-- Iterate up the list, and throw an error if a hole is found due to a | |||
-- missing parameter, treating empty parameters as part of the list. This | |||
-- applies beyond maxindex if blank arguments are supplied beyond it, so | |||
-- isn't mutually exclusive with `disallow_holes`. | |||
local empty = val.empty | |||
if param.disallow_missing then | |||
if empty then | |||
-- 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 | ||
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. | end | ||
-- If `allow_holes` is set, there's nothing left to do. | |||
if param.allow_holes then | |||
-- do nothing | |||
-- Otherwise, remove any holes | -- Otherwise, remove any holes: `pairs` won't work, as it's unsorted, and | ||
-- | -- iterating from 1 to `maxindex` times out with inputs like |100000000000=, | ||
-- | -- 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 | ||
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 | |||
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 | ||
return val | |||
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 | local args_new, args_unknown, any_args_set, required, patterns, list_args, index_list, args_placeholders, placeholders_n = {} | ||
-- 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 | if param ~= true then | ||
local spec_type = type(param) | |||
if type(param) ~= "table" then | |||
internal_process_error( | |||
"spec for parameter %s must be a table of specs or the value true, but found %s.", | |||
name, spec_type ~= "boolean" and spec_type or param | |||
) | |||
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 | if required == nil then | ||
required = {} | |||
required = { | |||
end | end | ||
required[name] = true | |||
end | end | ||
local listname, alias_of = param.list, param.alias_of | |||
if alias_of then | |||
validate_name(alias_of, "the alias_of field of parameter ", name) | |||
if alias_of == name then | |||
local listname, | |||
if | |||
validate_name( | |||
internal_process_error( | internal_process_error( | ||
" | "parameter %s cannot be an alias of itself.", | ||
name | name | ||
) | ) | ||
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 is an alias of an invalid parameter.", | ||
name | name | ||
) | ) | ||
end | end | ||
validate_alias_options(param, name, main_param, alias_of) | |||
-- 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 | if listname and (main_param == true or not main_param.list) then | ||
internal_process_error( | internal_process_error( | ||
" | "list parameter %s is set as an alias of %s, which is not a list parameter.", name, 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 | 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 = { | list_args = {} | ||
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( | ||
" | "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." | ||
) | ) | ||
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", | if find(name, "\1", nil, true) then | ||
if args_placeholders then | if args_placeholders then | ||
placeholders_n = placeholders_n + 1 | |||
args_placeholders[ | args_placeholders[placeholders_n] = name | ||
else | else | ||
args_placeholders, | args_placeholders, placeholders_n = {name}, 1 | ||
end | end | ||
end | end | ||
end | end | ||
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, | 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 = | 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.", | ||
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.", | ||
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.", | ||
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 = 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 | 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 | if main_param ~= true and main_param.list then | ||
index = | 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 | ||
else | else | ||
name = gsub(raw_name, "\1", "") | name = gsub(raw_name, "\1", "") | ||
local main_param = params[name] | local main_param = params[name] | ||
if | if not index and main_param ~= true and main_param.list then | ||
index = | 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_modifier(param.type, name, "no_trim") | |||
else | else | ||
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_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 | ||
-- | -- 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 | 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 | 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 | ||
arg[index] = val | |||
-- Store the highest index we find. | -- Store the highest index we find. | ||
local maxindex = arg.maxindex | |||
if | if index > maxindex then | ||
maxindex = index | |||
if | end | ||
if arg[0] ~= nil then | |||
arg.default, arg[0] = arg[0], nil | |||
if maxindex < 1 then | |||
maxindex = 1 | |||
end | end | ||
end | end | ||
arg.maxindex = maxindex | |||
if params[name].list then | if not params[name].list then | ||
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 | if not raw_name then | ||
args_new[name] = val | args_new[name] = val | ||
else | else | ||
local main_param = params[ | local main_param = params[raw_name] | ||
if | if main_param ~= true and main_param.list then | ||
args_new[ | local main_arg = args_new[raw_name] | ||
main_arg[1] = val | |||
-- Store the highest index we find. | -- Store the highest index we find. | ||
if main_arg.maxindex < 1 then | |||
main_arg.maxindex = 1 | |||
end | |||
else | else | ||
args_new[ | 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 | 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 | 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. | ||