48,355
edits
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
local export = {} | local export = {} | ||
local families_module = "Module:families" | local families_module = "Module:families" | ||
local function_module = "Module:fun" | |||
local labels_module = "Module:labels" | local labels_module = "Module:labels" | ||
local languages_module = "Module:languages" | local languages_module = "Module:languages" | ||
local math_module = "Module:math" | |||
local pages_module = "Module:pages" | |||
local parse_utilities_module = "Module:parse utilities" | local parse_utilities_module = "Module:parse utilities" | ||
local references_module = "Module:references" | local references_module = "Module:references" | ||
local scripts_module = "Module:scripts" | local scripts_module = "Module:scripts" | ||
local string_utilities_module = "Module:string utilities" | |||
local table_module = "Module:table" | |||
local wikimedia_languages_module = "Module:wikimedia languages" | local wikimedia_languages_module = "Module:wikimedia languages" | ||
local yesno_module = "Module:yesno" | |||
local | local mw = mw | ||
local mw_title = mw.title | |||
local string = string | |||
local table = table | |||
local dump = mw.dumpObject | local dump = mw.dumpObject | ||
local | local find = string.find | ||
local | local format = string.format | ||
local gmatch = string.gmatch | |||
local gsub = string.gsub | local gsub = string.gsub | ||
local insert = table.insert | local insert = table.insert | ||
local | local ipairs = ipairs | ||
local list_to_text = mw.text.listToText | local list_to_text = mw.text.listToText | ||
local make_title = mw_title.makeTitle | |||
local match = string.match | local match = string.match | ||
local max = math.max | local max = math.max | ||
local new_title = mw_title.new | |||
local next = next | |||
local pairs = pairs | local pairs = pairs | ||
local | local pcall = pcall | ||
local | local rawset = rawset | ||
local | local require = require | ||
local sort = table.sort | local sort = table.sort | ||
local | local sub = string.sub | ||
local tonumber = tonumber | |||
local traceback = debug.traceback | |||
local type = type | local type = type | ||
local yesno = | |||
local current_title_text, current_namespace -- Defined when needed. | |||
local namespaces = mw.site.namespaces | |||
local function decode_entities(...) | |||
decode_entities = require(string_utilities_module).decode_entities | |||
return decode_entities(...) | |||
end | |||
local function get_family_by_code(...) | |||
get_family_by_code = require(families_module).getByCode | |||
return get_family_by_code(...) | |||
end | |||
local function get_family_by_name(...) | |||
get_family_by_name = require(families_module).getByCanonicalName | |||
return get_family_by_name(...) | |||
end | |||
local function get_language_by_code(...) | |||
get_language_by_code = require(languages_module).getByCode | |||
return get_language_by_code(...) | |||
end | |||
local function get_language_by_name(...) | |||
get_language_by_name = require(languages_module).getByCanonicalName | |||
return get_language_by_name(...) | |||
end | |||
local function get_script_by_code(...) | |||
get_script_by_code = require(scripts_module).getByCode | |||
return get_script_by_code(...) | |||
end | |||
local function get_script_by_name(...) | |||
get_script_by_name = require(scripts_module).getByCanonicalName | |||
return get_script_by_name(...) | |||
end | |||
local function get_wm_lang_by_code(...) | |||
get_wm_lang_by_code = require(wikimedia_languages_module).getByCode | |||
return get_wm_lang_by_code(...) | |||
end | |||
local function get_wm_lang_by_code_with_fallback(...) | |||
get_wm_lang_by_code_with_fallback = require(wikimedia_languages_module).getByCodeWithFallback | |||
return get_wm_lang_by_code_with_fallback(...) | |||
end | |||
local function gsplit(...) | |||
gsplit = require(string_utilities_module).gsplit | |||
return gsplit(...) | |||
end | |||
local function is_callable(...) | |||
is_callable = require(function_module).is_callable | |||
return is_callable(...) | |||
end | |||
local function is_finite_real_number(...) | |||
is_finite_real_number = require(math_module).is_finite_real_number | |||
return is_finite_real_number(...) | |||
end | |||
local function is_integer(...) | |||
is_integer = require(math_module).is_integer | |||
return is_integer(...) | |||
end | |||
local function is_internal_title(...) | |||
is_internal_title = require(pages_module).is_internal_title | |||
return is_internal_title(...) | |||
end | |||
local function is_positive_integer(...) | |||
is_positive_integer = require(math_module).is_positive_integer | |||
return is_positive_integer(...) | |||
end | |||
local function iterate_list(...) | |||
iterate_list = require(table_module).iterateList | |||
return iterate_list(...) | |||
end | |||
local function list_to_set(...) | |||
list_to_set = require(table_module).listToSet | |||
return list_to_set(...) | |||
end | |||
local function num_keys(...) | |||
num_keys = require(table_module).numKeys | |||
return num_keys(...) | |||
end | |||
local function parse_references(...) | |||
parse_references = require(references_module).parse_references | |||
return parse_references(...) | |||
end | |||
local function pattern_escape(...) | |||
pattern_escape = require(string_utilities_module).pattern_escape | |||
return pattern_escape(...) | |||
end | |||
local function scribunto_param_key(...) | |||
scribunto_param_key = require(string_utilities_module).scribunto_param_key | |||
return scribunto_param_key(...) | |||
end | |||
local function sorted_pairs(...) | |||
sorted_pairs = require(table_module).sortedPairs | |||
return sorted_pairs(...) | |||
end | |||
local function split(...) | |||
split = require(string_utilities_module).split | |||
return split(...) | |||
end | |||
local function split_labels_on_comma(...) | |||
split_labels_on_comma = require(labels_module).split_labels_on_comma | |||
return split_labels_on_comma(...) | |||
end | |||
local function split_on_comma(...) | |||
split_on_comma = require(parse_utilities_module).split_on_comma | |||
return split_on_comma(...) | |||
end | |||
local function trim(...) | |||
trim = require(string_utilities_module).trim | |||
return trim(...) | |||
end | |||
local function yesno(...) | |||
yesno = require(yesno_module) | |||
return yesno(...) | |||
end | |||
--[==[ intro: | --[==[ intro: | ||
| Line 42: | Line 191: | ||
local params = { | local params = { | ||
[1] = {required = true, type = "language", default = "und"}, | [1] = {required = true, type = "language", default = "und"}, | ||
[2] = | [2] = true, | ||
[3] = {list = true}, | [3] = {list = true}, | ||
["alt"] = | ["alt"] = true, | ||
["id"] = | ["id"] = true, | ||
["sc"] = {type = "script"}, | ["sc"] = {type = "script"}, | ||
["tr"] = | ["tr"] = true, | ||
["ts"] = | ["ts"] = true, | ||
["lit"] = | ["lit"] = true, | ||
} | } | ||
| Line 60: | Line 209: | ||
The `params` table should have the parameter names as the keys, and a (possibly empty) table of parameter tags as the | The `params` table should have the parameter names as the keys, and a (possibly empty) table of parameter tags as the | ||
value. An empty table as the value merely states that the parameter exists, but should not receive any special | value. An empty table as the value merely states that the parameter exists, but should not receive any special | ||
treatment. Possible parameter tags are listed below: | treatment; if desired, empty tables can be replaced with the value `true` as a perforamnce optimization. | ||
Possible parameter tags are listed below: | |||
; {required = true} | ; {required = true} | ||
| Line 97: | Line 248: | ||
that are aliases and required at the same time cause an error to be thrown. | that are aliases and required at the same time cause an error to be thrown. | ||
; {allow_empty = true} | ; {allow_empty = true} | ||
: If the argument is an empty string value, it is not converted to {nil}, but kept as-is. | : If the argument is an empty string value, it is not converted to {nil}, but kept as-is. The use of `allow_empty` is | ||
; { | disallowed if a type has been specified, and causes an error to be thrown. | ||
; {no_trim = true} | |||
: Spacing characters such as spaces and newlines at the beginning and end of a positional parameter are not removed. | : Spacing characters such as spaces and newlines at the beginning and end of a positional parameter are not removed. | ||
(MediaWiki itself automatically trims spaces and newlines at the edge of named parameters.) | (MediaWiki itself automatically trims spaces and newlines at the edge of named parameters.) The use of `no_trim` is | ||
disallowed if a type has been specified, and causes an error to be thrown. | |||
; {type =} | ; {type =} | ||
: Specifies what value type to convert the argument into. The default is to leave it as a text string. Alternatives are: | : Specifies what value type to convert the argument into. The default is to leave it as a text string. Alternatives are: | ||
:; {type = "boolean"} | :; {type = "boolean"} | ||
:: The value is treated as a boolean value, either true or false. No value, the empty string, and the strings {"0"}, | :: The value is treated as a boolean value, either true or false. No value, the empty string, and the strings {"0"}, | ||
{"no"}, {"n"} and {" | {"no"}, {"n"}, {"false"}, {"f"} and {"off"} are treated as {false}, all other values are considered {true}. | ||
:; {type = "number"} | :; {type = "number"} | ||
:: The value is converted into a number, | :: The value is converted into a number, and throws an error if the value is not parsable as a number. Input values may | ||
be signed (`+` or `-`), and may contain decimal points and leading zeroes. If {allow_hex = true}, then hexadecimal | |||
values in the form {"0x100"} may optionally be used instead, which otherwise have the same syntax restrictions | |||
(including signs, decimal digits, and leading zeroes after {"0x"}). Hexadecimal inputs are not case-sensitive. Lua's | |||
special number values (`inf` and `nan`) are not possible inputs. | |||
:; {type = "language"} | :; {type = "language"} | ||
:: The value is interpreted as a full or [[Wiktionary:Languages#Etymology-only languages|etymology-only language]] code | :: The value is interpreted as a full or [[Wiktionary:Languages#Etymology-only languages|etymology-only language]] code | ||
| Line 120: | Line 277: | ||
are not allowed. The additional setting {family = true} can be given to allow | are not allowed. The additional setting {family = true} can be given to allow | ||
[[Wiktionary:Language families|language family codes]] to be considered valid and the corresponding object returned. | [[Wiktionary:Language families|language family codes]] to be considered valid and the corresponding object returned. | ||
:; {type = " | :; {type = "Wikimedia language"} | ||
:: The value is interpreted as a code and converted into a | :: The value is interpreted as a code and converted into a Wikimedia language object. If the code is invalid, then an | ||
error is thrown. If { | error is thrown. If {fallback = true} is specified, conventional language codes which are different from their | ||
Wikimedia equivalent will also be accepted as a fallback. | Wikimedia equivalent will also be accepted as a fallback. | ||
:; {type = "family"} | :; {type = "family"} | ||
| Line 130: | Line 287: | ||
:: The value is interpreted as a script code (or name, if {method = "name"}) and converted into the corresponding object | :: The value is interpreted as a script code (or name, if {method = "name"}) and converted into the corresponding object | ||
(see [[Module:scripts]]). If the code or name is invalid, then an error is thrown. | (see [[Module:scripts]]). If the code or name is invalid, then an error is thrown. | ||
:; {type = "title"} | |||
:: The value is interpreted as a page title and converted into the corresponding object (see the [[mw:Extension:Scribunto/Lua_reference_manual#Title_library|Title library]]). If the page title is invalid, then an error is thrown; by default, external titles (i.e. those on other wikis) are not treated as valid. Options are: | |||
::; {namespace = n} | |||
::: The default namespace, where {n} is a namespace number; this is treated as {0} (the mainspace) if not specified. | |||
::; {allow_external = true} | |||
::: External titles are treated as valid. | |||
::; {prefix = "namespace override"} (default) | |||
::: The default namespace prefix will be prefixed to the value is already prefixed by a namespace prefix. For instance, the input {"Foo"} with namespace {10} returns {"Template:Foo"}, {"Wiktionary:Foo"} returns {"Wiktionary:Foo"}, and {"Template:Foo"} returns {"Template:Foo"}. Interwiki prefixes cannot act as overrides, however: the input {"fr:Foo"} returns {"Template:fr:Foo"}. | |||
::; {prefix = "force"} | |||
::: The default namespace prefix will be prefixed unconditionally, even if the value already appears to be prefixed. This is the way that {{tl|#invoke:}} works when calling modules from the module namespace ({828}): the input {"Foo"} returns {"Module:Foo"}, {"Wiktionary:Foo"} returns {"Module:Wiktionary:Foo"}, and {"Module:Foo"} returns {"Module:Module:Foo"}. | |||
::; {prefix = "full override"} | |||
::: The same as {prefix = "namespace override"}, except that interwiki prefixes can also act as overrides. For instance, {"el:All topics"} with namespace {14} returns {"el:Category:All topics"}. Due to the limitations of MediaWiki, only the first prefix in the value may act as an override, so the namespace cannot be overridden if the first prefix is an interwiki prefix: e.g. {"el:Template:All topics"} with namespace {14} returns {"el:Category:Template:All topics"}. | |||
:; {type = "qualifier"} | :; {type = "qualifier"} | ||
:: The value is interpreted as a qualifier and converted into the correct format for passing into `format_qualifiers()` | :: The value is interpreted as a qualifier and converted into the correct format for passing into `format_qualifiers()` | ||
in [[Module: | in [[Module:qualifier]] (which currently just means converting it to a one-item list). | ||
:; {type = "labels"} | :; {type = "labels"} | ||
:: The value is interpreted as a comma-separated list of labels and converted into the correct format for passing into | :: The value is interpreted as a comma-separated list of labels and converted into the correct format for passing into | ||
| Line 143: | Line 312: | ||
[[Module:references]], and converted into a list of objects of the form accepted by `format_references()` in the same | [[Module:references]], and converted into a list of objects of the form accepted by `format_references()` in the same | ||
module. If a syntax error is found in the reference format, an error is thrown. | module. If a syntax error is found in the reference format, an error is thrown. | ||
:; {type = function(val) ... end} | |||
:: `type` may be set to a function (or callable table), which must take the argument value as its sole argument, and must | |||
output one of the other recognized types. This is particularly useful for lists (see below), where certain values need | |||
to be interpreted differently to others. | |||
; {list =} | ; {list =} | ||
: Treat the parameter as a list of values, each having its own parameter name, rather than a single value. The | : Treat the parameter as a list of values, each having its own parameter name, rather than a single value. The | ||
| Line 166: | Line 339: | ||
: The value of the parameter is a delimiter-separated list of individual raw values. The resulting field in `args` will | : The value of the parameter is a delimiter-separated list of individual raw values. The resulting field in `args` will | ||
be a Lua list (i.e. a table with numeric indices) of the converted values. If {sublist = true} is given, the values | be a Lua list (i.e. a table with numeric indices) of the converted values. If {sublist = true} is given, the values | ||
will be split on | will be split on commas (possibly with whitespace on one or both sides of the comma, which is ignored). If | ||
value of `sublist` should be either a Lua pattern specifying the delimiter(s) to split on or a function to do the | {sublist = "comma without whitespace"} is given, the values will be split on commas which are not followed by whitespace, | ||
and which aren't preceded by an escaping backslash. Otherwise, the value of `sublist` should be either a Lua pattern | |||
specifying the delimiter(s) to split on or a function (or callable table) to do the splitting, which is passed two values | |||
(the value to split and a function to signal an error) and should return a list of the split values. | |||
; {convert =} | ; {convert =} | ||
: If given, this specifies a function to convert the raw parameter value into the Lua object used during further | : If given, this specifies a function (or callable table) to convert the raw parameter value into the Lua object used | ||
during further processing. The function is passed two arguments, the raw parameter value itself and a function used to | |||
signal an error during parsing or conversion, and should return one value, the converted parameter. The error-signaling | |||
contains the name and raw value of the parameter embedded into the message it generates, so these do not need to | function contains the name and raw value of the parameter embedded into the message it generates, so these do not need to | ||
specified in the message passed into it. If `type` is specified in conjunction with `convert`, the processing by | specified in the message passed into it. If `type` is specified in conjunction with `convert`, the processing by | ||
`type` happens first. If `sublist` is given in conjunction with `convert`, the raw parameter value will be split | `type` happens first. If `sublist` is given in conjunction with `convert`, the raw parameter value will be split | ||
appropriately and `convert` called on each resulting item. | appropriately and `convert` called on each resulting item. | ||
; {allow_hex = true} | |||
: When used in conjunction with {type = "number"}, allows hexadecimal numbers as inputs, in the format {"0x100"} (which is | |||
not case-sensitive). | |||
; {family = true} | ; {family = true} | ||
: When used in conjunction with {type = "language"}, allows [[Wiktionary:Language families|language family codes]] to be | : When used in conjunction with {type = "language"}, allows [[Wiktionary:Language families|language family codes]] to be | ||
| Line 215: | Line 391: | ||
indicate that it is in a different language). When this is used, the resulting table will contain an additional named | indicate that it is in a different language). When this is used, the resulting table will contain an additional named | ||
value, `default`, which contains the value for the indexless argument. | value, `default`, which contains the value for the indexless argument. | ||
; {demo = true} | |||
: This is used as a way to ensure that the parameter is only enabled on the template's own page (and its documentation page), and in the User: namespace; otherwise, it will be treated as an unknown parameter. This should only be used if special settings are required to showcase a template in its documentation (e.g. adjusting the pagename or disabling categorization). In most cases, it should be possible to do this without using demo parameters, but they may be required if a template/documentation page also contains real uses of the same template as well (e.g. {{tl|shortcut}}), as a way to distinguish them. | |||
]==] | ]==] | ||
---- | -- Returns true if the current page is a template or module containing the current {{#invoke}}. | ||
-- If the include_documentation argument is given, also returns true if the current page is either page's docuemntation page. | |||
local own_page, own_page_or_documentation | |||
local function is_own_page(include_documentation) | |||
if own_page == nil then | |||
if current_namespace == nil then | |||
local current_title = mw_title.getCurrentTitle() | |||
current_title_text, current_namespace = current_title.prefixedText, current_title.namespace | |||
end | |||
local frame = current_namespace == 828 and mw.getCurrentFrame() or | |||
current_namespace == 10 and mw.getCurrentFrame():getParent() | |||
if frame then | |||
local frame_title_text = frame:getTitle() | |||
own_page = current_title_text == frame_title_text | |||
own_page_or_documentation = own_page or current_title_text == frame_title_text .. "/documentation" | |||
else | |||
own_page, own_page_or_documentation = false, false | |||
end | |||
end | end | ||
return include_documentation and own_page_or_documentation or own_page | |||
end | end | ||
-------------------------------------- | -------------------------------------- Some helper functions ----------------------------- | ||
-- Convert a list in `list` to a string, separating the final element from the preceding one(s) by `conjunction`. If | -- Convert a list in `list` to a string, separating the final element from the preceding one(s) by `conjunction`. If | ||
| Line 254: | Line 425: | ||
local function concat_list(list, conjunction, dump_vals) | local function concat_list(list, conjunction, dump_vals) | ||
if dump_vals then | if dump_vals then | ||
for | for k, v in pairs(list) do | ||
list[ | list[k] = dump(v) | ||
end | end | ||
end | end | ||
| Line 263: | Line 434: | ||
-- Helper function for use with convert_val_error(). Format a list of possible choices using `concat_list` and | -- Helper function for use with convert_val_error(). Format a list of possible choices using `concat_list` and | ||
-- conjunction "or", displaying "either " before the choices if there's more than one. | -- conjunction "or", displaying "either " before the choices if there's more than one. | ||
local function format_choice_list( | local function format_choice_list(param_type) | ||
return (# | return (#param_type > 1 and "either " or "") .. concat_list(param_type, " or ") | ||
end | 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) | |||
local function | |||
if | |||
end | end | ||
return split(val, ",") | |||
end | end | ||
-- Convert a value that is not a string or number to a string using mw.dumpObject(), for debugging purposes. | -- Convert a value that is not a string or number to a string using mw.dumpObject(), for debugging purposes. | ||
local function dump_if_unusual(val) | local function dump_if_unusual(val) | ||
local val_type = type(val) | |||
return (val_type == "string" or val_type == "number") and val or dump(val) | |||
end | end | ||
| Line 299: | Line 458: | ||
if rawval == processed then | if rawval == processed then | ||
return msg | return msg | ||
end | |||
return format("%s (processed value %s)", msg, dump_if_unusual(processed)) | |||
end | |||
-------------------------------------- Error handling ----------------------------- | |||
local function process_error(fmt, ...) | |||
local args = {...} | |||
for i, val in ipairs(args) do | |||
args[i] = dump(val) | |||
end | |||
if type(fmt) == "table" then | |||
-- hacky signal that we're called from internal_process_error(), and not to omit stack frames | |||
return error(format(fmt[1], unpack(args))) | |||
end | |||
return error(format(fmt, unpack(args)), 3) | |||
end | |||
local function internal_process_error(fmt, ...) | |||
process_error({"Internal error in `params` table: " .. fmt}, ...) | |||
end | |||
-- Check that a parameter or argument is in the form form Scribunto normalizes input argument keys into (e.g. 1 not "1", "foo" not " foo "). Otherwise, it won't be possible to normalize inputs in the expected way. Unless is_argument is set, also check that the name only contains one placeholder at most, and that strings don't resolve to numeric keys once the placeholder has been substituted. | |||
local function validate_name(name, desc, extra_name, is_argument) | |||
local normalized = scribunto_param_key(name) | |||
if name and name == normalized then | |||
if is_argument or type(name) ~= "string" then | |||
return | |||
end | |||
local placeholder = find(name, "\1", 1, true) | |||
if not placeholder then | |||
return | |||
elseif find(name, "\1", placeholder + 1, true) then | |||
error(format( | |||
'Expected %s to only contain one placeholder, but saw %s', | |||
extra_name and (desc .. dump(extra_name)) or desc, dump(name) | |||
)) | |||
end | |||
local first_name = gsub(name, "\1", "1") | |||
normalized = scribunto_param_key(first_name) | |||
if first_name == normalized then | |||
return | |||
end | |||
error(format( | |||
'%s cannot resolve to numeric parameters once any placeholder has been substituted, but %s resolves to %s', | |||
extra_name and (desc .. dump(extra_name)) or desc, dump(name), dump(normalized) | |||
)) | |||
elseif normalized == nil then | |||
error(format( | |||
'Expected %s to be of type string or number, but saw %s', | |||
extra_name and (desc .. dump(extra_name)) or desc, type(name) | |||
)) | |||
end | |||
error(format( | |||
"Expected %s to be Scribunto-compatible: %s (a %s) should be %s (a %s)", | |||
extra_name and (desc .. dump(extra_name)) or desc, dump(name), type(name), dump(normalized), type(normalized) | |||
)) | |||
end | |||
-- TODO: give ranges instead of long lists, if possible. | |||
local function params_list_error(params, msg) | |||
local list, n = {}, 0 | |||
for name in sorted_pairs(params) do | |||
n = n + 1 | |||
list[n] = name | |||
end | |||
error(format( | |||
"Parameter%s %s.", | |||
format(n == 1 and " %s is" or "s %s are", concat_list(list, " and ", true)), | |||
msg | |||
), 3) | |||
end | |||
-- Signal an error for a value `val` that is not of the right type `param_type` (which is either a string specifying a type or | |||
-- a list of possible values, in the case where `set` was used). `name` is the name of the parameter and can be a | |||
-- function to signal an error (which is assumed to automatically display the parameter's name and value). `seetext` is | |||
-- an optional additional explanatory link to display (e.g. [[WT:LOL]], the list of possible languages and codes). | |||
local function convert_val_error(val, name, param_type, seetext) | |||
if is_callable(name) then | |||
if type(param_type) == "table" then | |||
param_type = "choice, must be " .. format_choice_list(param_type) | |||
end | |||
name(format("Invalid %s; the value %s is not valid%s", param_type, val, seetext and "; see " .. seetext or "")) | |||
else | else | ||
if type(param_type) == "table" then | |||
param_type = "must be " .. format_choice_list(param_type) | |||
else | |||
param_type = "should be a valid " .. param_type | |||
end | |||
error(format("Parameter %s %s; the value %s is not valid.%s", dump(name), param_type, dump(val), | |||
seetext and " See " .. seetext .. "." or "")) | |||
end | end | ||
end | end | ||
| Line 308: | Line 556: | ||
-- along with the parameter's name and value. | -- along with the parameter's name and value. | ||
local function make_parse_err(val, name) | local function make_parse_err(val, name) | ||
if | if is_callable(name) then | ||
return name | return name | ||
end | |||
return function(msg) | |||
error(format("%s: parameter %s=%s", msg, name, val)) | |||
end | end | ||
end | end | ||
-- | -------------------------------------- Value conversion ----------------------------- | ||
-- | |||
-- | -- For a list parameter `name` and corresponding value `list_name` of the `list` field (which should have the same value | ||
local function | -- as `name` if `list = true` was given), generate a pattern to match parameters of the list and store the pattern as a | ||
-- key in `patterns`, with corresponding value set to `name`. For example, if `list_name` is "tr", the pattern will | |||
-- match "tr" as well as "tr1", "tr2", ..., "tr10", "tr11", etc. If the `list_name` contains a \1 in it, the numeric | |||
-- portion goes in place of the \1. For example, if `list_name` is "f\1accel", the pattern will match "faccel", | |||
-- "f1accel", "f2accel", etc. Any \1 in `name` is removed before storing into `patterns`. | |||
local function save_pattern(name, list_name, patterns) | |||
name = type(name) == "string" and gsub(name, "\1", "") or name | |||
if find(list_name, "\1", 1, true) then | |||
patterns["^" .. gsub(pattern_escape(list_name), "\1", "([1-9]%%d*)") .. "$"] = name | |||
else | |||
patterns["^" .. pattern_escape(list_name) .. "([1-9]%d*)$"] = name | |||
list_name = list_name .. "\1" | |||
end | |||
validate_name(list_name, "the list field of parameter ", name) | |||
return patterns | |||
end | end | ||
-- A helper function for use with `sublist`. It is an iterator function for use in a for-loop that returns split | -- A helper function for use with `sublist`. It is an iterator function for use in a for-loop that returns split | ||
-- elements of `val` using `sublist` (a Lua split pattern; boolean `true` to split on commas optionally surrounded by | -- elements of `val` using `sublist` (a Lua split pattern; boolean `true` to split on commas optionally surrounded by | ||
-- whitespace; or a function to do the splitting, which is passed two values, the value to split and a function to | -- whitespace; "comma without whitespace" to split only on commas not followed by whitespace which have not been escaped | ||
-- by a backslash; or a function to do the splitting, which is passed two values, the value to split and a function to | |||
-- signal an error, and should return a list of the split elements). `name` is the parameter name or error-signaling | -- signal an error, and should return a list of the split elements). `name` is the parameter name or error-signaling | ||
-- function passed into convert_val(). | -- function passed into convert_val(). | ||
local function split_sublist(val, name, sublist) | local function split_sublist(val, name, sublist) | ||
if sublist == true then | |||
return gsplit(val, "%s*,%s*") | |||
elseif sublist == "comma without whitespace" then | |||
sublist = split_on_comma_without_whitespace | |||
elseif type(sublist) == "string" then | |||
return gsplit(val, sublist) | return gsplit(val, sublist) | ||
elseif | 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(('Internal error: Expected `sublist` to be of type "string" or "function" or boolean `true`, but saw %s' | |||
end | end | ||
return iterate_list(sublist(val, make_parse_err(val, name))) | |||
end | end | ||
-- For parameter named `name` with value `val` and param spec `param`, if the `set` field is specified, verify that the | -- For parameter named `name` with value `val` and param spec `param`, if the `set` field is specified, verify that the | ||
-- value is one of the one specified in `set`, and throw an error otherwise. `name` is taken directly from the | -- value is one of the one specified in `set`, and throw an error otherwise. `name` is taken directly from the | ||
-- corresponding parameter passed into convert_val() and may be a function to signal an error. Optional ` | -- corresponding parameter passed into convert_val() and may be a function to signal an error. Optional `param_type` is a | ||
-- string specifying the conversion type of `val` and is used for special-casing: If ` | -- string specifying the conversion type of `val` and is used for special-casing: If `param_type` is "boolean", an internal | ||
-- error is thrown (since `set` cannot be used in conjunction with booleans) and if ` | -- error is thrown (since `set` cannot be used in conjunction with booleans) and if `param_type` is "number", no checking | ||
-- happens because in this case `set` contains numbers and is checked inside the number conversion function itself, | -- happens because in this case `set` contains numbers and is checked inside the number conversion function itself, | ||
-- after converting `val` to a number. | -- after converting `val` to a number. | ||
local function check_set(val, name, param, | local function check_set(val, name, param, param_type) | ||
if | if param_type == "boolean" then | ||
error(('Internal error: Cannot use `set` with `type = "%s"`' | error(format('Internal error: Cannot use `set` with `type = "%s"`', param_type)) | ||
elseif param_type == "number" then | |||
-- Needs to be special cased because the check happens after conversion to numbers. | -- Needs to be special cased because the check happens after conversion to numbers. | ||
return | return | ||
| Line 378: | Line 632: | ||
local function convert_language(val, name, param, allow_etym) | local function convert_language(val, name, param, allow_etym) | ||
local | local method, func = param.method | ||
if method == nil or method == "code" then | |||
func, method = get_language_by_code, "code" | |||
elseif method == "name" then | |||
func, method = get_language_by_name, "name" | |||
else | |||
error(format('Internal error: Expected `method` for type `language` to be "code", "name" or undefined, but saw %s', dump(method))) | |||
end | |||
local lang = func(val, nil, allow_etym, param.family) | |||
if lang then | if lang then | ||
return lang | return lang | ||
end | end | ||
local list = {"language"} | local list, links = {"language"}, {"[[WT:LOL]]"} | ||
if allow_etym then | if allow_etym then | ||
insert(list, "etymology language") | insert(list, "etymology language") | ||
| Line 392: | Line 653: | ||
insert(links, "[[WT:LOF]]") | insert(links, "[[WT:LOF]]") | ||
end | end | ||
convert_val_error(val, name, concat_list(list, " or ") .. " " .. ( | convert_val_error(val, name, concat_list(list, " or ") .. " " .. (method == "name" and "name" or "code"), concat_list(links, " and ")) | ||
end | end | ||
-- | -- TODO: validate parameter specs separately, as it's making the handler code really messy at the moment. | ||
local type_handlers = setmetatable({ | |||
local | |||
["boolean"] = function(val) | ["boolean"] = function(val) | ||
return yesno(val, true) | return yesno(val, true) | ||
end, | end, | ||
["family"] = function(val, name, param) | ["family"] = function(val, name, param) | ||
local method, func = param.method | |||
if method == nil or method == "code" then | |||
func, method = get_family_by_code, "code" | |||
elseif method == "name" then | |||
func, method = get_family_by_name, "name" | |||
else | |||
error(format('Internal error: Expected `method` for type `family` to be "code", "name" or undefined, but saw %s', dump(method))) | |||
end | |||
return func(val) or convert_val_error(val, name, "family " .. method, "[[WT:LOF]]") | |||
end, | end, | ||
["labels"] = function(val, name, param) | ["labels"] = function(val, name, param) | ||
-- FIXME: Should be able to pass in a parse_err function. | -- FIXME: Should be able to pass in a parse_err function. | ||
return | return split_labels_on_comma(val) | ||
end, | end, | ||
| Line 430: | Line 682: | ||
return convert_language(val, name, param, true) | return convert_language(val, name, param, true) | ||
end, | end, | ||
["full language"] = function(val, name, param) | ["full language"] = function(val, name, param) | ||
return convert_language(val, name, param | return convert_language(val, name, param) | ||
end, | end, | ||
["number"] = function(val, name, param) | ["number"] = function(val, name, param) | ||
if type(val) | local allow_hex = param.allow_hex | ||
if allow_hex and allow_hex ~= true then | |||
error(format('Internal error: Expected `allow_hex` for type `number` to be of type "boolean" or undefined, but saw %s', dump(allow_hex))) | |||
end | |||
local num = tonumber(val) | |||
-- Avoid converting inputs like "nan" or "inf", and disallow 0x hex inputs unless explicitly enabled | |||
-- with `allow_hex`. | |||
if not (num and is_finite_real_number(num) and (allow_hex or not match(val, "^[+-]?0[Xx]%x*%.?%x*$"))) then | |||
convert_val_error(val, name, (allow_hex and "decimal or hexadecimal " or "") .. "number") | |||
end | end | ||
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( | check_set(num, name, param) | ||
end | end | ||
return val | return num | ||
end, | |||
["qualifier"] = function(val, name, param) | |||
return {val} | |||
end, | end, | ||
["references"] = function(val, name, param) | |||
return parse_references(val, make_parse_err(val, name)) | |||
end, | |||
["script"] = function(val, name, param) | ["script"] = function(val, name, param) | ||
local method, func = param.method | |||
if method == nil or method == "code" then | |||
func, method = get_script_by_code, "code" | |||
elseif method == "name" then | |||
func, method = get_script_by_name, "name" | |||
else | |||
error(format('Internal error: Expected `method` for type `script` to be "code", "name" or undefined, but saw %s', dump(method))) | |||
end | |||
return func(val) or convert_val_error(val, name, "script " .. method, "[[WT:LOS]]") | |||
end, | end, | ||
["string"] = function(val, name, param) | ["string"] = function(val, name, param) -- To be removed as unnecessary. | ||
return val | return val | ||
end, | end, | ||
[" | -- TODO: add support for resolving to unsupported titles. | ||
local | -- TODO: split this into "page name" (i.e. internal) and "link target" (i.e. external as well), which is more intuitive. | ||
local | ["title"] = function(val, name, param) | ||
local namespace = param.namespace | |||
return | if namespace == nil then | ||
namespace = 0 | |||
else | |||
local valid_type = type(namespace) ~= "number" and 'of type "number" or undefined' or | |||
not namespaces[namespace] and "a valid namespace number" or | |||
nil | |||
if valid_type then | |||
error(format('Internal error: Expected `namespace` for type `title` to be %s, but saw %s', valid_type, dump(namespace))) | |||
end | |||
end | |||
-- Decode entities. WARNING: mw.title.makeTitle must be called with `decoded` (as it doesn't decode) and mw.title.new must be called with `val` (as it does decode, so double-decoding needs to be avoided). | |||
local decoded, prefix, title = decode_entities(val), param.prefix | |||
-- If the input is a fragment, treat the title as the current title with the input fragment. | |||
if sub(decoded, 1, 1) == "#" then | |||
-- If prefix is "force", only get the current title if it's in the specified namespace. current_title includes the namespace prefix. | |||
if current_namespace == nil then | |||
local current_title = mw_title.getCurrentTitle() | |||
current_title_text, current_namespace = current_title.prefixedText, current_title.namespace | |||
end | |||
if not (prefix == "force" and namespace ~= current_namespace) then | |||
title = new_title(current_title_text .. val) | |||
end | |||
elseif prefix == "force" then | |||
-- Unconditionally add the namespace prefix (mw.title.makeTitle). | |||
title = make_title(namespace, decoded) | |||
elseif prefix == "full override" then | |||
-- The first input prefix will be used as an override (mw.title.new). This can be a namespace or interwiki prefix. | |||
title = new_title(val, namespace) | |||
elseif prefix == nil or prefix == "namespace override" then | |||
-- Only allow namespace prefixes to override. Interwiki prefixes therefore need to be treated as plaintext (e.g. "el:All topics" with namespace 14 returns "el:Category:All topics", but we want "Category:el:All topics" instead; if the former is really needed, then the input ":el:Category:All topics" will work, as the initial colon overrides the namespace). mw.title.new can take namespace names as well as numbers in the second argument, and will throw an error if the input isn't a valid namespace, so this can be used to determine if a prefix is for a namespace, since mw.title.new will return successfully only if there's either no prefix or the prefix is for a valid namespace (in which case we want the override). | |||
local success | |||
success, title = pcall(new_title, val, match(decoded, ".-%f[:]") or namespace) | |||
-- Otherwise, get the title with mw.title.makeTitle, which unconditionally adds the namespace prefix, but behaves like mw.title.new if the namespace is 0. | |||
if not success then | |||
title = make_title(namespace, decoded) | |||
end | |||
else | |||
error(format('Internal error: Expected `prefix` for type `title` to be "force", "full override", "namespace override" or undefined, but saw %s', dump(prefix))) | |||
end | |||
local allow_external = param.allow_external | |||
if allow_external == true then | |||
return title or convert_val_error(val, name, "Wiktionary or external page title") | |||
elseif not allow_external then | |||
return title and is_internal_title(title) and title or convert_val_error(val, name, "Wiktionary page title") | |||
end | end | ||
local | error(format('Internal error: Expected `allow_external` for type `title` to be of type "boolean" or undefined, but saw %s', dump(allow_external))) | ||
end, | |||
["Wikimedia language"] = function(val, name, param) | |||
local fallback = param.fallback | |||
if fallback == true then | |||
return get_wm_lang_by_code_with_fallback(val) or convert_val_error(val, name, "Wikimedia language or language code") | |||
elseif not fallback then | |||
return get_wm_lang_by_code(val) or convert_val_error(val, name, "Wikimedia language code") | |||
end | end | ||
error(format('Internal error: Expected `fallback` for type `Wikimedia language` to be of type "boolean" or undefined, but saw %s', dump(fallback))) | |||
end, | end, | ||
}, { | }, { | ||
__call = function(self, val, name, param) | -- TODO: decode HTML entities in all input values. Non-trivial to implement, because we need to avoid any downstream functions decoding the output from this module, which would be double-decoding. Note that "title" has this implemented already, and it needs to have both the raw input and the decoded input to avoid double-decoding by me.title.new, so any implementation can't be as simple as decoding in __call then passing the result to the handler. | ||
local | __call = function(self, val, name, param, param_type) | ||
local func | local val_type = type(val) | ||
if | -- TODO: check this for all possible parameter types. | ||
error("Internal error: " .. | if val_type == param_type then | ||
return val | |||
-- TODO: throw an internal error. | |||
end | |||
local func = self[param_type] | |||
if func == nil then | |||
error(format("Internal error: %s is not a recognized parameter type.", dump(param_type))) | |||
end | |||
return func(val, name, param) | |||
end | |||
}) | |||
--[==[ func: export.convert_val(val, name, param) | |||
Convert a parameter value according to the associated specs listed in the `params` table passed to | |||
[[Module:parameters]]. `val` is the value to convert for a parameter whose name is `name` (used only in error messages). | |||
`param` is the spec (the value part of the `params` table for the parameter). In place of passing in the parameter name, | |||
`name` can be a function that throws an error, displaying the specified message along with the parameter name and value. | |||
This function processes all the conversion-related fields in `param`, including `type`, `set`, `sublist`, `convert`, | |||
etc. It returns the converted value. | |||
]==] | |||
local function convert_val(val, name, param) | |||
local param_type = param.type or "string" | |||
-- If param.type is a function, resolve it to a recognized type. | |||
if is_callable(param_type) then | |||
param_type = param_type(val) | |||
end | |||
local sublist = param.sublist | |||
if sublist then | |||
local retlist = {} | |||
if type(val) ~= "string" then | |||
error(format("Internal error: %s is not a string.", dump(val))) | |||
end | |||
if param.convert then | |||
local thisval, insval | |||
local thisindex = 0 | |||
local parse_err | |||
if is_callable(name) then | |||
-- We assume the passed-in error function in `name` already shows the parameter name and raw value. | |||
parse_err = function(msg) | |||
name(format("%s: item #%s=%s", msg_with_processed(msg, thisval, insval), thisindex, | |||
thisval)) | |||
end | end | ||
else | |||
parse_err = function(msg) | |||
error(format("%s: item #%s=%s of parameter %s=%s", msg_with_processed(msg, thisval, insval), | |||
thisindex, thisval, name, val)) | |||
end | end | ||
end | |||
for v in split_sublist(val, name, sublist) do | |||
thisval = v | |||
thisindex = thisindex + 1 | |||
if param.set then | |||
check_set(v, name, param, param_type) | |||
end | end | ||
insert(retlist, param.convert(type_handlers(v, name, param, param_type), parse_err)) | |||
end | end | ||
else | else | ||
if param.set then | for v in split_sublist(val, name, sublist) do | ||
if param.set then | |||
check_set(v, name, param, param_type) | |||
end | |||
insert(retlist, type_handlers(v, name, param, param_type)) | |||
end | end | ||
local retval = | end | ||
return retlist | |||
else | |||
if param.set then | |||
check_set(val, name, param, param_type) | |||
end | |||
local retval = type_handlers(val, name, param, param_type) | |||
if param.convert then | |||
local parse_err | |||
if is_callable(name) then | |||
-- We assume the passed-in error function in `name` already shows the parameter name and raw value. | |||
if retval == val then | |||
-- This is an optimization to avoid creating a closure. The second arm works correctly even | |||
-- when retval == val. | |||
parse_err = name | |||
else | else | ||
parse_err = function(msg) | parse_err = function(msg) | ||
name(msg_with_processed(msg, val, retval)) | |||
end | end | ||
end | end | ||
else | |||
parse_err = function(msg) | |||
error(format("%s: parameter %s=%s", msg_with_processed(msg, val, retval), name, val)) | |||
end | |||
end | end | ||
retval = param.convert(retval, parse_err) | |||
end | end | ||
return retval | |||
end | end | ||
end | |||
export.convert_val = convert_val -- used by [[Module:parameter utilities]] | export.convert_val = convert_val -- used by [[Module:parameter utilities]] | ||
local function | local function unknown_param(name, val, args_unknown) | ||
args_unknown[name] = val | |||
return args_unknown | |||
end | |||
local function check_string_param(param_type, name, tag) | |||
if param_type and param_type ~= "string" then | |||
internal_process_error( | |||
"%s cannot be set unless the parameter has the type %s (the default): parameter %s has the type %s.", | |||
tag, "string", name, param_type | |||
) | |||
end | end | ||
if type( | end | ||
return | local function handle_holes(param, val, name) | ||
-- Iterate up the list, and throw an error if a hole is found. | |||
if param.disallow_holes then | |||
for i = 1, val.maxindex do | |||
if val[i] == nil then | |||
local listname = param.list | |||
if type(listname) == "string" then | |||
listname = dump(listname) | |||
elseif type(name) == "number" then | |||
i = i + name - 1 -- Absolute index. | |||
listname = "numeric" | |||
else | |||
listname = dump(name) | |||
end | |||
process_error( | |||
"Item %d in the list of %s parameters cannot be empty, because the list must be contiguous.", | |||
i, listname | |||
) | |||
end | |||
end | |||
-- If `allow_holes` is set, there's nothing to do. This is placed after | |||
-- `disallow_holes`, so that the latter takes priority. | |||
elseif param.allow_holes then | |||
return | |||
-- Otherwise, remove any holes. Use num_keys to get a list of numerical keys | |||
-- instead of iterating from 1 to `maxindex`, as it could be enormous if | |||
-- there is a huge hole in the list. | |||
else | else | ||
local keys, i = num_keys(val), 0 | |||
while true do | |||
i = i + 1 | |||
local key = keys[i] | |||
if key == nil then | |||
break | |||
elseif i ~= key then | |||
val[i], val[key] = val[key], nil | |||
end | |||
end | |||
end | end | ||
-- Some code depends on only numeric params being present when no holes are | |||
-- allowed (e.g. by checking for the presence of arguments using next()), so | |||
-- remove `maxindex`. | |||
val.maxindex = nil | |||
end | end | ||
local function | -- If both `template_default` and `default` are given, `template_default` takes precedence, but only on the template or | ||
-- module page. This means a different default can be specified for the template or module page example. However, | |||
-- `template_default` doesn't apply if any args are set, which helps (somewhat) with examples on documentation pages | |||
-- transcluded into the template page. HACK: We still run into problems on documentation pages transcluded into the | |||
-- template page when pagename= is set. Check this on the assumption that pagename= is fairly standard. | |||
local function convert_default_val(name, param, pagename_set, any_args_set) | |||
if not pagename_set then | |||
local val = param.template_default | |||
if val ~= nil and not any_args_set and is_own_page() then | |||
return convert_val(val, name, param) | |||
end | |||
end | |||
local val = param.default | |||
if val ~= nil then | |||
return convert_val(val, name, param) | |||
end | |||
end | end | ||
| Line 584: | Line 988: | ||
function export.process(args, params, return_unknown) | function export.process(args, params, return_unknown) | ||
-- Process parameters for specific properties | -- Process parameters for specific properties | ||
local args_new | local args_new, args_unknown, any_args_set, param_types, required, patterns, list_args, index_list, | ||
args_placeholders, n_ph = {} | |||
for name, param in pairs(params) do | for name, param in pairs(params) do | ||
validate_name(name, "parameter names") | |||
local param_type = type(param) | |||
if param_types then | |||
param_types[param] = param_type | |||
else | |||
param_types = {[param] = param_type} | |||
end | end | ||
if param_type == "table" then | |||
-- Populate required table, and make sure aliases aren't set to required. | |||
if param.required then | |||
if param.alias_of then | |||
if | internal_process_error( | ||
"Parameter %s is an alias of %s, but is also set as a required parameter. Only %s should be set as required.", | |||
name, param.alias_of, name | |||
) | |||
elseif required then | |||
required[name] = true | |||
else | |||
-- | required = {[name] = true} | ||
if | end | ||
internal_process_error("Parameter %s is an alias of | |||
end | end | ||
-- | |||
-- Convert param.set from a list into a set. | |||
-- `converted_set` prevents double-conversion if multiple parameter keys share the same param table. | |||
-- rawset avoids errors if param has been loaded via mw.loadData; however, it's probably more efficient to preconvert them, and set the `converted_set` key in advance. | |||
local set = param.set | |||
if set and not param.converted_set then | |||
-- | rawset(param, "set", list_to_set(set)) | ||
rawset(param, "converted_set", true) | |||
end | end | ||
local listname, alias = param.list, param.alias_of | |||
if alias then | |||
validate_name(alias, "the alias_of field of parameter ", name) | |||
-- Check that the alias_of is set to a valid parameter. | |||
if not params[alias] then | |||
internal_process_error( | |||
"Parameter %s is an alias of an invalid parameter.", | |||
name | |||
) | |||
elseif alias == name then | |||
internal_process_error( | |||
"Parameter %s cannot be an alias of itself.", | |||
name | |||
) | |||
end | |||
local main_param = params[alias] | |||
local main_type = param_types[main_param] or type(main_param) -- Might not yet be memoized. | |||
-- Aliases can't be lists unless the canonical parameter is also a list. | |||
if listname and not (main_type == "table" and main_param.list) then | |||
internal_process_error( | |||
"The list parameter %s is set as an alias of %s, which is not a list parameter.", name, alias | |||
) | |||
-- Aliases can't be aliases of other aliases. | |||
elseif main_type == "table" and main_param.alias_of then | |||
internal_process_error( | |||
"alias_of cannot be set to another alias: parameter %s is set as an alias of %s, which is in turn an alias of %s. Set alias_of for %s to %s.", | |||
name, alias, params[alias].alias_of, name, params[alias].alias_of | |||
) | |||
end | end | ||
end | end | ||
if listname then | |||
-- If the list property is a string, then it represents the name | if not alias then | ||
local key = name | |||
if type(name) == "string" then | |||
key = gsub(name, "\1", "") | |||
end | |||
local list_arg = {maxindex = 0} | |||
args_new[key] = list_arg | |||
if list_args == nil then | |||
list_args = {[key] = list_arg} | |||
else | |||
list_args[key] = list_arg | |||
end | |||
end | |||
local list_type = type(listname) | |||
if list_type == "string" then | |||
-- If the list property is a string, then it represents the name | |||
-- to be used as the prefix for list items. This is for use with lists | |||
-- where the first item is a numbered parameter and the | |||
-- subsequent ones are named, such as 1, pl2, pl3. | |||
patterns = save_pattern(name, listname, patterns or {}) | |||
elseif listname ~= true then | |||
internal_process_error( | internal_process_error( | ||
"Only one numeric parameter can be a list, unless the list property is a string.") | "The list field for parameter %s must be a boolean, string or undefined, but saw a %s.", | ||
name, list_type | |||
) | |||
elseif type(name) == "number" then | |||
if index_list ~= nil then | |||
internal_process_error( | |||
"Only one numeric parameter can be a list, unless the list property is a string." | |||
) | |||
end | |||
-- If the name is a number, then all indexed parameters from | |||
-- this number onwards go in the list. | |||
index_list = name | |||
else | |||
patterns = save_pattern(name, name, patterns or {}) | |||
end | |||
if find(name, "\1", 1, true) then | |||
if args_placeholders then | |||
n_ph = n_ph + 1 | |||
args_placeholders[n_ph] = name | |||
else | |||
args_placeholders, n_ph = {name}, 1 | |||
end | |||
end | end | ||
end | end | ||
elseif param ~= true then | |||
internal_process_error( | |||
"Spec for parameter %s must be a table of specs or the value true, but found %s.", | |||
name, param_type ~= "boolean" and param_type or param | |||
) | |||
end | end | ||
end | end | ||
--Process required changes to `params`. | --Process required changes to `params`. | ||
for i = 1, | if args_placeholders then | ||
for i = 1, n_ph do | |||
local name = args_placeholders[i] | |||
params[gsub(name, "\1", "")], params[name] = params[name], nil | |||
end | |||
end | end | ||
-- Process the arguments | -- Process the arguments | ||
for name, val in pairs(args) do | for name, val in pairs(args) do | ||
any_args_set = true | |||
validate_name(name, "argument names", nil, true) | |||
local orig_name, raw_type, index, canonical = name, type(name) | |||
if raw_type == "number" then | if raw_type == "number" then | ||
if | if index_list and name >= index_list then | ||
index = name - | index = name - index_list + 1 | ||
name = | name = index_list | ||
end | end | ||
elseif patterns then | |||
-- Does this argument name match a pattern? | -- Does this argument name match a pattern? | ||
for pattern, pname in | for pattern, pname in next, patterns do | ||
index = match(name, pattern) | index = match(name, pattern) | ||
-- It matches, so store the parameter name and the | -- It matches, so store the parameter name and the | ||
| Line 708: | Line 1,142: | ||
end | end | ||
end | end | ||
local param = params[name] | local param = params[name] | ||
-- If the argument is not in the list of parameters, store it in a separate list. | |||
-- If the argument is not in the list of parameters, | |||
if not param then | if not param then | ||
if | args_unknown = unknown_param(name, val, args_unknown or {}) | ||
elseif param == true then | |||
canonical = orig_name | |||
val = trim(val) | |||
if val ~= "" then | |||
-- If the parameter is duplicated, throw an error. | |||
if args_new[name] ~= nil then | |||
process_error( | |||
"Parameter %s has been entered more than once. This is probably because a parameter alias has been used.", | |||
canonical | |||
) | |||
end | |||
args_new[name] = val | |||
end | end | ||
else | else | ||
if param.require_index then | |||
-- Disallow require_index for numeric parameter names, as this doesn't make sense. | |||
if raw_type == "number" then | |||
internal_process_error( | |||
"Cannot set require_index for numeric parameter %s.", | |||
name | |||
) | |||
-- If a parameter without the trailing index was found, and | |||
-- require_index is set on the param, treat it | |||
-- as if it isn't recognized. | |||
elseif not index then | |||
args_unknown = unknown_param(name, val, args_unknown or {}) | |||
end | |||
end | |||
-- Check that separate_no_index is not being used with a numeric parameter. | -- Check that separate_no_index is not being used with a numeric parameter. | ||
if param.separate_no_index then | if param.separate_no_index then | ||
if raw_type == "number" then | if raw_type == "number" then | ||
internal_process_error("Cannot set separate_no_index for numeric parameter %s.", name) | internal_process_error( | ||
"Cannot set separate_no_index for numeric parameter %s.", | |||
name | |||
) | |||
elseif type(param.alias_of) == "number" then | elseif type(param.alias_of) == "number" then | ||
internal_process_error( | internal_process_error( | ||
"Cannot set separate_no_index for parameter %s, as it is an alias of numeric parameter %s.", | "Cannot set separate_no_index for parameter %s, as it is an alias of numeric parameter %s.", | ||
name, param.alias_of) | name, param.alias_of | ||
) | |||
end | end | ||
end | end | ||
-- If no index was found, use 1 as the default index. | -- If no index was found, use 1 as the default index. | ||
-- This makes list parameters like g, g2, g3 put g at index 1. | -- This makes list parameters like g, g2, g3 put g at index 1. | ||
| Line 749: | Line 1,198: | ||
index = index or param.separate_no_index and 0 or 1 | index = index or param.separate_no_index and 0 or 1 | ||
end | end | ||
-- Normalize to the canonical parameter name. If it's a list, but the alias is not, then determine the index. | -- Normalize to the canonical parameter name. If it's a list, but the alias is not, then determine the index. | ||
local raw_name = param.alias_of | local raw_name = param.alias_of | ||
| Line 755: | Line 1,204: | ||
raw_type = type(raw_name) | raw_type = type(raw_name) | ||
if raw_type == "number" then | if raw_type == "number" then | ||
local main_param = params[raw_name] | |||
if param_types and param_types[main_param] == "table" and main_param.list then | |||
index = index or param.separate_no_index and 0 or 1 | index = index or param.separate_no_index and 0 or 1 | ||
canonical = raw_name + index - 1 | |||
else | else | ||
canonical = raw_name | |||
end | end | ||
name = raw_name | name = raw_name | ||
else | else | ||
name = gsub(raw_name, "\1", "") | name = gsub(raw_name, "\1", "") | ||
local main_param = params[name] | |||
if param_types and param_types[main_param] == "table" and main_param.list then | |||
index = index or param.separate_no_index and 0 or 1 | index = index or param.separate_no_index and 0 or 1 | ||
end | end | ||
if not index or index == 0 then | if not index or index == 0 then | ||
canonical = name | |||
elseif name == raw_name then | elseif name == raw_name then | ||
canonical = name .. index | |||
else | else | ||
canonical = gsub(raw_name, "\1", index) | |||
end | end | ||
end | end | ||
else | else | ||
canonical = orig_name | |||
end | end | ||
-- Remove leading and trailing whitespace unless | -- Only recognize demo parameters if this is the current template or module's | ||
if | -- page, or its documentation page. | ||
if param.demo and not is_own_page("include_documentation") then | |||
args_unknown = unknown_param(name, val, args_unknown or {}) | |||
end | |||
-- Remove leading and trailing whitespace unless no_trim is true. | |||
if param.no_trim then | |||
check_string_param(param.type, name, "no_trim") | |||
else | |||
val = trim(val) | val = trim(val) | ||
end | end | ||
-- Empty string is equivalent to nil unless allow_empty is true. | -- Empty string is equivalent to nil unless allow_empty is true. | ||
if val == "" | if param.allow_empty then | ||
check_string_param(param.type, name, "allow_empty") | |||
elseif val == "" then | |||
val = nil | val = nil | ||
end | end | ||
-- Can't use "if val" alone, because val may be a boolean false. | -- Can't use "if val" alone, because val may be a boolean false. | ||
if val ~= nil then | if val ~= nil then | ||
-- Convert to proper type if necessary. | -- Convert to proper type if necessary. | ||
val = convert_val(val, orig_name, | local main_param = params[raw_name] | ||
if not main_param or (param_types and param_types[main_param] == "table") then | |||
val = convert_val(val, orig_name, main_param or param) | |||
end | |||
-- Mark it as no longer required, as it is present. | -- Mark it as no longer required, as it is present. | ||
required[name] = nil | if required then | ||
required[name] = nil | |||
end | |||
-- Store the argument value. | -- Store the argument value. | ||
if index then | if index then | ||
| Line 822: | Line 1,269: | ||
process_error( | process_error( | ||
"Parameter %s has been entered more than once. This is probably because a list parameter has been entered without an index and with index 1 at the same time, or because a parameter alias has been used.", | "Parameter %s has been entered more than once. This is probably because a list parameter has been entered without an index and with index 1 at the same time, or because a parameter alias has been used.", | ||
canonical | |||
) | |||
end | end | ||
args_new[name][index] = val | args_new[name][index] = val | ||
-- Store the highest index we find. | -- Store the highest index we find. | ||
args_new[name].maxindex = max(index, args_new[name].maxindex) | args_new[name].maxindex = max(index, args_new[name].maxindex) | ||
| Line 834: | Line 1,282: | ||
end | end | ||
args_new[name][0] = nil | args_new[name][0] = nil | ||
end | end | ||
if params[name].list then | if params[name].list then | ||
-- Don't store index 0, as it's a proxy for the default. | -- Don't store index 0, as it's a proxy for the default. | ||
| Line 852: | Line 1,299: | ||
process_error( | process_error( | ||
"Parameter %s has been entered more than once. This is probably because a parameter alias has been used.", | "Parameter %s has been entered more than once. This is probably because a parameter alias has been used.", | ||
canonical | |||
) | |||
end | end | ||
if not param.alias_of then | if not param.alias_of then | ||
args_new[name] = val | args_new[name] = val | ||
else | else | ||
local main_param = params[param.alias_of] | |||
if param_types and param_types[main_param] == "table" and main_param.list then | |||
args_new[param.alias_of][1] = val | args_new[param.alias_of][1] = val | ||
-- Store the highest index we find. | -- Store the highest index we find. | ||
args_new[param.alias_of].maxindex = max(1, args_new[param.alias_of].maxindex) | args_new[param.alias_of].maxindex = max(1, args_new[param.alias_of].maxindex) | ||
| Line 871: | Line 1,319: | ||
end | end | ||
end | end | ||
-- Remove holes in any list parameters if needed. | -- Remove holes in any list parameters if needed. | ||
for name, val in | if list_args then | ||
for name, val in next, list_args do | |||
handle_holes(params[name], val, name) | |||
end | end | ||
end | end | ||
-- | -- If the current page is the template which invoked this Lua instance, then ignore the `require` flag, as it | ||
-- `template_default` key | -- means we're viewing the template directly. Required parameters sometimes have a `template_default` key set, | ||
-- | -- which gets used in such cases as a demo. | ||
-- Note: this won't work on other pages in the Template: namespace (including the /documentation subpage), | |||
local | -- or if the #invoke: is on a page in another namespace. | ||
local pagename_set = args_new.pagename | |||
-- Handle defaults. | -- Handle defaults. | ||
for name, param in pairs(params) do | for name, param in pairs(params) do | ||
if param_types and param_types[param] == "table" then | |||
local arg_new = args_new[name] | local arg_new = args_new[name] | ||
if | if arg_new == nil then | ||
args_new[name] = convert_default_val(name, param, pagename_set, any_args_set) | |||
arg_new[1] = | elseif param.list and arg_new[1] == nil then | ||
local default_val = convert_default_val(name, param, pagename_set, any_args_set) | |||
if default_val ~= nil then | |||
arg_new[1] = default_val | |||
if arg_new.maxindex == 0 then | |||
arg_new.maxindex = 1 | |||
end | |||
end | end | ||
end | end | ||
end | end | ||
| Line 945: | Line 1,353: | ||
-- The required table should now be empty. | -- The required table should now be empty. | ||
-- If any | -- If any parameters remain, throw an error, unless we're on the current template or module's page. | ||
if | if required and next(required) ~= nil and not is_own_page() then | ||
params_list_error(required, "required") | |||
-- Return the arguments table. | |||
-- If there are any unknown parameters, throw an error, unless return_unknown is set, in which case return args_unknown as a second return value. | |||
elseif return_unknown then | |||
return args_new, args_unknown or {} | |||
elseif args_unknown and next(args_unknown) ~= nil then | |||
params_list_error(args_unknown, "not used by this template") | |||
-- | |||
return args_new, args_unknown | |||
end | end | ||
return args_new | |||
end | end | ||
return export | return export | ||