모듈:Parameters

local m_str_utils = require("Module:String utilities")
local require_when_needed = require("Module:utilities/require when needed")
local labels_module = "Module:labels"
local dump = mw.dumpObject
local floor = math.floor
local gsplit = mw.text.gsplit
local gsub = string.gsub
local huge = math.huge
local insert = table.insert
local list_to_set = require("모듈:TableTools").listToSet
local list_to_text = mw.text.listToText
local match = string.match
local max = math.max
local pairs = pairs
local pattern_escape = m_str_utils.pattern_escape
local remove_holes = require_when_needed("Module:parameters/remove holes")
local scribunto_param_key = m_str_utils.scribunto_param_key
local sort = table.sort
local trim = mw.text.trim
local type = type
local yesno = require_when_needed("Module:yesno")
local export = {}
local function track(page)
	require("Module:debug/track")("parameters/" .. page)
end
local function save_pattern(name, list_name, patterns)
	name = type(name) == "string" and gsub(name, "\1", "") or name
	if match(list_name, "\1") then
		patterns"^" .. gsub(pattern_escape(list_name), "\1", "(1-9%%d*)") .. "$" = name
	else
		patterns"^" .. pattern_escape(list_name) .. "(1-9%d*)$" = name
	end
end
local function iterate_over_list(list)
local index, len = 0, #list
return function()
index = index + 1
if index  1 and "either " or "") .. concat_list(list, " or ") .. "; the value " .. dump(val) .. " is not valid.")
	end
