Module:En-headword

local export = {}
local pos_functions = {}

local lang = require("Module:languages").getByCode("en")
local PAGENAME = mw.title.getCurrentTitle().text

local function glossary_link(entry, text)
	text = text or entry
	return "[[Appendix:Glossary#" .. entry .. "|" .. text .. "]]"
end

-- The main entry point.
-- This is the only function that can be invoked from a template.
function export.show(frame)
	
	local poscat = frame.args[1] or error("Part of speech has not been specified. Please pass parameter 1 to the module invocation.")
	
	local params = {
		["head"] = {list = true, default = ""},
		["suff"] = {type = "boolean"},
		["sort"] = {},
	}
	
	local pos_data = pos_functions[poscat]
	if pos_data then
		for key, val in pairs(pos_data.params) do
			params[key] = val
		end
	end
	
	local args, unknown_args = require("Module:parameters").process(frame:getParent().args, params, pos_data.return_unknown)
	
	if unknown_args and next(unknown_args) then
		require "Module:debug".track { "en-headword/unknown args", "en-headword/unknown args/POS/" .. tostring(poscat) }
		for parameter, value in pairs(unknown_args) do
			require "Module:debug".track("en-headword/unknown args/param/" .. tostring(parameter))
			mw.log("unknown parameter in [[Module:headword]]: |" .. tostring(parameter) .. "=" .. tostring(value))
		end
	end
	
	local data = {lang = lang, pos_category = poscat, categories = {}, heads = args["head"], inflections = {}}
	
	if args["suff"] then
		data.pos_category = "suffixes"
		
		if poscat == "adjectives" or poscat == "adverbs" or poscat == "nouns" or poscat == "verbs" then
			table.insert(data.categories, ("%s %s-forming suffixes")
				:format(lang:getCanonicalName(), poscat:gsub("s$", "")))
		else
			error("No category exists for suffixes forming " .. poscat .. ".")
		end
	end
	
	if pos_data then
		pos_data.func(args, data)
	end
	
	local extra_categories = {}
	if PAGENAME:find("[Qq][^Uu]") or PAGENAME:find("[Qq]$") then
		table.insert(data.categories, lang:getCanonicalName() .. " words containing Q not followed by U")
	end
	if PAGENAME:find("([A-Za-z])%1%1") then
		table.insert(data.categories, lang:getCanonicalName() .. " words containing three consecutive instances of the same letter")
	end
	if PAGENAME:find("([A-Za-z])%1%1%1") then
		table.insert(data.categories, lang:getCanonicalName() .. " words containing four consecutive instances of the same letter")
	end
	if PAGENAME:find("[^c]ie") or PAGENAME:find("cei") then
		table.insert(data.categories, lang:getCanonicalName() .. " words following the I before E except after C rule")
	end
	if PAGENAME:find("[^c]ei") or PAGENAME:find("cie") then
		table.insert(data.categories, lang:getCanonicalName() .. " words not following the I before E except after C rule")
	end
	-- mw.ustring.toNFD performs decomposition, so letters that decompose
	-- to an ASCII vowel and a diacritic, such as é, are counted as vowels and
	-- do not need to be included in the pattern.
	if not mw.ustring.find(mw.ustring.lower(mw.ustring.toNFD(PAGENAME)), "[aeiouyæœø]") then
		table.insert(data.categories, lang:getCanonicalName() .. " words without vowels")
	end
	if PAGENAME:find("yre$") then
		table.insert(data.categories, lang:getCanonicalName() .. ' words ending in "-yre"')
	end
	if not PAGENAME:find(" ") and PAGENAME:len() > 25 then
		table.insert(extra_categories, "Long " .. lang:getCanonicalName() .. ' words')
	end
	if PAGENAME:find("^[^aeiou ]*a[^aeiou ]*e[^aeiou ]*i[^aeiou ]*o[^aeiou ]*u[^aeiou ]*$") then
		table.insert(data.categories, lang:getCanonicalName() .. ' words that use all vowels in alphabetical order')
	end
	data.sort_key = args.sort
	return require("Module:headword").full_headword(data)
		.. (#extra_categories > 0
			and require("Module:utilities").format_categories(extra_categories, lang, args.sort)
			or "")
