Jump to content

Module:Parameters

From Wikibooks, open books for an open world
local export = {}

local function track(page, calling_module, calling_function, param_name)
	local track = require("Module:debug/track")
	local tracking_page = "parameters/" .. page
	-- Cascades down in specificity, as each level is a prerequisite for the next.
	track(tracking_page)
	if calling_module then
		track(tracking_page .. "/" .. calling_module)
		if calling_function then
			track(tracking_page .. "/" .. calling_module .. "/" .. calling_function)
			if param_name then
				track(tracking_page .. "/" .. calling_module .. "/" .. calling_function .. "/" .. param_name)
			end
		end
	end
	return true
end

function export.process(args, params, return_unknown, calling_module, calling_function)
	local args_new = {}
	
	if not calling_module then
		track("no calling module")
	end
	if not calling_function then
		track("no calling function", calling_module)
	end
	
	-- Process parameters for specific properties
	local required = {}
	local patterns = {}
	local names_with_equal_sign = {}
	local list_from_index = nil
	
	for name, param in pairs(params) do
		if param.required then
			if param.alias_of then
				track("required alias", calling_module, calling_function, name)
			end
			required[name] = true
		end
		
		if name == 1 and param.no_lang_code then
			if not params["notlangcode"] then
				error("The parameter \"notlangcode\" must be enabled for this template.", 2)
			elseif not args["notlangcode"] and require("Module:languages").getByCode(args[name]) then
				error("The parameter \"" .. name .. "\" should not be a language code.", 2)
			end
		end
		
		if param.list then
			-- A helper function to escape magic characters in a string
			-- Magic characters: ^$()%.[]*+-?
			local plain = require("Module:string/pattern_escape")

			local key = name
			if type(name) == "string" then
				key = string.gsub(name, "=", "")
			end
			if param.default ~= nil then
				args_new[key] = {param.default, maxindex = 1}
			else
				args_new[key] = {maxindex = 0}
			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.
				if string.find(param.list, "=") then
					patterns["^" .. string.gsub(plain(param.list), "=", "(%%d+)") .. "$"] = name
				else
					patterns["^" .. plain(param.list) .. "(%d+)$"] = name
				end
			elseif type(name) == "number" then
				-- If the name is a number, then all indexed parameters from
				-- this number onwards go in the list.
				list_from_index = name
			else
				if string.find(name, "=") then
					patterns["^" .. string.gsub(plain(name), "=", "(%%d+)") .. "$"] = string.gsub(name, "=", "")
				else
					patterns["^" .. plain(name) .. "(%d+)$"] = name
				end
			end
			
			if string.find(name, "=") then
				-- DO NOT SIDE-EFFECT A TABLE WHILE ITERATING OVER IT.
				-- Some elements may be skipped or processed twice if you do.
				-- Instead, track the changes we want to make to `params`, and
				-- do them after the iteration over `params` is done.
				table.insert(names_with_equal_sign, name)
			end
		elseif param.default ~= nil then
			args_new[name] = param.default
		end
	end

	--Process required changes to `params`.
	if #names_with_equal_sign > 0 then
		local m_params_data = calling_module and mw.loadData("Module:parameters/data")[calling_module]
		-- If there is a ready-made version in the data module, use that.
		if m_params_data and m_params_data[calling_function .. "_no_equals"] then
			params = m_params_data[calling_function .. "_no_equals"]
		-- Otherwise, shallow copy the params table and substitute the keys.
		else
			params = require("Module:table").shallowcopy(params)
			for _, name in ipairs(names_with_equal_sign) do
				track("name with equals", calling_module, calling_function, name)
				params[string.gsub(name, "=", "")] = params[name]
				params[name] = nil
			end
		end
	end

	-- Process the arguments
	local args_unknown = {}
	local max_index
	
	for name, val in pairs(args) do
		local index = nil
		
		if type(name) == "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 = mw.ustring.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 = params[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.
		if not index and param and param.require_index then
			param = nil
		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.
		index = index or (param and param.separate_no_index and 0) or 1
		
		-- 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_unknown[name] = val
			else
				error("The parameter \"" .. name .. "\" is not used by this template.", 2)
			end
		else
			-- Remove leading and trailing whitespace unless allow_whitespace is true.
			if not param.allow_whitespace then
				val = mw.text.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 {{l|en|term|}}, but not {{l|en||term}}).
				if type(name) == "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 max_contiguous_index = 0
					while args[max_contiguous_index + 1] do
						max_contiguous_index = max_contiguous_index + 1
					end
					if max_contiguous_index > 0 then
						for name, val in pairs(args) do
							if type(name) == "number" and name > 0 and name <= max_contiguous_index and ((not max_index) or name > max_index) and val ~= "" then
								max_index = name
							end
						end
					end
					max_index = max_index or 0
				end
				if type(name) ~= "number" or name > max_index then
					track("empty parameter", calling_module, calling_function, name)
				end
			end
			
			-- Convert to proper type if necessary.
			if param.type == "boolean" then
				val = require("Module:yesno")(val, true)
			elseif param.type == "number" then
				val = tonumber(val)
			elseif param.type then
				track("unrecognized type", calling_module, calling_function, name)
				track("unrecognized type/" .. tostring(param.type), calling_module, calling_function, name)
			end
			
			-- Can't use "if val" alone, because val may be a boolean false.
			if val ~= nil then
				-- Mark it as no longer required, as it is present.
				required[param.alias_of or name] = nil
				
				-- Store the argument value.
				if param.list then
					-- If the parameter is an alias of another, store it as the original,
					-- but avoid overwriting it; the original takes precedence.
					if not param.alias_of then
						args_new[name][index] = val
						
						-- Store the highest index we find.
						args_new[name].maxindex = math.max(index, args_new[name].maxindex)
						if args_new[name][0] then
							args_new[name].default = args_new[name][0]
							args_new[name][0] = nil
						end
					elseif args[param.alias_of] == nil then
						if params[param.alias_of] and params[param.alias_of].list then
							args_new[param.alias_of][index] = val
							
							-- Store the highest index we find.
							args_new[param.alias_of].maxindex = math.max(index, args_new[param.alias_of].maxindex)
						else
							args_new[param.alias_of] = val
						end
					end
				else
					-- If the parameter is an alias of another, store it as the original,
					-- but avoid overwriting it; the original takes precedence.
					if not param.alias_of then
						args_new[name] = val
					elseif args[param.alias_of] == nil then
						if params[param.alias_of] and params[param.alias_of].list then
							args_new[param.alias_of][1] = val
							
							-- Store the highest index we find.
							args_new[param.alias_of].maxindex = math.max(1, args_new[param.alias_of].maxindex)
						else
							args_new[param.alias_of] = val
						end
					end
				end
			end
		end
	end
	
	-- The required table should now be empty.
	-- If any entry remains, trigger an error, unless we're in the template namespace.
	if mw.title.getCurrentTitle().namespace ~= 10 then
		local list = {}
		for name, param in pairs(required) do
			table.insert(list, name)
		end
		if #list > 0 then
			error('The parameters "' .. mw.text.listToText(list, '", "', '" and "') .. '" are required.', 2)
		end
	end
	
	-- Remove holes in any list parameters if needed.
	for name, val in pairs(args_new) do
		if type(val) == "table" and not params[name].allow_holes then
			args_new[name] = require("Module:parameters/remove_holes")(val)
		end
	end
	
	if return_unknown then
		return args_new, args_unknown
	else
		return args_new
	end
end

return export