Pumunta sa nilalaman

Module:la-headword

Mula Wiksiyonaryo


-- TODO: handle "-old" subtypes generally, appending "in Old Latin", where terms have additional OL inflections.

local export = {}
local pos_functions = {}

local string_utilities = "Module:string utilities"
local table_module = "Module:table"
local utilities_module = "Module:utilities"

local concat = table.concat
local form_is_empty = require("Module:la-utilities").form_is_empty
local insert = table.insert
local remove = table.remove
local umatch = mw.ustring.match

local lang = require("Module:languages").getByCode("la")
local NAMESPACE = mw.title.getCurrentTitle().nsText
local PAGENAME = mw.loadData("Module:headword/data").pagename

local legal_gender = {
	["m"] = true,
	["f"] = true,
	["n"] = true,
	["?"] = true,
	["?!"] = true,
}

local declension_to_english = {
	["1"] = "first",
	["2"] = "second",
	["3"] = "third",
	["4"] = "fourth",
	["5"] = "fifth",
}

local gender_names = {
	["m"] = "masculine",
	["f"] = "feminine",
	["n"] = "neuter",
	["?"] = "unknown gender",
	["?!"] = "unattested gender",
}

local function deep_equals(...)
	deep_equals = require(table_module).deepEquals
	return deep_equals(...)
end

local function get_plaintext(...)
	get_plaintext = require(utilities_module).get_plaintext
	return get_plaintext(...)
end

local function insert_if_not(...)
	insert_if_not = require(table_module).insertIfNot
	return insert_if_not(...)
end

local function invert(...)
	invert = require(table_module).invert
	return invert(...)
end

local function keys_to_list(...)
	keys_to_list = require(table_module).keysToList
	return keys_to_list(...)
end

local function pattern_escape(...)
	pattern_escape = require(string_utilities).pattern_escape
	return pattern_escape(...)
end

local function serial_comma_join(...)
	serial_comma_join = require(table_module).serialCommaJoin
	return serial_comma_join(...)
end

local function split(...)
	split = require(string_utilities).split
	return split(...)
end

local function ulower(...)
	ulower = require(string_utilities).lower
	return ulower(...)
end

local function format(array, concatenater)
	if #array == 0 then
		return ""
	end
	local concatenated = concat(array, concatenater)
	if concatenated == "" then
		return ""
	elseif concatenated:sub(-1) == "'" then
		concatenated = concatenated .. " "
	end
	return "; ''" .. concatenated .. "''"
end

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

local function make_link(page, display, face, accel)
	return require("Module:links").full_link({term = page, alt = display, lang = lang, accel = accel}, face)
end

-- The main entry point.
-- This is the only function that can be invoked from a template.
function export.show(frame)
	local iargs = require("Module:parameters").process(frame.args, {
		[1] = {required = true},
		["def"] = true,
		["suff_type"] = true,
	})
	local args = frame:getParent().args
	local poscat = iargs[1]
	local def = iargs.def
	local suff_type = iargs.suff_type
	local postype = nil
	if suff_type then
		postype = poscat .. '-' .. suff_type
	else
		postype = poscat
	end

	local data = {lang = lang, categories = {}, heads = {}, genders = {}, inflections = {}}
	local infl_classes = {}
	local title = {}
	local postscript = {}

	if poscat == "suffixes" then
		insert_if_not(data.categories, "Latin " .. suff_type .. "-forming suffixes")
	end

	if pos_functions[postype] then
		local new_poscat = pos_functions[postype](def, args, data, infl_classes, title, postscript)
		if new_poscat then
			poscat = new_poscat
		end
	end

	data.pos_category = poscat

	postscript = concat(postscript, ", ")

	return
		require("Module:headword").full_headword(data)
		.. format(infl_classes, "/")
		.. format(title, ", ")
		.. (postscript ~= "" and " (" .. postscript .. ")" or "")
end

local function process_num_type(numtype, categories)
	if numtype == "card" then
		insert_if_not(categories, "Latin cardinal numbers")
	elseif numtype == "ord" then
		insert_if_not(categories, "Latin ordinal numbers")
	elseif numtype == "frac" then
		insert_if_not(categories, "Latin fractional numbers")
	elseif numtype == "dist" then
		insert_if_not(categories, "Latin distributive numbers")
	elseif numtype == "mul" then
		insert_if_not(categories, "Latin multiplicative numbers")
	elseif numtype == "coll" then
		insert_if_not(categories, "Latin collective numbers")
	elseif numtype then
		error("Unrecognized numeral type '" .. numtype .. "'")
	end
end