end
local get_val = setmetatable({
	"boolean" = function(val)
		-- Set makes no sense with booleans, so don't bother checking for it.
		return yesno(val, true)
	end,
	
	"family" = function(val, name, param)
		if param.set then
			check_set(val, name, param)
		end
		return require("Module:families")param.method == "name" and "getByCanonicalName" or "getByCode"(val) or
			error("Parameter " .. dump(name) .. " should be a valid family " .. (param.method == "name" and "name" or "code") .. "; the value " .. dump(val) .. " is not valid. See WT:LOF.")
	end,
	
	"labels" = function(val, name, param)
		-- FIXME: Should be able to pass in a parse_err function.
		return require(labels_module).split_labels_on_comma(val)
	end,
	
	"language" = function(val, name, param)
		if param.set then
			check_set(val, name, param)
		end
		local lang = require("Module:languages")param.method == "name" and "getByCanonicalName" or "getByCode"(val, nil, param.etym_lang, param.family)
		if lang then
			return lang
		end
		local list = {"language"}
		local links = {"WT:LOL"}
		if param.etym_lang then
			insert(list, "etymology language")
			insert(links, "WT:LOL/E")
		end
		if param.family then
			insert(list, "family")
			insert(links, "WT:LOF")
		end
		error("Parameter " .. dump(name) .. " should be a valid " .. concat_list(list, " or ") .. " " .. (param.method == "name" and "name" or "code") .. "; the value " .. dump(val) .. " is not valid. See " .. concat_list(links, " and ") .. ".")
	end,
	
	"number" = function(val, name, param)
		if type(val) == "number" then
			return val
		end
		-- Avoid converting inputs like "nan" or "inf".
		val = tonumber(val:match("^+%-?%d+%.?%d*")) or
			error("Parameter " .. dump(name) .. " should be a valid number; the value " .. dump(val) .. " is not valid.")
		if param.set then
			check_set(val, name, param)
		end
		return val
	end,
	
	"script" = function(val, name, param)
		if param.set then
			check_set(val, name, param)
		end
		return require("Module:scripts")param.method == "name" and "getByCanonicalName" or "getByCode"(val) or
			error("Parameter " .. dump(name) .. " should be a valid script " .. (param.method == "name" and "name" or "code") .. "; the value " .. dump(val) .. " is not valid. See WT:LOS.")
	end,
	
	"string" = function(val, name, param)
		if param.set then
			check_set(val, name, param)
		end
		return val
	end,
	
	"wikimedia language" = function(val, name, param)
		if param.set then
			check_set(val, name, param)
		end
		return require("Module:wikimedia languages").getByCode(val) or
			error("Parameter " .. dump(name) .. " should be a valid wikimedia language code; the value " .. dump(val) .. " is not valid.")
	end,
}, {
	__call = function(self, val, name, param)
		local typ = param.type or "string"
		local func, sublist = selftyp, param.sublist
		if not func then
			error("Internal error: " .. dump(typ) .. " is not a recognized parameter type.")
		elseif sublist then
			local retlist = {}
			if type(val) ~= "string" then
				error("Internal error: " .. dump(val) .. " is not a string.")
			end
			if param.convert then
				local thisval, insval
				local thisindex = 0
				local parse_err
				if type(name) == "function" then
					-- We assume the passed-in error function in `name` already shows the parameter name and raw value.
					parse_err = function(msg)
						name(("%s: item #%s=%s"):format(msg_with_processed(msg, thisval, insval), thisindex,
							thisval))
					end
				else
					parse_err = function(msg)
						error(("%s: item #%s=%s of parameter %s=%s"):format(msg_with_processed(msg, thisval, insval),
							thisindex, thisval, name, val))
					end
				end
				for v in split_sublist(val, name, sublist) do
					thisval = v
					thisindex = thisindex + 1
					if param.set then
						check_set(v, name, param, typ)
					end
					insval = func(v, name, param)
					insert(retlist, param.convert(insval, parse_err))
				end
			else
				for v in split_sublist(val, name, sublist) do
					if param.set then
						check_set(v, name, param, typ)
					end
					insert(retlist, func(v, name, param))
				end
			end
			return retlist
		else
			if param.set then
				check_set(val, name, param, typ)
			end
			local retval = func(val, name, param)
			if param.convert then
				local parse_err
				if type(name) == "function" then
					-- We assume the passed-in error function in `name` already shows the parameter name and raw value.
					if retval == val then
						-- This is an optimization to avoid creating a closure. The second arm works correctly even
						-- when retval == val.
						parse_err = name
					else
						parse_err = function(msg)
							name(msg_with_processed(msg, val, retval))
						end
					end
				else
					parse_err = function(msg)
						error(("%s: parameter %s=%s"):format(msg_with_processed(msg, val, retval), name, val))
					end
				end
				retval = param.convert(retval, parse_err)
			end
			return retval
		end
	end
})
export.convert_val = convert_val -- used by Module:parameter utilities
function export.process(args, params, return_unknown)
	-- Process parameters for specific properties
	local args_new = {}
	local required = {}
	local seen = {}
	local patterns = {}
	local names_with_equal_sign = {}
	local list_from_index
	
	for name, param in pairs(params) do
		-- Populate required table, and make sure aliases aren't set to required.
		if param.required then
			if param.alias_of then
				error("`params` table error: parameter " .. dump(name) .. " is an alias of " .. dump(param.alias_of) .. ", but is also set as a required parameter. Only " .. dump(name) .. " should be set as required.")
			end
			requiredname = true
		end
		
		-- Convert param.set from a list into a set.
		-- `seen` prevents double-conversion if multiple parameter keys share the same param table.
		local set = param.set
		if set and not seenparam then
			param.set = list_to_set(set)
			seenparam = true
		end
		
		local alias = param.alias_of
		if alias then
			-- Check that the alias_of is set to a valid parameter.
			if not paramsalias then
				error("`params` table error: parameter " .. dump(name) .. " is an alias of an invalid parameter.")
			end
			-- Check that all the parameters in params are in the form Scribunto normalizes input argument keys into (e.g. 1 not "1", "foo" not " foo "). Otherwise, this function won't be able to normalize the input arguments in the expected way.
			local normalized = scribunto_param_key(alias)
			if alias ~= normalized then
				error("`params` table error: parameter " .. dump(alias) .. " (a " .. type(alias) .. ") given in the alias_of field of parameter " .. dump(name) .. " is not a normalized Scribunto parameter. Should be " .. dump(normalized) .. " (a " .. type(normalized) .. ").")
			-- Aliases can't be lists unless the canonical parameter is also a list.
			elseif param.list and not paramsalias.list then
				error("`params` table error: the list parameter " .. dump(name) .. " is set as an alias of " .. dump(alias) .. ", which is not a list parameter.")
			-- Aliases can't be aliases of other aliases.
			elseif paramsalias.alias_of then
				error("`params` table error: alias_of cannot be set to another alias: parameter " .. dump(name) .. " is set as an alias of " .. dump(alias) .. ", which is in turn an alias of " .. dump(paramsalias.alias_of) .. ". Set alias_of for " .. dump(name) .. " to " .. dump(paramsalias.alias_of) .. ".")
			end
		end
		
		local normalized = scribunto_param_key(name)
		if name ~= normalized then
			error("`params` table error: parameter " .. dump(name) .. " (a " .. type(name) .. ") is not a normalized Scribunto parameter. Should be " .. dump(normalized) .. " (a " .. type(normalized) .. ").")
		end
		
		if param.list then
			if not param.alias_of then
				local key = name
				if type(name) == "string" then
					key = gsub(name, "\1", "")
				end
				-- _list is used as a temporary flag.
				args_newkey = {maxindex = 0, _list = true}
			end
			
			if type(param.list) == "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.
				save_pattern(name, param.list, patterns)
			elseif type(name) == "number" then
				if list_from_index then
					error("`params` table 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.
				list_from_index = name
			else
				save_pattern(name, name, patterns)
			end
			
			if match(name, "\1") then
				insert(names_with_equal_sign, name)
			end
		end
	end
	
	--Process required changes to `params`.
	for i = 1, #names_with_equal_sign do
		local name = names_with_equal_signi
		paramsgsub(name, "\1", "") = paramsname
		paramsname = nil
	end
	
	-- Process the arguments
	local args_unknown = {}
	local max_index
	
	for name, val in pairs(args) do
		local orig_name, raw_type, index, normalized = name, type(name)
		
		if raw_type == "number" then
			if list_from_index ~= nil and name >= list_from_index then
				index = name - list_from_index + 1
				name = list_from_index
			end
		else
			-- Does this argument name match a pattern?
			for pattern, pname in pairs(patterns) do
				index = match(name, pattern)
				-- It matches, so store the parameter name and the
				-- numeric index extracted from the argument name.
				if index then
					index = tonumber(index)
					name = pname
					break
				end
			end
		end
		
		local param = paramsname
		
		if param and param.require_index then
			-- Disallow require_index for numeric parameter names, as this doesn't make sense.
			if raw_type == "number" then
				error("`params` table error: cannot set require_index for numeric parameter " .. dump(name) .. ".")
			-- If a parameter without the trailing index was found, and
			-- require_index is set on the param, set the param to nil to treat it
			-- as if it isn't recognized.
			elseif not index then
				param = nil
			end
		end
		
		-- If the argument is not in the list of parameters, trigger an error.
		-- return_unknown suppresses the error, and stores it in a separate list instead.
		if not param then
			if return_unknown then
				args_unknownname = val
			else
				error("매개변수 " .. dump(name) .. "는 이 틀에서 사용되지 않습니다.", 2)
			end
		else
			-- Check that separate_no_index is not being used with a numeric parameter.
			if param.separate_no_index then
				if raw_type == "number" then
					error("`params` table error: cannot set separate_no_index for numeric parameter " .. dump(name) .. ".")
				elseif type(param.alias_of) == "number" then
					error("`params` table error: cannot set separate_no_index for parameter " .. dump(name) .. ", as it is an alias of numeric parameter " .. dump(param.alias_of) .. ".")
				end
			end
			
			-- If no index was found, use 1 as the default index.
			-- This makes list parameters like g, g2, g3 put g at index 1.
			-- If `separate_no_index` is set, then use 0 as the default instead.
			if param.list then
				index = index or param.separate_no_index and 0 or 1
			end
			
			-- Normalize to the canonical parameter name. If it's a list, but the alias is not, then determine the index.
			local raw_name = param.alias_of
			if param.alias_of then
				raw_type = type(raw_name)
				if raw_type == "number" then
					if paramsraw_name.list then
						index = index or param.separate_no_index and 0 or 1
						normalized = raw_name + index - 1
					else
						normalized = raw_name
					end
					name = raw_name
				else
					name = gsub(raw_name, "\1", "")
					if paramsname.list then
						index = index or param.separate_no_index and 0 or 1
					end
					if not index or index == 0 then
						normalized = name
					elseif name == raw_name then
						normalized = name .. index
					else
						normalized = gsub(raw_name, "\1", index)
					end
				end
			else
				normalized = orig_name
			end
			
			-- Remove leading and trailing whitespace unless allow_whitespace is true.
			if not param.allow_whitespace then
				val = trim(val)
			end
			
			-- Empty string is equivalent to nil unless allow_empty is true.
			if val == "" and not param.allow_empty then
				val = nil
				-- Track empty parameters, unless (1) allow_empty is set or (2) they're numbered parameters where a higher numbered parameter is also in use (e.g. track , but not term).
				if raw_type == "number" and not max_index then
					-- Find the highest numbered parameter that's in use/an empty string, as we don't want parameters like 500= to mean we can't track any empty parameters with a lower index than 500.
					local n = 0
					while argsn + 1 do
						n = n + 1
					end
					max_index = 0
					for n = n, 1, -1 do
						if argsn ~= "" then
							max_index = n
							break
						end
					end
				end
				if raw_type ~= "number" or name > max_index then
					-- Disable this for now as it causes slowdowns on large pages like a.
					-- track("empty parameter")
				end
			end
			
			-- Can't use "if val" alone, because val may be a boolean false.
			if val ~= nil then
				-- Convert to proper type if necessary.
				val = get_val(val, orig_name, paramsraw_name or param)
				
				-- Mark it as no longer required, as it is present.
				requiredname = nil
				
				-- Store the argument value.
				if index then
					-- If the parameter is duplicated, throw an error.
					if args_newnameindex ~= nil then
						error("Parameter " .. dump(normalized) .. " 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.")
					end
					args_newnameindex = val
					
					-- Store the highest index we find.
					args_newname.maxindex = max(index, args_newname.maxindex)
					if args_newname0 ~= nil then
						args_newname.default = args_newname0
						if args_newname.maxindex == 0 then
							args_newname.maxindex = 1
						end
						args_newname0 = nil
						
					end
					
					if paramsname.list then
						-- Don't store index 0, as it's a proxy for the default.
						if index > 0 then
							args_newnameindex = val
							-- Store the highest index we find.
							args_newname.maxindex = max(index, args_newname.maxindex)
						end
					else
						args_newname = val
					end
				else
					-- If the parameter is duplicated, throw an error.
					if args_newname ~= nil then
						error("Parameter " .. dump(normalized) .. " has been entered more than once. This is probably because a parameter alias has been used.")
					end
					
					if not param.alias_of then
						args_newname = val
					else
						if paramsparam.alias_of.list then
							args_newparam.alias_of1 = val
							
							-- Store the highest index we find.
							args_newparam.alias_of.maxindex = max(1, args_newparam.alias_of.maxindex)
						else
							args_newparam.alias_of = val
						end
					end
				end
			end
		end
	end
	
	-- Remove holes in any list parameters if needed.
	for name, val in pairs(args_new) do
		if type(val) == "table" and val._list then
			if paramsname.disallow_holes then
				local highest = 0
				for num, _ in pairs(val) do
					if type(num) == "number" and num > 0 and num  0 then
			error("Parameter" .. (
				n == 1 and (" " .. list1 .. " is") or
				("s " .. concat_list(list, " and ", true) .. " are")
			) .. " required.", 2)
		end
	end
	
	-- Remove the temporary _list flag.
	for _, arg_new in pairs(args_new) do
		if type(arg_new) == "table" then
			arg_new._list = nil
		end
	end
	
	if return_unknown then
		return args_new, args_unknown
	else
		return args_new
	end
end
return export