local require = require
local require_when_needed = require("Module:require when needed")
local m_str_utils = require("Module:string utilities")
local concat = table.concat
local find_parameters = require("Module:template parser").find_parameters
local format_categories = require_when_needed("Module:utilities", "format_categories")
local get_current_title = mw.title.getCurrentTitle
local gsplit = m_str_utils.gsplit
local html_create = mw.html.create
local match = string.match
local new_title = mw.title.new
local next = next
local pairs = pairs
local process_params = require("Module:parameters").process
local scribunto_param_key = m_str_utils.scribunto_param_key
local select = select
local sort = table.sort
local tostring = tostring
local type = type
local uses_hidden_category = require_when_needed("Module:maintenance category", "uses_hidden_category")
local export = {}
-- Returns a table of all arguments in `template_args` which are not supported
-- by `template_title` or listed in `additional`.
local function get_invalid_args(template_title, template_args, additional)
local content = template_title:getContent()
if not content then
-- This should only be possible if the input frame has been tampered with.
error("Could not retrieve the page content of \"" .. template_title.prefixedText .. "\".")
end
local allowed_params, seen = {}, {}
-- Detect all params used by the parent template. param:get_name() takes the
-- parent frame arg table as an argument so that preprocessing will take
-- them into account, since it will matter if the name contains another
-- parameter (e.g. the outer param in "{{{foo{{{bar}}}baz}}}" will change
-- depending on the value for bar=). `seen` memoizes results based on the
-- raw parameter text (which is stored as a string in the parameter object),
-- which avoids unnecessary param:get_name() calls, which are non-trivial.
for param in find_parameters(content) do
local raw = param.raw
if not seen[raw] then
allowed_params[param:get_name(template_args)] = true
seen[raw] = true
end
end
-- If frame.args[1] contains a comma separated list of param names, add
-- those as well.
if additional then
for param in gsplit(additional, ",", true) do
-- scribunto_param_key normalizes the param into the form returned
-- by param:get_name() (i.e. trimmed and converted to a number if
-- appropriate).
allowed_params[scribunto_param_key(param)] = true
end
end
local invalid_args = select(2, process_params(
template_args,
allowed_params,
"return unknown"
))
if not next(invalid_args) then
return invalid_args
end
-- Some templates use params 1 and 3 without using 2, which means that 2
-- will be in the list of invalid args when used as an empty placeholder
-- (e.g. {{foo|foo||bar}}). Detect and remove any empty positional
-- placeholder args.
local max_pos = 0
for param in pairs(allowed_params) do
if type(param) == "number" and param > max_pos then
max_pos = param
end
end
for param, arg in pairs(invalid_args) do
if (
type(param) == "number" and
param >= 1 and
param < max_pos and
-- Ignore if arg is empty, or only contains chars trimmed by
-- MediaWiki when handling named parameters.
not match(arg, "[^%z\t-\v\r ]")
) then
invalid_args[param] = nil
end
end
return invalid_args
end
local function compare_params(a, b)
a, b = a[1], b[1]
local type_a = type(a)
if type_a == type(b) then
return a < b
end
return type_a == "number"
end
-- Convert `args` into an array of sorted PARAM=ARG strings, using the parameter
-- name as the sortkey, with numbered params sorted before strings.
local function args_to_sorted_tuples(args)
local msg, i = {}, 0
for k, v in pairs(args) do
i = i + 1
msg[i] = {k, v}
end
sort(msg, compare_params)
for j = 1, i do
msg[j] = concat(msg[j], "=")
end
return msg
end
local function apply_pre_tag(frame, invalid_args)
return frame:extensionTag("pre", concat(invalid_args, "\n"))
end
local function make_message(template_name, invalid_args, no_link)
local open, close
if no_link then
open, close = "", ""
else
open, close = "[[", "]]"
end
return "The template " .. open .. template_name .. close .. " does not use the parameter(s): " .. invalid_args .. " Please see " .. open .. "Module:checkparams" .. close .. " for help with this warning."
end
-- Called by non-Lua templates using "{{#invoke:checkparams|warn}}". `frame`
-- is checked for the following params:
-- `1=` (optional) a comma separated list of additional allowed parameters
-- `nowarn=` (optional) do not include preview warning in warning_text
-- `noattn=` (optional) do not include attention seeking span in in warning_text
function export.warn(frame)
local parent, frame_args = frame:getParent(), frame.args
local template_name = parent:getTitle()
local template_title = new_title(template_name)
local invalid_args = get_invalid_args(template_title, parent.args, frame_args[1])
-- If there are no invalid template args, return.
if not next(invalid_args) then
return ""
end
-- Otherwise, generate "Invalid params" warning to be inserted onto the
-- wiki page.
local warn, attn, cat
invalid_args = args_to_sorted_tuples(invalid_args)
-- Show warning in previewer.
if not frame_args.nowarn then
warn = tostring(html_create("sup")
:addClass("error")
:addClass("previewonly")
:tag("small")
:wikitext(make_message(template_name, apply_pre_tag(frame, invalid_args)))
:allDone())
end
-- Add attentionseeking message. <pre> tags don't work in HTML attributes,
-- so use semicolons as delimiters.
if not frame_args.noattn then
attn = tostring(html_create("span")
:addClass("attentionseeking")
:attr("title", make_message(template_name, concat(invalid_args, "; ") .. ".", "no_link"))
:allDone())
end
-- Categorize if neither the current page nor the template would go in a hidden maintenance category.
if not (uses_hidden_category(get_current_title()) or uses_hidden_category(template_title)) then
cat = format_categories({"Pages using invalid parameters when calling " .. template_name}, nil, "-", nil, "force_output")
end
return (warn or "") .. (attn or "") .. (cat or "")
end
-- Called by non-Lua templates using "{{#invoke:checkparams|error}}". `frame`
-- is checked for the following params:
-- `1=` (optional) a comma separated list of additional allowed parameters
function export.error(frame)
local parent = frame:getParent()
local template_name = parent:getTitle()
local invalid_args = get_invalid_args(new_title(template_name), parent.args, frame.args[1])
-- Use formatted_error, so that we can use <pre> tags in error messages:
-- any whitespace which isn't trimmed is treated as literal, so errors
-- caused by double-spaces or erroneous newlines in inputs need to be
-- displayed accurately.
if next(invalid_args) then
return require("Module:debug").formatted_error(make_message(
template_name,
apply_pre_tag(frame, args_to_sorted_tuples(invalid_args))
))
end
end
return export