local function normalize_derivation(deriv, anchor, text)
	if not deriv then
		return nil
	end
	local deriv1 = deriv[1]
	if not deriv1 or deriv1 == "-" then
		return deriv1
	end
	deriv.label = glossary_link(anchor, text)
	return deriv
end

local function derivation(data, decldata, arg, anchor, text)
	local der = normalize_derivation(decldata[arg], anchor, text)
	if der and der ~= "-" then
		insert(data.inflections, der)
	end
end

local function nouns(pos, def, args, data, infl_classes, title)
	local is_num = pos == "numerals"
	local is_pn = false
	if pos == "proper nouns" then
		is_pn = true
		pos = "nouns"
	end
	local decldata = require("Module:la-nominal").do_generate_noun_forms(args, pos, "headword", def, is_num)
	local lemma = decldata.overriding_lemma
	-- TODO: modify the headword line if the slot isn't the expected one.
	if #lemma == 0 then
		for slot in require("Module:la-nominal").iter_potential_noun_lemma_slots() do
			local potential_lemma = decldata.forms["linked_" .. slot]
			if not form_is_empty(potential_lemma) then
				if decldata.unattested[slot] then
					potential_lemma = "*" .. potential_lemma
				end
				lemma = potential_lemma
				break
			end
		end
	end

	data.heads = lemma
	-- Since we always set data.heads to the lemma and specification of the lemma is mandatory in {{la-noun}}, there aren't
	-- really any redundant heads.
	data.no_redundant_head_cat = true
	data.id = decldata.id
	
	local genders = decldata.overriding_genders
	if #genders == 0 then
		if decldata.gender then
			genders = {ulower(decldata.gender)}
		elseif not is_num then
			error("No gender explicitly specified in headword template using g=, and can't infer gender from lemma spec")
		end
	end

	local categories = data.categories
	if is_num then
		process_num_type(decldata.num_type, categories)
	end

	if decldata.indecl then
		insert(data.inflections, {label = glossary_link("indeclinable")})
		insert_if_not(categories, "Latin indeclinable " .. decldata.pos)

		for _, g in ipairs(genders) do
			local gender = g:match("^(.[\128-\191]*)%-[sp]$")
			if not gender then
				gender = g
			end
			if not legal_gender[gender] then
				error("Gender “" .. gender .. "” is not a valid Latin gender.")
			end
			insert(data.genders, g)
			insert_if_not(categories, "Latin " .. gender_names[gender] .. " indeclinable " .. decldata.pos)
		end
	else
		local is_irreg = false
		local is_indecl = false
		local is_decl = false
		local has_multiple_decls = false
		local has_multiple_variants = false
		-- flatten declension specs
		local decls = {}

		for _, g in ipairs(genders) do
			if not legal_gender[g] then
				error("Gender “" .. g .. "” is not a valid Latin gender.")
			elseif decldata.num == "pl" then
				g = g .. "-p"
			elseif decldata.num == "sg" then
				g = g .. "-s"
			end
			insert(data.genders, g)
		end

		local function process_decl(decl_list, props)
			local headword_decl = props.headword_decl
			-- skip adjectival declensions
			if headword_decl:match("+$") then
				return
			elseif props.decl == "irreg" then
				is_irreg = true
				headword_decl = headword_decl:match("^irreg/(.*)$") or headword_decl
				local irreg_decls = split(headword_decl, ",")
				if #irreg_decls > 1 then
					has_multiple_decls = true
				end
				for _, d in ipairs(irreg_decls) do
					if d == "indecl" or d == "0" then
						is_indecl = true
					else
						is_decl = true
					end
					insert_if_not(decl_list, d)
				end
			else
				if headword_decl == "indecl" or headword_decl == "0" then
					is_indecl = true
				else
					is_decl = true
				end
				insert_if_not(decl_list, headword_decl)
			end
		end

		for _, props in ipairs(decldata.propses) do
			if props.headword_decl then
				process_decl(decls, props)
			else
				local alternant_decls = {}
				for _, alternant in ipairs(props) do
					for _, single_props in ipairs(alternant) do
						process_decl(alternant_decls, single_props)
					end
				end
				if #alternant_decls > 1 then
					has_multiple_decls = true
				elseif #decls > 1 then
					has_multiple_variants = true
				end
				for _, d in ipairs(alternant_decls) do
					insert_if_not(decls, d)
				end
			end
		end

		if is_indecl and is_decl then
			has_multiple_decls = true
		end
		if has_multiple_decls then
			insert_if_not(categories, "Latin " .. decldata.pos .. " with multiple declensions")
		end
		if has_multiple_variants then
			insert_if_not(categories, "Latin " .. decldata.pos .. " with multiple variants of a single declension")
		end
		if is_irreg then
			insert(title, glossary_link("irregular"))
			insert_if_not(categories, "Latin irregular " .. decldata.pos)
			for _, g in ipairs(genders) do
				insert_if_not(categories, "Latin " .. gender_names[g] .. " irregular " .. decldata.pos)
			end
		end

		if is_indecl then
			if is_decl then
				insert(title, glossary_link("indeclinable"))
			else
				insert(data.inflections, {label = glossary_link("indeclinable")})
			end
			insert_if_not(categories, "Latin indeclinable " .. decldata.pos)
			for _, g in ipairs(genders) do
				insert_if_not(categories, "Latin " .. gender_names[g] .. " indeclinable " .. decldata.pos)
			end
		end

		if #decls > 1 then
			insert(title, "variously declined")
		end

		for _, decl in ipairs(decls) do
			if not (decl == "0" or decl == "indecl" or decl == "sgpl" or decl == "irreg") then
				local decl_class = declension_to_english[decl]
				if not decl_class then
					error("Internal error: declension '" .. decl .. "' not recognized")
				end
				insert(title, "[[Appendix:Latin " .. decl_class .. " declension|" .. decl_class .. " declension]]")
				insert_if_not(categories, "Latin " .. decl_class .. " declension " .. decldata.pos)
				for _, g in ipairs(genders) do
					insert_if_not(categories, "Latin " .. gender_names[g] .. " " .. decldata.pos .. " in the " .. decl_class .. " declension")
				end
			end
		end

		local lemma_num = decldata.num == "pl" and "pl" or "sg"
		if NAMESPACE == "Reconstruction" then
			-- For reconstructed nouns:
			if data.genders[1] == 'n' and lemma_num == 'sg' then
				-- singular neuter nouns give a plural
				local pl = decldata.forms["nom_pl"]
				if pl and pl ~= "" and #pl > 0 then
					pl.label = "plural"
					insert(data.inflections, pl)
				end
			else
				-- all others give an oblique
				local obl = decldata.forms["acc_" .. lemma_num]
				if obl and obl ~= "" and #obl > 0 then
					obl.label = "oblique"
					insert(data.inflections, obl)
				end
			end
		else
			local gen = decldata.forms["gen_" .. lemma_num]
			if (decldata.unattested["gen_" .. lemma_num]) then
				gen[1] = '*' .. gen[1]
				data.nolink = true
			end
			if gen and gen ~= "" and gen ~= "—" and #gen > 0 then
				if is_decl then
					-- Skip displaying the genitive for nouns that are only
					-- indeclinable. But we do display it for nouns like Abrahām
					-- and Ādām that can be either indeclinable or declined.
					gen.label = "genitive"
					insert(data.inflections, gen)
				end
			else
				insert(data.inflections, {label = "no genitive"})
				insert_if_not(categories, "Latin " .. decldata.pos .. " with no genitive singular")
			end
		end
	end

	derivation(data, decldata, "m", "masculine")
	derivation(data, decldata, "f", "feminine")
	derivation(data, decldata, "n", "neuter")
	derivation(data, decldata, "adj", "relational", "relational adjective")
	derivation(data, decldata, "dim", "diminutive")
	derivation(data, decldata, "aug", "augmentative")

	for _, cat in ipairs(decldata.categories) do
		insert_if_not(categories, cat)
	end

	for _, cat in ipairs(decldata.cat) do
		insert_if_not(categories, "Latin " .. cat)
	end
	
	return is_pn and decldata.pos == "nouns" and "proper nouns" or decldata.pos