end

-- This function does the common work between adjectives and adverbs
function make_comparatives(params, data)
	local comp_parts = {label = glossary_link("comparative"), accel = {form = "comparative"}}
	local sup_parts = {label = glossary_link("superlative"), accel = {form = "superlative"}}
	
	if #params == 0 then
		table.insert(params, {"more"})
	end
	
	-- To form the stem, replace -(e)y with -i and remove a final -e.
	local stem = PAGENAME:gsub("([^aeiou])e?y$", "%1i"):gsub("e$", "")
	
	-- Go over each parameter given and create a comparative and superlative form
	for i, val in ipairs(params) do
		local comp = val[1]
		local sup = val[2]
		
		if comp == "more" and PAGENAME ~= "many" and PAGENAME ~= "much" then
			table.insert(comp_parts, "[[define:mair|maist]] " .. PAGENAME)
			table.insert(sup_parts, "[[define:maist|maist]] " .. PAGENAME)
		elseif comp == "further" and PAGENAME ~= "far" then
			table.insert(comp_parts, "[[further]] " .. PAGENAME)
			table.insert(sup_parts, "[[furthest]] " .. PAGENAME)
		elseif comp == "er" then
			table.insert(comp_parts, stem .. "er")
			table.insert(sup_parts, stem .. "est")
		elseif comp == "-" or sup == "-" then
			-- Allowing '-' makes it more flexible to not have some forms
			if comp ~= "-" then
				table.insert(comp_parts, comp)
			end
			if sup ~= "-" then
				table.insert(sup_parts, sup)
			end
		else
			-- If the full comparative was given, but no superlative, then
			-- create it by replacing the ending -er with -est.
			if not sup then
				if comp:find("er$") then
					sup = comp:gsub("er$", "est")
				else
					error("The superlative of \"" .. comp .. "\" cannae be generatit automatically. Please provide it wi the \"sup" .. (i == 1 and "" or i) .. "=\" parameter.")
				end
			end
			
			table.insert(comp_parts, comp)
			table.insert(sup_parts, sup)
		end
	end
	
	table.insert(data.inflections, comp_parts)
	table.insert(data.inflections, sup_parts)
end

pos_functions["adjectives"] = {
	params = {
		[1] = {list = true, allow_holes = true},
		["sup"] = {list = true, allow_holes = true},
		},
	func = function(args, data)
		local shift = 0
		local is_not_comparable = false
		local is_comparative_only = false
		
		-- If the first parameter is ?, then don't show anything, just return.
		if args[1][1] == "?" then
			return
		-- If the first parameter is -, then move all parameters up one position.
		elseif args[1][1] == "-" then
			shift = 1
			is_not_comparable = true
		-- If the only argument is +, then remember this and clear parameters
		elseif args[1][1] == "+" and args[1].maxindex == 1 then
			shift = 1
			is_comparative_only = true
		end
		
		-- Gather all the comparative and superlative parameters.
		local params = {}
		
		for i = 1, args[1].maxindex - shift do
			local comp = args[1][i + shift]
			local sup = args["sup"][i]
			
			if comp or sup then
				table.insert(params, {comp, sup})
			end
		end
		
		if shift == 1 then
			-- If the first parameter is "-" but there are no parameters,
			-- then show "not comparable" only and return.
			-- If there are parameters, then show "not generally comparable"
			-- before the forms.
			if #params == 0 then
				if is_not_comparable then
					table.insert(data.inflections, {label = "nae " .. glossary_link("comparable")})
					table.insert(data.categories, lang:getCanonicalName() .. " uncomparable adjectives")
					return
				end
				if is_comparative_only then
					table.insert(data.inflections, {label = glossary_link("comparative") .. " form only"})
					table.insert(data.categories, lang:getCanonicalName() .. " comparative-only adjectives")
					return
				end
			else
				table.insert(data.inflections, {label = "nae generally " .. glossary_link("comparable")})
			end
		end
		
		-- Process the parameters
		make_comparatives(params, data)
	end
}

