Module:parameters

From Linguifex
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.


local export = {}

local function track(page, calling_module, calling_function, param_name)
	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 = not (not val or val == "" or val == "0" or val == "no" or val == "n" or val == "false")
			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