end

pos_functions["nouns"] = function(def, args, data, infl_classes, title)
	return nouns("nouns", def, args, data, infl_classes, title)
end

pos_functions["proper nouns"] = function(def, args, data, infl_classes, title)
	return nouns("proper nouns", def, args, data, infl_classes, title)
end

pos_functions["suffixes-noun"] = function(def, args, data, infl_classes, title)
	return nouns("suffixes", def, args, data, infl_classes, title)
end

pos_functions["numerals-noun"] = function(def, args, data, infl_classes, title)
	return nouns("numerals", def, args, data, infl_classes, title)
end

function export.verb_title(title, typeinfo, lemma_forms)
	local conj = typeinfo.conj
	local irreg_processing = typeinfo.irreg
	local subtypes = typeinfo.subtypes

	local first_lemma = ""
	if #lemma_forms > 0 then
		first_lemma = require("Module:links").remove_links(lemma_forms[1])
	end

	if conj == "1st" then
		insert(title, "[[Appendix:Latin first conjugation|first conjugation]]")
	elseif conj == "2nd" then
		insert(title, "[[Appendix:Latin second conjugation|second conjugation]]")
	elseif conj == "3rd" then
		insert(title, "[[Appendix:Latin third conjugation|third conjugation]]")
	elseif conj == "3rd-io" then
		insert(title, ("[[Appendix:Latin third conjugation|third (%s variant) conjugation]]"):format(make_link(nil, "-iō", "term")))
	elseif conj == "3rd/4th" then
		insert(title, ("[[Appendix:Latin third conjugation|third (%s variant)]] / [[Appendix:Latin fourth conjugation|fourth conjugation]]"):format(make_link(nil, "-iō", "term")))
	elseif conj == "4th" then
		insert(title, "[[Appendix:Latin fourth conjugation|fourth conjugation]]")
	elseif conj == "irreg" then -- sum
		insert(title, "[[Appendix:Latin irregular verbs|irregular conjugation]]")
	end
	if subtypes.highlydef then -- āiō, inquam
		insert(title, "highly [[defective verb#English|defective]]")
	end
	if subtypes.suppl then -- sum, volō
		insert(title, "[[suppletive#English|suppletive]]")
	end
	if subtypes["3only"] then -- decet
		insert(title, "[[third person#English|third person]]-only")
	elseif subtypes.impers then -- decet, advesperāscit (also nopass)
		insert(title, "[[impersonal verb#English|impersonal]]")
	end
	if subtypes.depon then -- dēmōlior, calvor (also noperf)
		insert(title, "[[deponent#English|deponent]]")
	end
	if subtypes.perfaspres then -- meminī, ōdī
		insert(title, "no [[present tense#English|present]] stem")
		-- If semidepon is set, only the active forms are affected.
		local voice = subtypes.semidepon and "[[active voice#English|active]] " or ""
		insert(title, ("[[perfect tense#English|perfect]] %sforms have [[present tense#English|present]] %smeaning"):format(voice, voice))
		-- Clarify that deponency can only apply to the perfect.
		if subtypes.semidepon then -- ōdī
			insert(title, "[[deponent#English|deponent]] in the [[perfect tense#English|perfect]]")
		end
		if subtypes.optsemidepon then
			insert(title, "optionally [[deponent#English|deponent]] in the [[perfect tense#English|perfect]]")
		end
	else
		if subtypes.semidepon then -- fīdō, gaudeō
			insert(title, "[[semi-deponent#English|semi-deponent]]")
		end
		if subtypes.optsemidepon then -- audeō, placeō, soleō, pudeō
			insert(title, "optionally [[semi-deponent#English|semi-deponent]]")
		end
	end
	if subtypes.imponly then -- cedo, apage
		insert(title, "[[imperative mood#English|imperative]]-only")
	end
	if subtypes.nopass then -- coacēscō
		insert(title, "no [[passive voice#English|passive]]")
	end
	
	local stems = {}
	if subtypes.nopres then -- coepī; perfaspres already handles this above
		insert(stems, "[[present tense#English|present]]")
	end
	if subtypes.noperf then
		insert(stems, "[[perfect tense#English|perfect]]")
	end
	if subtypes.nosup or subtypes.supfutractvonly then
		insert(stems, "[[supine#English|supine]]")
	end
	if #stems > 0 then
		local extra = subtypes.supfutractvonly and " except in the [[future tense#English|future]] [[active voice#English|active]] [[participle#English|participle]]"
		insert(title, ("no %s stem%s%s"):format(serial_comma_join(stems, {conj = "or"}), #stems > 1 and "s" or "", extra or ""))
	end
	if subtypes.nofutr then -- soleō
		insert(title, "no [[future tense#English|future]]")
	end
	if subtypes.pass3only then -- praefundō
		insert(title, "[[third person#English|third person]]-only in the [[passive voice#English|passive]]")
	elseif subtypes.passimpers then -- abambulō
		local msg = "[[impersonal verb#English|impersonal]] in the [[passive voice#English|passive]]"
		if subtypes.passimpersold then -- possum
			msg = msg .. " in [[Old Latin]]"
		end
		insert(title, msg)
	end
	if subtypes.noimp then -- volō
		insert(title, "no [[imperative mood#English|imperative]]")
	end
	if irreg_processing and umatch(first_lemma, "d[īū]cō$") then -- dīcō
		insert(title, "[[Appendix:Latin irregular verbs|irregular]] short [[imperative mood#English|imperative]]")
	end
	if subtypes.nofutractvptc and not subtypes.nosup then -- fīō
		insert(title, "no [[future tense#English|future]] [[active voice#English|active]] [[participle#English|participle]]")
	end
	if not (subtypes.nopres or subtypes.perfaspres or subtypes.imponly) then
		if subtypes.noinf then -- inquam
			insert(title, "no [[infinitive#English|infinitive]]")
		end
		if subtypes.noger then -- libet
			insert(title, "no [[gerund#English|gerund]]")
		end
	end
	if subtypes.shorta then -- dō
		insert(title, ("[[Appendix:Latin irregular verbs|irregular]] short %s in most forms"):format(make_link(nil, "ă", "term")))
	end
	if irreg_processing then
		if first_lemma:match("edō$") then -- edō
			insert(title, "[[Appendix:Latin irregular verbs|irregular]] alternative forms")
		elseif first_lemma:match("fīō$") then -- fīō
			insert(title, "[[Appendix:Latin irregular verbs|irregular]] long " .. make_link(nil, "ī", "term"))
		end
	end
end

pos_functions["verbs"] = function(def, args, data, infl_classes, title)
	local m_la_verb = require("Module:la-verb")
	local def1, def2
	if def then
		def1, def2 = def:match("^(.-):(.*)$")
	end
	local conjdata, typeinfo = m_la_verb.make_data(args, true, def1, def2)
	local lemma_forms = conjdata.overriding_lemma
	if not lemma_forms or #lemma_forms == 0 then
		lemma_forms = m_la_verb.get_lemma_forms(conjdata, true)
	end
	data.heads = lemma_forms
	-- Since we always set data.heads to the lemma and specification of the lemma is mandatory in {{la-verb}}, there aren't
	-- really any redundant heads.
	data.no_redundant_head_cat = true
	data.id = conjdata.id
	
	local perf_only = false

	local function insert_inflection(infl, label)
		infl.label = label
		insert(data.inflections, infl)
	end

	local inf = m_la_verb.get_valid_forms(conjdata.forms["pres_actv_inf"])
	if #inf > 0 then
		insert_inflection(inf, "present infinitive")
	else
		inf = m_la_verb.get_valid_forms(conjdata.forms["perf_actv_inf"])
		if #inf > 0 then
			perf_only = true
			insert_inflection(inf, "perfect infinitive")
		end
	end

	if not perf_only then
		local perf = m_la_verb.get_valid_forms(conjdata.forms["1s_perf_actv_indc"])
		if #perf == 0 then
			perf = m_la_verb.get_valid_forms(conjdata.forms["3s_perf_actv_indc"])
		end
		if #perf > 0 then
			insert_inflection(perf, "perfect active")
		end
	end

	local subtypes = typeinfo.subtypes
	if not (subtypes.depon or subtypes.semidepon) then
		local sup = m_la_verb.get_valid_forms(conjdata.forms["acc_sup"])
		if #sup > 0 then
			insert_inflection(sup, "supine")
		else
			local fap = m_la_verb.get_valid_forms(conjdata.forms["futr_actv_ptc"])
			if #fap > 0 then
				insert_inflection(fap, "future active participle")
			end
		end
	end

	export.verb_title(title, typeinfo, lemma_forms)
end

pos_functions["suffixes-verb"] = pos_functions["verbs"]

local function attested_form(decldata, index)
	local form
	if (decldata.unattested[index]) then
		form = { { term = '*' .. decldata.forms[index][1], nolink = true } }
	else
		form = decldata.forms[index]
	end
	return form
end

local function degree_derivations(comp, sup, data)
	local inflections = data.inflections
	if not (comp or sup) then
		return
	elseif (not comp or comp == "-") and (not sup or sup == "-") then
		insert(inflections, {label = "not [[Appendix:Glossary#comparative|comparable]]"})
		insert_if_not(data.categories, "Latin uncomparable adverbs")
		return
	elseif comp == "-" then
		insert(inflections, {label = "no [[Appendix:Glossary#comparative|comparative]]"})
	elseif comp then
		insert(inflections, comp)
	end
	if sup == "-" then
		insert(inflections, {label = "no [[Appendix:Glossary#superlative|superlative]]"})
	elseif sup then
		insert(inflections, sup)
	end
end

local function adjectives(pos, def, args, data, infl_classes)
	local is_num = pos == "numerals"
	local decldata = require("Module:la-nominal").do_generate_adj_forms(args, pos, "headword", nil, def)
	local lemma = decldata.overriding_lemma
	-- TODO: modify the headword line if the slot isn't the expected one.
	if #lemma == 0 then
		for slot in require("Module:la-nominal").iter_potential_adj_lemma_slots() do
			local potential_lemma = decldata.forms["linked_" .. slot]
			if not form_is_empty(potential_lemma) then
				if decldata.unattested[slot] then
					potential_lemma = "*" .. potential_lemma
				end
				lemma = potential_lemma
				break
			end
		end
	end

	data.heads = lemma
	-- Since we always set data.heads to the lemma and specification of the lemma is mandatory in {{la-noun}}, there aren't
	-- really any redundant heads.
	data.no_redundant_head_cat = true
	data.id = decldata.id

	local categories = data.categories
	if is_num then
		process_num_type(decldata.num_type, categories)
	end

	if decldata.num == "pl" then
		insert_if_not(categories, "Latin plural-only " .. decldata.pos)
	end

	if decldata.indecl then
		insert(data.inflections, {label = glossary_link("indeclinable")})

		if decldata.pos == "participles" then
			local lemma1 = lemma[1]
			if lemma1:sub(-4) == "ndum" then
				insert_if_not(categories, "Latin future passive participles")
			elseif lemma1:match("um$") then
				insert_if_not(categories, "Latin perfect participles")
			end
		end
	else
		local lemma_num = decldata.num == "pl" and "pl" or "sg"
		local masc = decldata.forms["nom_" .. lemma_num .. "_m"]
		local fem = attested_form(decldata, "nom_" .. lemma_num .. "_f")
		local neut = attested_form(decldata, "nom_" .. lemma_num .. "_n")
		local gen = attested_form(decldata, "gen_" .. lemma_num .. "_m")
		local acc = attested_form(decldata, "acc_" .. lemma_num .. "_m")
	
		if decldata.pos == "participles" then
			local masc1 = masc[1]
			if masc1:sub(-5) == "ūrus" then
				insert_if_not(categories, "Latin future active participles")
			elseif masc1:sub(-4) == "ndus" then
				-- FIXME, should rename to "Latin gerundives")
				insert_if_not(categories, "Latin future passive participles")
			else
				local masc1_final2 = masc1:sub(-2)
				if masc1_final2 == "us" then
					insert_if_not(categories, "Latin perfect participles")
				elseif masc1_final2 == "ns" then
					insert_if_not(categories, "Latin present participles")
				else
					error("Unrecognized participle ending: " .. masc1)
				end
			end
		end
	
		-- We display the inflections in three different ways to mimic the
		-- old way of doing things:
		--
		-- 1. If masc and fem are different, show masc, fem and neut.
		-- 2. Otherwise, if masc and neut are different, show masc and neut.
		-- 3. Otherwise, show masc nominative and masc genitive.
		if not form_is_empty(fem) and not deep_equals(masc, fem) then
			fem.label = "feminine"
			insert(data.inflections, fem)
			if not form_is_empty(neut) then
				neut.label = "neuter"
				insert(data.inflections, neut)
			end
		elseif not form_is_empty(neut) and not deep_equals(masc, neut) then
			neut.label = "neuter"
			insert(data.inflections, neut)
		elseif not form_is_empty(gen) then
			gen.label = "genitive"
			insert(data.inflections, gen)
		elseif not form_is_empty(acc) then
			acc.label = "accusative"
			insert(data.inflections, acc)
		end

		insert(infl_classes, decldata.title)
	end

	local comp = normalize_derivation(decldata.comp, "comparative")
	local sup = normalize_derivation(decldata.sup, "superlative")
	degree_derivations(comp, sup, data)

	derivation(data, decldata, "adv", "adverb")

	for _, cat in ipairs(decldata.categories) do
		insert_if_not(categories, cat)
	end

	for _, cat in ipairs(decldata.cat) do
		insert_if_not(categories, "Latin " .. cat)
	end
	
	return decldata.pos
end

pos_functions["adjectives"] = function(def, args, data, infl_classes, title)
	return adjectives("adjectives", def, args, data, infl_classes, title)
end

pos_functions["participles"] = function(def, args, data, infl_classes, title)
	return adjectives("participles", def, args, data, infl_classes, title)
end

pos_functions["determiners"] = function(def, args, data, infl_classes, title)
	return adjectives("determiners", def, args, data, infl_classes, title)
end

pos_functions["pronouns"] = function(def, args, data, infl_classes, title)
	return adjectives("pronouns", def, args, data, infl_classes, title)
end

pos_functions["suffixes-adjective"] = function(def, args, data, infl_classes, title)
	return adjectives("suffixes", def, args, data, infl_classes, title)
end

pos_functions["numerals-adjective"] = function(def, args, data, infl_classes, title)
	return adjectives("numerals", def, args, data, infl_classes, title)
end

pos_functions["adverbs"] = function(def, args, data)
	local sublist = {sublist = "/"}
	args = require("Module:parameters").process(args, {
		[1] = {alias_of = "head", list = false},
		[2] = {alias_of = "comp"},
		[3] = {alias_of = "sup"},
		["head"] = {list = true, required = true},
		["comp"] = sublist,
		["sup"] = sublist,
		["adj"] = sublist,
		["id"] = true,
	})
	data.heads = args.head
	data.no_redundant_head_cat = true -- since head= is required
	data.id = args.id
	local comp, sup = args.comp, args.sup
	local irreg = false

	if comp then
		if comp[1] == "-" then
			comp = "-"
		elseif comp[1] == nil then
			comp = nil
		else
			comp.label = glossary_link("comparative")
			comp = args.comp
			irreg = true
		end
	end
	if sup then
		if sup[1] == "-" then
			sup = "-"
		elseif sup[1] == nil then
			sup = nil
		else
			sup.label = glossary_link("superlative")
			sup = args.sup
			irreg = true
		end
	end

	local categories = data.categories
	if irreg then
		insert_if_not(categories, "Latin irregular adverbs")
	end

	if not (comp or sup) then
		local default_comp = {label = glossary_link("comparative")}
		local default_sup = {label = glossary_link("superlative")}
		for _, head in ipairs(args.head) do
			local stem = nil
			for _, suff in ipairs{"iter", "nter", "ter", "er", "iē", "ē", "rā", "im", "ō"} do
				stem = head:match("(.*)" .. pattern_escape(suff) .. "$")
				comp_suff, sup_suff = "ius", "issimē"
				if stem ~= nil then
					if suff == "nter" then
						stem = stem .. "nt"
					elseif suff == "rā" then
						comp_suff = "er" .. comp_suff
						sup_suff = "imē"
					end
					insert(default_comp, stem .. comp_suff)
					insert(default_sup, stem .. sup_suff)
					break
				end
			end
			if not stem then
				error("Unrecognized adverb type, recognized types are “-ē”, “-er”, “-ter”, “-iter”, “-im”, or “-ō”, “-ra” or specify irregular forms or “-” if incomparable.")
			end
		end
		comp = comp or default_comp
		sup = sup or default_sup
	end

	degree_derivations(comp, sup, data)
	derivation(data, args, "adj", "adjective")
end

pos_functions["suffixes-adverb"] = pos_functions["adverbs"]

local function get_forms(forms)
	if #forms == 0 then
		return nil
	end
	local i, attested = 1, false
	while true do
		local form = forms[i]
		if form == nil then
			return forms, attested
		elseif form == "-" then
			remove(forms, i)
		else
			if not (attested or get_plaintext(form):sub(1, 1) == "*") then
				attested = true
			end
			i = i + 1
		end
	end
end

local function degree(pos, deg, pos_func, other_pos, other_pos_label, other_deg, other_deg_label, args, data, infl_classes)
	local list = {list = true}
	args = require("Module:parameters").process(args, {
		[1] = {alias_of = "head", list = false},
		["head"] = list,
		["positive"] = list,
		[other_pos] = {sublist = "/"},
		[other_deg] = list,
		["id"] = true,
	})
	data.no_redundant_head_cat = #args.head == 0
	-- Set default manually so we can tell whether the user specified head=.
	if #args.head == 0 then
		args.head = {PAGENAME}
	end
	data.heads = args.head
	data.id = args.id

	insert(data.inflections, {label = deg})
	
	if pos_func then
		pos_func(args, data, infl_classes)
	end

	local positive, positive_attested = get_forms(args.positive)

	if positive then
		if not positive_attested then
			insert(data.categories, "Latin " .. deg .. "-only " .. pos)
		end
		if #positive > 0 then
			args.positive.label = "positive"
			insert(data.inflections, args.positive)
		else
			insert(data.inflections, {label = "no positive form"})
		end
	end

	local other = get_forms(args[other_deg])
	if other then
		if #other > 0 then
			args[other_deg].label = other_deg_label
			insert(data.inflections, args[other_deg])
		else
			insert(data.inflections, {label = "no " .. other_deg_label .. " form"})
		end
	end
	
	derivation(data, args, other_pos, other_pos_label)

	-- If a lemma, return the primary part of speech ("adjectives" or
	-- "adverbs"), so that the term is categorized in "Latin adjectives" or
	-- "Latin adverbs". Otherwise, return nothing, so that the term goes in the
	-- relevant non-lemma category (e.g. "Latin comparative adjectives"), and
	-- into "Latin non-lemma forms".
	if positive and not positive_attested then
		return pos
	end
end

local function comp_adj(args, data, infl_classes)
	insert(infl_classes, "[[Appendix:Latin third declension|third declension]]")
	local n = {label = "neuter"}
	for _, head in ipairs(args.head) do
		local neuter = head:gsub("or$", "us")
		insert(n, neuter)
	end
	insert(data.inflections, n)
end

pos_functions["comparative adjectives"] = function(def, args, data, infl_classes, title)
	return degree("adjectives", "comparative", comp_adj, "adv", "adverb", "sup", "superlative", args, data, infl_classes)
end

pos_functions["comparative adverbs"] = function(def, args, data, infl_classes, title)
	return degree("adverbs", "comparative", nil, "adj", "adjective", "sup", "superlative", args, data, infl_classes)
end

local function sup_adj(args, data, infl_classes)
	insert(infl_classes, "[[Appendix:Latin first declension|first]]")
	insert(infl_classes, "[[Appendix:Latin second declension|second declension]]")
	local f, n = {label = "feminine"}, {label = "neuter"}
	for _, head in ipairs(args.head) do
		local stem = head:gsub("us$", "")
		insert(f, stem .. "a")
		insert(n, stem .. "um")
	end
	insert(data.inflections, f)
	insert(data.inflections, n)
end

pos_functions["superlative adjectives"] = function(def, args, data, infl_classes, title)
	return degree("adjectives", "superlative", sup_adj, "adv", "adverb", "comp", "comparative", args, data, infl_classes)
end

pos_functions["superlative adverbs"] = function(def, args, data, infl_classes, title)
	return degree("adverbs", "superlative", nil, "adj", "adjective", "comp", "comparative", args, data, infl_classes)
end

local function adpositions(pos, def, args, data, infl_classes, title, postscript)
	local cases = invert(require("Module:la-utilities").cases)

	args = require("Module:parameters").process(args, {
		[1] = {alias_of = "head", list = false},
		[2] = {list = true, set = keys_to_list(cases)},
		["head"] = {list = true, required = true},
		["id"] = true,
	})

	-- Case names are supplied in numbered arguments, optionally preceded by
	-- headwords.
	cases = args[2]
	for i = 1, #cases do
		for j = i + 1, #cases do
			if cases[i] == cases[j] then
				error("Duplicate case")
			end
		end
		local case = cases[i]
		local appendix_link = glossary_link(case)
		if i == 1 then
			appendix_link = "+ " .. appendix_link
		end
		insert(postscript, appendix_link)
		insert_if_not(data.categories, "Latin " .. case .. " " .. pos)
	end

	data.heads = args.head
	data.no_redundant_head_cat = true -- since head= is required
	data.id = args.id
end

pos_functions["prepositions"] = function(...)
	return adpositions("prepositions", ...)
end

pos_functions["postpositions"] = function(...)
	return adpositions("postpositions", ...)
end

pos_functions["gerunds"] = function(def, args, data)
	args = require("Module:parameters").process(args, {
		[1] = {required = true, default = "labōrandum"}, -- headword
		[2] = true, -- gerundive
	})

	data.heads = {args[1]}
	data.no_redundant_head_cat = true -- since 1= is required and goes into data.heads
	insert(data.inflections, {label = "[[Appendix:Glossary#accusative|accusative]]"})
	local stem = args[1]:match("^(.*)um$")
	if not stem then
		error("Unrecognized gerund ending: " .. stem)
	end
	if args[2] == "-" then
		insert(data.inflections, {label = "no [[Appendix:Glossary#gerundive|gerundive]]"})
	else
		insert(data.inflections, {[1] = args[2] or stem .. "us", label = "[[Appendix:Glossary#gerundive|gerundive]]"})
	end
end

local function non_lemma_forms(def, args, data)
	args = require("Module:parameters").process(args, {
		[1] = {required = true, default = def}, -- headword or cases
		["head"] = {list = true, require_index = true},
		["g"] = {list = true},
		["id"] = true,
	})

	local heads = {args[1]}
	for _, head in ipairs(args.head) do
		insert(heads, head)
	end
	data.heads = heads
	data.no_redundant_head_cat = true -- since 1= is required and goes into data.heads
	data.genders = args.g
	data.id = args.id
end

pos_functions["noun forms"] = non_lemma_forms
pos_functions["proper noun forms"] = non_lemma_forms
pos_functions["pronoun forms"] = non_lemma_forms
pos_functions["verb forms"] = non_lemma_forms
pos_functions["gerund forms"] = non_lemma_forms
pos_functions["adjective forms"] = non_lemma_forms
pos_functions["participle forms"] = non_lemma_forms
pos_functions["determiner forms"] = non_lemma_forms
pos_functions["numeral forms"] = non_lemma_forms
pos_functions["suffix forms"] = non_lemma_forms

return export