pos_functions["adverbs"] = {
	params = {
		[1] = {list = true, allow_holes = true},
		["sup"] = {list = true, allow_holes = true},
		},
	func = function(args, data)
		local shift = 0
		
		-- If the first parameter is ?, then don't show anything, just return.
		if args[1][1] == "?" then
			return
		-- If the first parameter is -, then move all parameters up one position.
		elseif args[1][1] == "-" then
			shift = 1
		end
		
		-- Gather all the comparative and superlative parameters.
		local params = {}
		
		for i = 1, args[1].maxindex - shift do
			local comp = args[1][i + shift]
			local sup = args["sup"][i]
			
			if comp or sup then
				table.insert(params, {comp, sup})
			end
		end
		
		if shift == 1 then
			-- If the first parameter is "-" but there are no parameters,
			-- then show "not comparable" only and return. If there are parameters,
			-- then show "not generally comparable" before the forms.
			if #params == 0 then
				table.insert(data.inflections, {label = "nae " .. glossary_link("comparable")})
				table.insert(data.categories, lang:getCanonicalName() .. " uncomparable adverbs")
				return
			else
				table.insert(data.inflections, {label = "nae generally " .. glossary_link("comparable")})
			end
		end
		
		-- Process the parameters
		make_comparatives(params, data)
	end
}

pos_functions["conjunctions"] = {
	params = {
		[1] = { alias_of = "head" },
	},
	return_unknown = true,
	func = function (args, data)
		
	end,
}

pos_functions["interjections"] = {
	params = {
		[1] = { alias_of = "head" },
	},
	return_unknown = true,
	func = function (args, data)
		
	end,
}

pos_functions["nouns"] = {
	params = {
		[1] = {list = true, allow_holes = true},
		["pl=qual"] = { list = true, allow_holes = true },
		},
	func = function(args, data)
		-- Gather all the plural parameters from the numbered parameters.
		local plurals = {}
		
		for i = 1, args[1].maxindex do
			local pl = args[1][i]
			
			if pl then
				local qual = args["plqual"][i]
				
				if qual then
					table.insert(plurals, {term = pl, qualifiers = {qual}})
				else
					table.insert(plurals, pl)
				end
			end
		end
		
		-- Decide what to do next...
		local mode = nil
		
		if plurals[1] == "?" or plurals[1] == "!" or plurals[1] == "-" or plurals[1] == "~" then
			mode = plurals[1]
			table.remove(plurals, 1)  -- Remove the mode parameter
		end
		
		-- Plural is unknown
		if mode == "?" then
			table.insert(data.categories, lang:getCanonicalName() .. " nouns with unknown or uncertain plurals")
			return
		-- Plural is not attested
		elseif mode == "!" then
			table.insert(data.inflections, {label = "plural nae attestit"})
			table.insert(data.categories, lang:getCanonicalName() .. " nouns wi unattestit plurals")
			return
		-- Uncountable noun; may occasionally have a plural
		elseif mode == "-" then
			table.insert(data.categories, lang:getCanonicalName() .. " uncountable nouns")
			
			-- If plural forms were given explicitly, then show "usually"
			if #plurals > 0 then
				table.insert(data.inflections, {label = "usually " .. glossary_link("uncountable")})
				table.insert(data.categories, lang:getCanonicalName() .. " countable nouns")
			else
				table.insert(data.inflections, {label = glossary_link("uncountable")})
			end
		-- Mixed countable/uncountable noun, always has a plural
		elseif mode == "~" then
			table.insert(data.inflections, {label = glossary_link("countable") .. " and " .. glossary_link("uncountable")})
			table.insert(data.categories, lang:getCanonicalName() .. " uncountable nouns")
			table.insert(data.categories, lang:getCanonicalName() .. " countable nouns")
			
			-- If no plural was given, add a default one now
			if #plurals == 0 then
				plurals = {"s"}
			end
		-- The default, always has a plural
		else
			table.insert(data.categories, lang:getCanonicalName() .. " countable nouns")
			
			-- If no plural was given, add a default one now
			if #plurals == 0 then
				plurals = {"s"}
			end
			if plural and not mw.title.new(plural).exists then
				table.insert(categories, "English nouns wi missin plurals")
			end
		end
		
		-- If there are no plurals to show, return now
		if #plurals == 0 then
			return
		end
		
		-- There are plural forms to show, so show them
		local pl_parts = {label = "plural", accel = {form = "p"}}
		
		local function check_ies(pl, stem)
			local newplural, nummatches = stem:gsub("([^aeiou])y$","%1ies")
			return nummatches > 0 and pl == newplural
		end
		local stem = PAGENAME
		local irregular = false
		for i, pl in ipairs(plurals) do
			if pl == "s" then
				table.insert(pl_parts, stem .. "s")
			elseif pl == "es" then
				table.insert(pl_parts, stem .. "es")
			else
				table.insert(pl_parts, pl)
				if type(pl) == "table" then
					pl = pl.term
				end
				if not stem:find(" ") and not (pl == stem .. "s" or pl == stem .. "es" or check_ies(pl, stem)) then
					irregular = true
					if pl == stem then
						table.insert(data.categories, lang:getCanonicalName() .. " indeclinable nouns")
					end
				end
			end
		end
		if irregular then
			table.insert(data.categories, lang:getCanonicalName() .. " nouns with irregular plurals")
		end
		
		table.insert(data.inflections, pl_parts)
	end
}

pos_functions["proper nouns"] = {
	params = {
		[1] = {list = true},
		},
	func = function(args, data)
		local plurals = args[1]
		
		-- Decide what to do next...
		local mode = nil
		
		if plurals[1] == "?" or plurals[1] == "!" or plurals[1] == "-" or plurals[1] == "~" then
			mode = plurals[1]
			table.remove(plurals, 1)  -- Remove the mode parameter
		end
		
		-- Plural is unknown
		if mode == "?" then
			table.insert(data.categories, lang:getCanonicalName() .. " proper nouns wi unkent or uncertain plurals")
			return
		-- Plural is not attested
		elseif mode == "!" then
			table.insert(data.inflections, {label = "plural nae attestit"})
			table.insert(data.categories, lang:getCanonicalName() .. " proper nouns wi unattestit plurals")
			return
		-- Uncountable noun; may occasionally have a plural
		elseif mode == "-" then
			-- If plural forms were given explicitly, then show "usually"
			if #plurals > 0 then
				table.insert(data.inflections, {label = "usually " .. glossary_link("uncountable")})
				table.insert(data.categories, lang:getCanonicalName() .. " countable proper nouns")
			else
				table.insert(data.inflections, {label = glossary_link("uncountable")})
			end
		-- Mixed countable/uncountable noun, always has a plural
		elseif mode == "~" then
			table.insert(data.inflections, {label = glossary_link("countable") .. " and " .. glossary_link("uncountable")})
			table.insert(data.categories, lang:getCanonicalName() .. " countable proper nouns")
			
			-- If no plural was given, add a default one now
			if #plurals == 0 then
				plurals = {"s"}
			end
		elseif #plurals > 0 then
			table.insert(data.categories, lang:getCanonicalName() .. " countable proper nouns")
		end
		
		-- If there are no plurals to show, return now
		if #plurals == 0 then
			return
		end
		
		-- There are plural forms to show, so show them
		local pl_parts = {label = "plural", accel = {form = "p"}}
		
		local stem = PAGENAME
		
		for i, pl in ipairs(plurals) do
			if pl == "s" then
				table.insert(pl_parts, stem .. "s")
			elseif pl == "es" then
				table.insert(pl_parts, stem .. "es")
			else
				table.insert(pl_parts, pl)
			end
	
		end
		
		table.insert(data.inflections, pl_parts)
	end
}

pos_functions["verbs"] = {
	params = {
		[1] = {list = "pres_3sg", allow_holes = true},
		
		["pres_3sg_qual"] = {},
		["pres_3sg2_qual"] = {},
		["pres_3sg3_qual"] = {},
		["pres_3sg4_qual"] = {},
		["pres_3sg5_qual"] = {},
		
		[2] = {list = "pres_ptc", allow_holes = true},
		
		["pres_ptc_qual"] = {},
		["pres_ptc2_qual"] = {},
		["pres_ptc3_qual"] = {},
		["pres_ptc4_qual"] = {},
		["pres_ptc5_qual"] = {},
		
		[3] = {list = "past", allow_holes = true},
		
		["past_qual"] = {},
		["past2_qual"] = {},
		["past3_qual"] = {},
		["past4_qual"] = {},
		["past5_qual"] = {},
		
		[4] = {list = "past_ptc", allow_holes = true},
		
		["past_ptc_qual"] = {},
		["past_ptc2_qual"] = {},
		["past_ptc3_qual"] = {},
		["past_ptc4_qual"] = {},
		["past_ptc5_qual"] = {},
		},
	func = function(args, data)
		-- Get parameters
		local par1 = args[1][1]
		local par2 = args[2][1]
		local par3 = args[3][1]
		local par4 = args[4][1]
		
		local pres_3sg_forms = {label = "third-person singular simple present", accel = {form = "3|s|pres"}}
		local pres_ptc_forms = {label = "present participle", accel = {form = "pres|ptcp"}}
		local past_forms = {label = "simple past", accel = {form = "past"}}
		local pres_3sg_form = par1 or PAGENAME .. "s"
		local pres_ptc_form = par2 or PAGENAME .. "ing"
		local past_form = par3 or PAGENAME .. "ed"
		
		if par1 and not par2 and not par3 then
			-- This is the "new" format, which uses only the first parameter.
			if par1 == "es" then
				pres_3sg_form = PAGENAME .. "es"
				pres_ptc_form = PAGENAME .. "ing"
				past_form = PAGENAME .. "ed"
			elseif par1 == "ies" then
				if not mw.ustring.find(PAGENAME, "y$") then
					error("The first parameter is \"ies\" but the verb dinnae end in -y.")
				end
				
				local stem = mw.ustring.gsub(PAGENAME, "y$", "")
				pres_3sg_form = stem .. "ies"
				pres_ptc_form = stem .. "ying"
				past_form = stem .. "ied"
			elseif par1 == "d" then
				pres_3sg_form = PAGENAME .. "s"
				pres_ptc_form = PAGENAME .. "ing"
				past_form = PAGENAME .. "d"
			else
				pres_3sg_form = PAGENAME .. "s"
				pres_ptc_form = par1 .. "ing"
				past_form = par1 .. "ed"
			end
		else
			-- This is the "legacy" format, using the second and third parameters as well.
			-- It is included here for backwards compatibility and to ease the transition.
			if par3 then
				if par3 == "es" then
					require("Module:debug").track("en-headword/es3")
					pres_3sg_form = par1 .. par2 .. "es"
					pres_ptc_form = par1 .. par2 .. "ing"
					past_form = par1 .. par2 .. "ed"
				elseif par3 == "ing" then
					require("Module:debug").track("en-headword/ing3")
					pres_3sg_form = PAGENAME .. "s"
					pres_ptc_form = par1 .. par2 .. "ing"
					
					if par2 == "y" then
						past_form = PAGENAME .. "d"
					else
						past_form = par1 .. par2 .. "ed"
					end
				elseif par3 == "ed" then
					require("Module:debug").track("en-headword/ed3")
					
					if par2 == "i" then
						pres_3sg_form = par1 .. par2 .. "es"
						pres_ptc_form = PAGENAME .. "ing"
					else
						pres_3sg_form = PAGENAME .. "s"
						pres_ptc_form = par1 .. par2 .. "ing"
					end
					
					past_form = par1 .. par2 .. "ed"
				elseif par3 == "d" then
					require("Module:debug").track("en-headword/d3")
					pres_3sg_form = PAGENAME .. "s"
					pres_ptc_form = par1 .. par2 .. "ing"
					past_form = par1 .. par2 .. "d"
				else
					require("Module:debug").track("en-headword/xxx3")
				end
			else
				if par2 == "es" then
					require("Module:debug").track("en-headword/es2")
					pres_3sg_form = par1 .. "es"
					pres_ptc_form = par1 .. "ing"
					past_form = par1 .. "ed"
				elseif par2 == "ies" then
					require("Module:debug").track("en-headword/ies2")
					
					if par1 .. "y" ~= PAGENAME then
						require("Module:debug").track("en-headword/ies2/par1 nae pagename")
					end
					
					pres_3sg_form = par1 .. "ies"
					pres_ptc_form = par1 .. "ying"
					past_form = par1 .. "ied"
				elseif par2 == "ing" then
					require("Module:debug").track("en-headword/ing2")
					pres_3sg_form = PAGENAME .. "s"
					pres_ptc_form = par1 .. "ing"
					past_form = par1 .. "ed"
				elseif par2 == "ed" then
					require("Module:debug").track("en-headword/ed2")
					pres_3sg_form = PAGENAME .. "s"
					pres_ptc_form = par1 .. "ing"
					past_form = par1 .. "ed"
				elseif par2 == "d" then
					require("Module:debug").track("en-headword/d2")
					
					if par1 ~= PAGENAME then
						require("Module:debug").track("en-headword/d2/par1 nae pagename")
					end
					
					pres_3sg_form = PAGENAME .. "s"
					pres_ptc_form = par1 .. "ing"
					past_form = par1 .. "d"
				else
					require("Module:debug").track("en-headword/xxx2")
				end
			end
		end
		
		local pres_3sg_qual = args["pres_3sg_qual"]
		local pres_ptc_qual = args["pres_ptc_qual"]
		local past_qual = args["past_qual"]
		
		table.insert(pres_ptc_forms, {term = pres_ptc_form, qualifiers = {pres_ptc_qual}})
		table.insert(pres_3sg_forms, {term = pres_3sg_form, qualifiers = {pres_3sg_qual}})
		table.insert(past_forms, {term = past_form, qualifiers = {past_qual}})
		
		-- Present 3rd singular
		for i = 2, args[1].maxindex do
			local form = args[1][i]
			local qual = args["pres_3sg" .. i .. "_qual"]
			
			if form then
				table.insert(pres_3sg_forms, {term = form, qualifiers = {qual}})
			end
		end
		
		-- Present participle
		for i = 2, args[2].maxindex do
			local form = args[2][i]
			local qual = args["pres_ptc" .. i .. "_qual"]
			
			if form then
				table.insert(pres_ptc_forms, {term = form, qualifiers = {qual}})
			end
		end
		
		-- Past
		for i = 2, args[3].maxindex do
			local form = args[3][i]
			local qual = args["past" .. i .. "_qual"]
			
			if form then
				table.insert(past_forms, {term = form, qualifiers = {qual}})
			end
		end
		
		-- Past participle
		local found_past_ptc = false
		local past_ptc_forms = {label = "past participle", accel = {form = "past|ptcp"}}
		local qual = args["past_ptc_qual"]; if qual == "" then qual = nil end
		table.insert(past_ptc_forms, {term = par4 or past_form, qualifiers = {qual}})
		if par4 or qual then
			found_past_ptc = true
		end
		for i = 2, args[4].maxindex do
			local form = args[4][i]
			local qual = args["past_ptc" .. i .. "_qual"]
			if form then
				table.insert(past_ptc_forms, {term = form, qualifiers = {qual}})
				found_past_ptc = true
			end
		end
		
		-- Are the past forms identical to the past participle forms?
		local identical = true
		
		if #past_forms ~= #past_ptc_forms then
			identical = false
		else
			for key, val in ipairs(past_forms) do
				if past_ptc_forms[key].term ~= val.term or past_ptc_forms[key].qual ~= val.qual then
					identical = false
					break
				end
			end
		end
		
		-- Insert the forms
		table.insert(data.inflections, pres_3sg_forms)
		table.insert(data.inflections, pres_ptc_forms)
		
		if not found_past_ptc or identical then
			past_forms.label = "simple past an past participle"
			past_forms.accel = {form = "past|and|past|ptcp"}
			table.insert(data.inflections, past_forms)
		else
			table.insert(data.inflections, past_forms)
			table.insert(data.inflections, past_ptc_forms)
		end
	end
}

return export