Module:ca-headword

From Wiktionary, the free dictionary
Jump to navigation Jump to search

This module is used for Catalan headword-line templates. This module currently implements {{ca-noun}}, {{ca-verb}}, {{ca-adj}}, {{ca-adv}}, {{ca-pp}} and several others; see the documentation of those templates for more information.

The module is always invoked the same way, by passing a single parameter to the "show" function. This parameter is the name of the part of speech, but in plural (examples given are for nouns, adverbs and past participles respectively):

{{#invoke:ca-headword|show|nouns}}
{{#invoke:ca-headword|show|adverbs}}
{{#invoke:ca-headword|show|past participles}}

The template will, by default, accept the following parameters (specific parts of speech may accept or require others):

|head=
Override the headword display, used to add links to individual words in a multiword term.
|nolinkhead=1
Don't link individual words in the headword of a multiword term. Useful for foreign terms like enfant terrible and sex symbol where the expression functions as a whole in Catalan but the individual parts are not Catalan words.
|splithyph=1
Indicate that automatic splitting and linking of words should split on hyphens in multiword expressions with spaces in them (splitting on hyphens is automatic if there are no spaces in the term). See #Autosplitting below for more information.
|pagename=
Override the page name used to compute default values of various sorts. Useful when testing, for documentation pages, etc.

There is no parameter for the sort key, because this is not necessary. The sort key is automatically generated according to the normal alphabetical ordering in Catalan.

Autosplitting

All templates using this module use an intelligent autosplitting algorithm to link portions of multipart and multiword expressions, as follows:

  • The module will automatically split and link distinct space-separated words, similarly to {{head}}; hence, tot i així will be linked as [[tot]] [[i]] [[així]].
  • It also splits on apostrophes in the middle of words and includes the apostrophe in the preceding portion. Hence, a cau d'orella (whispering) will be linked as [[a]] [[cau]] [[d']][[orella]].
  • In addition, parts of hyphenated compounds in single-word expressions will be linked individually, e.g. boca-orella (word of mouth) will be linked as [[boca]]-[[orella]]. Hyphenated compound words will not be split if they occur in multiword expressions unless |splithyph=1 is specified, since the compound itself might be a valid word. An example where |splithyph=1 should be used is a corre-cuita (in a hurry); when used with this parameter, it will be split as [[a]] [[corre]]-[[cuita]]

Suffix handling

If the term begins with a hyphen (-), it is assumed to be a suffix rather than a base form, and is categorized into Category:Catalan suffixes and Category:Catalan POS-forming suffixes rather than Category:Catalan POSs (e.g. Category:Catalan noun-forming suffixes rather than Category:Catalan nouns).


local export = {}
local pos_functions = {}

local force_cat = false -- for testing; if true, categories appear in non-mainspace pages

local m_table = require("Module:table")
local com = require("Module:ca-common")
local inflection_utilities_module = "Module:inflection utilities"
local parse_utilities_module = "Module:parse utilities"
local romut_module = "Module:romance utilities"
local ca_verb_module = "Module:ca-verb"
local ca_IPA_module = "Module:ca-IPA"

local lang = require("Module:languages").getByCode("ca")
local langname = lang:getCanonicalName()

local list_to_text = mw.text.listToText
local rfind = mw.ustring.find
local rmatch = mw.ustring.match
local rsplit = mw.text.split
local usub = mw.ustring.sub
local rsub = com.rsub

local function track(page)
	require("Module:debug/track")("ca-headword/" .. page)
	return true
end

-----------------------------------------------------------------------------------------
--                                    Main entry point                                 --
-----------------------------------------------------------------------------------------

-- 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},
		["id"] = {},
		["splithyph"] = {type = "boolean"},
		["nolinkhead"] = {type = "boolean"},
		["json"] = {type = "boolean"},
		["pagename"] = {}, -- for testing
	}
	
	if pos_functions[poscat] then
		for key, val in pairs(pos_functions[poscat].params) do
			params[key] = val
		end
	end
	
	local args = require("Module:parameters").process(frame:getParent().args, params)
	local pagename = args.pagename or mw.loadData("Module:headword/data").pagename

	local user_specified_heads = args.head
	local heads = user_specified_heads
	if args.nolinkhead then
		if #heads == 0 then
			heads = {pagename}
		end
	else
		local romut = require(romut_module)
		local auto_linked_head = romut.add_links_to_multiword_term(pagename, args.splithyph)
		if #heads == 0 then
			heads = {auto_linked_head}
		else
			for i, head in ipairs(heads) do
				if head:find("^~") then
					head = romut.apply_link_modifiers(auto_linked_head, usub(head, 2))
					heads[i] = head
				end
				if head == auto_linked_head then
					track("redundant-head")
				end
			end
		end
	end

	local data = {
		lang = lang,
		pos_category = poscat,
		categories = {},
		heads = heads,
		user_specified_heads = user_specified_heads,
		no_redundant_head_cat = #user_specified_heads == 0,
		genders = {},
		inflections = {},
		pagename = pagename,
		id = args.id,
		force_cat_output = force_cat,
	}

	local is_suffix = false
	if pagename:find("^%-") and poscat ~= "suffix forms" then
		is_suffix = true
		data.pos_category = "suffixes"
		local singular_poscat = require("Module:string utilities").singularize(poscat)
		table.insert(data.categories, langname .. " " .. singular_poscat .. "-forming suffixes")
		table.insert(data.inflections, {label = singular_poscat .. "-forming suffix"})
	end

	if pos_functions[poscat] then
		pos_functions[poscat].func(args, data, is_suffix)
	end

	if args.json then
		return require("Module:JSON").toJSON(data)
	end

	local post_note = data.post_note and "; " .. data.post_note or ""
	return require("Module:headword").full_headword(data) .. post_note
end


-----------------------------------------------------------------------------------------
--                                     Utility functions                               --
-----------------------------------------------------------------------------------------

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

local function fetch_qualifiers(qual, existing)
	if not qual then
		return existing
	end
	if not existing then
		return {qual}
	end
	local retval = {}
	for _, e in ipairs(existing) do
		table.insert(retval, e)
	end
	table.insert(retval, qual)
	return retval
end


local function process_terms_with_qualifiers(terms, quals)
	local infls = {}
	for i, term in ipairs(terms) do
		table.insert(infls, {term = term, q = fetch_qualifiers(quals[i])})
	end
	return infls
end


local function replace_hash_with_lemma(term, lemma)
	-- If there is a % sign in the lemma, we have to replace it with %% so it doesn't get interpreted as a capture replace
	-- expression.
	lemma = lemma:gsub("%%", "%%%%")
	-- Assign to a variable to discard second return value.
	term = term:gsub("#", lemma)
	return term
end

local function check_all_missing(data, forms, plpos)
	for _, form in ipairs(forms) do
		if type(form) == "table" then
			form = form.term
		end
		if form then
			local title = mw.title.new(form)
			if title and not title.exists then
				table.insert(data.categories, langname .. " " .. plpos .. " with red links in their headword lines")
			end
		end
	end
end

local function insert_ancillary_inflection(data, forms, quals, label, plpos)
	if forms and #forms > 0 then
		local terms = process_terms_with_qualifiers(forms, quals)
		check_all_missing(data, terms, plpos)
		terms.label = label
		table.insert(data.inflections, terms)
	end
end


-----------------------------------------------------------------------------------------
--                                       Adjectives                                    --
-----------------------------------------------------------------------------------------

local function do_adjective(args, data, pos, is_suffix, is_superlative)
	local feminines = {}
	local masculine_plurals = {}
	local feminine_plurals = {}
	if is_suffix then
		pos = "suffix"
	end
	local plpos = require("Module:string utilities").pluralize(pos)

	if not is_suffix then
		data.pos_category = plpos
	end

	if args.sp then
		local romut = require(romut_module)
		if not romut.allowed_special_indicators[args.sp] then
			local indicators = {}
			for indic, _ in pairs(romut.allowed_special_indicators) do
				table.insert(indicators, "'" .. indic .. "'")
			end
			table.sort(indicators)
			error("Special inflection indicator beginning can only be " ..
				list_to_text(indicators) .. ": " .. args.sp)
		end
	end

	local lemma = data.pagename

	local function insert_inflection(forms, label, accel)
		if #forms > 0 then
			if forms[1].term == "-" then
				table.insert(data.inflections, {label = "no " .. label})
			else
				forms.label = label
				forms.accel = {form = accel}
				table.insert(data.inflections, forms)
			end
		end
	end

	if args.f[1] == "ind" or args.f[1] == "inv" then
		-- invariable adjective
		table.insert(data.inflections, {label = glossary_link("invariable")})
		table.insert(data.categories, langname .. " indeclinable " .. plpos)
		if args.sp or #args.f > 1 or #args.pl > 0 or #args.mpl > 0 or #args.fpl > 0 then
			error("Can't specify inflections with an invariable " .. pos)
		end
	elseif args.fonly then
		-- feminine-only
		if #args.f > 0 then
			error("Can't specify explicit feminines with feminine-only " .. pos)
		end
		if #args.pl > 0 then
			error("Can't specify explicit plurals with feminine-only " .. pos .. ", use fpl=")
		end
		if #args.mpl > 0 then
			error("Can't specify explicit masculine plurals with feminine-only " .. pos)
		end
		local argsfpl = args.fpl
		if #argsfpl == 0 then
			argsfpl = {"+"}
		end
		for i, fpl in ipairs(argsfpl) do
			local quals = fetch_qualifiers(args.fpl_qual[i])
			if fpl == "+" then
				-- Generate default feminine plural.
				local defpls = com.make_plural(lemma, "f", args.sp)
				if not defpls then
					error("Unable to generate default plural of '" .. lemma .. "'")
				end
				for _, defpl in ipairs(defpls) do
					table.insert(feminine_plurals, {term = defpl, q = quals})
				end
			else
				table.insert(feminine_plural, {term = replace_hash_with_lemma(fpl, lemma), q = quals})
			end
		end

		check_all_missing(data, feminine_plurals, plpos)

		table.insert(data.inflections, {label = "feminine-only"})
		insert_inflection(feminine_plurals, "feminine plural", "f|p")
	else
		-- Gather feminines.
		local argsf = args.f
		if #argsf == 0 then
			argsf = {"+"}
		end

		for i, f in ipairs(argsf) do
			if f == "mf" then
				f = lemma
			elseif f == "+" then
				-- Generate default feminine.
				f = com.make_feminine(lemma, args.sp)
			else
				f = replace_hash_with_lemma(f, lemma)
			end
			table.insert(feminines, {term = f, q = fetch_qualifiers(args.f_qual[i])})
		end

		local fem_like_lemma = #feminines == 1 and feminines[1].term == lemma and not feminines[1].q
		if fem_like_lemma then
			table.insert(data.categories, langname .. " epicene " .. plpos)
		end

		local argsmpl = args.mpl
		local argsfpl = args.fpl
		if #args.pl > 0 then
			if #argsmpl > 0 or #argsfpl > 0 or args.mpl_qual.maxindex > 0 or args.fpl_qual.maxindex > 0 then
				error("Can't specify both pl= and mpl=/fpl=")
			end
			argsmpl = args.pl
			args.mpl_qual = args.pl_qual
			argsfpl = args.pl
			args.fpl_qual = args.pl_qual
		end
		if #argsmpl == 0 then
			argsmpl = {"+"}
		end
		if #argsfpl == 0 then
			argsfpl = {"+"}
		end

		for i, mpl in ipairs(argsmpl) do
			local quals = fetch_qualifiers(args.mpl_qual[i])
			if mpl == "+" then
				-- Generate default masculine plural.
				local defpls

				-- First, some special hacks based on the feminine singular.
				if not fem_like_lemma and not args.sp and not lemma:find(" ") then
					for _, f in ipairs(feminines) do
						if f.term:find("ssa$") then
							-- If the feminine ends in -ssa, assume that the -ss- is also in the
							-- masculine plural form
							defpls = {rsub(f.term, "a$", "os")}
							break
						elseif f.term == lemma .. "na" then
							defpls = {lemma .. "ns"}
							break
						elseif lemma:find("ig$") and f.term:find("ja$") then
							-- Adjectives in -ig have two masculine plural forms, one derived from
							-- the m.sg. and the other derived from the f.sg.
							defpls = {lemma .. "s", rsub(f.term, "ja$", "jos")}
							break
						end
					end
				end

				defpls = defpls or com.make_plural(lemma, "m", args.sp)
				if not defpls then
					error("Unable to generate default plural of '" .. lemma .. "'")
				end
				for _, defpl in ipairs(defpls) do
					table.insert(masculine_plurals, {term = defpl, q = quals})
				end
			else
				table.insert(masculine_plurals, {term = replace_hash_with_lemma(mpl, lemma), q = quals})
			end
		end

		for i, fpl in ipairs(argsfpl) do
			if fpl == "+" then
				-- First, some special hacks based on the feminine singular.
				if fem_like_lemma and not args.sp and not lemma:find(" ") and lemma:find("[çx]$") then
					-- Adjectives ending in -ç or -x behave as mf-type in the singular, but
					-- regular type in the plural.
					local defpls = com.make_plural(lemma .. "a", "f")
					if not defpls then
						error("Unable to generate default plural of '" .. lemma .. "a'")
					end
					local fquals = fetch_qualifiers(args.fpl_qual[i])
					for _, defpl in ipairs(defpls) do
						table.insert(feminine_plurals, {term = defpl, q = fquals})
					end
				else
					for _, f in ipairs(feminines) do
						-- Generate default feminine plural; f is a table.
						local defpls = com.make_plural(f.term, "f", args.sp)
						if not defpls then
							error("Unable to generate default plural of '" .. f.term .. "'")
						end
						local fquals = fetch_qualifiers(args.fpl_qual[i], f.q)
						for _, defpl in ipairs(defpls) do
							table.insert(feminine_plurals, {term = defpl, q = fquals})
						end
					end
				end
			else
				fpl = replace_hash_with_lemma(fpl, lemma)
				table.insert(feminine_plurals, {term = fpl, q = fetch_qualifiers(args.fpl_qual[i])})
			end
		end

		check_all_missing(data, feminines, plpos)
		check_all_missing(data, masculine_plurals, plpos)
		check_all_missing(data, feminine_plurals, plpos)

		local fem_pl_like_masc_pl = #masculine_plurals > 0 and #feminine_plurals > 0 and
			m_table.deepEquals(masculine_plurals, feminine_plurals)
		local masc_pl_like_lemma = #masculine_plurals == 1 and masculine_plurals[1].term == lemma and
			not masculine_plurals[1].q
		if fem_like_lemma and fem_pl_like_masc_pl and masc_pl_like_lemma then
			-- actually invariable
			table.insert(data.inflections, {label = glossary_link("invariable")})
			table.insert(data.categories, langname .. " indeclinable " .. plpos)
		else
			-- Make sure there are feminines given and not same as lemma.
			if not fem_like_lemma then
				insert_inflection(feminines, "feminine", "f|s")
			else
				data.genders = {"mf"}
			end

			if fem_pl_like_masc_pl then
				insert_inflection(masculine_plurals, "masculine and feminine plural", "p")
			else
				insert_inflection(masculine_plurals, "masculine plural", "m|p")
				insert_inflection(feminine_plurals, "feminine plural", "f|p")
			end
		end
	end

	if args.irreg and is_superlative then
		table.insert(data.categories, langname .. " irregular superlative " .. plpos)
	end
end

local function get_adjective_params(adjtype)
	local params = {
		["sp"] = {}, -- special indicator: "first", "first-last", etc.
		["f"] = {list = true}, --feminine form(s)
		[1] = {alias_of = "f"},
		["f_qual"] = {list = "f\1_qual", allow_holes = true},
		["pl"] = {list = true}, --plural override(s)
		["pl_qual"] = {list = "pl\1_qual", allow_holes = true},
		["mpl"] = {list = true}, --masculine plural override(s)
		["mpl_qual"] = {list = "mpl\1_qual", allow_holes = true},
		["fpl"] = {list = true}, --feminine plural override(s)
		["fpl_qual"] = {list = "fpl\1_qual", allow_holes = true},
	}
	if adjtype == "base" then
		params["comp"] = {list = true} --comparative(s)
		params["comp_qual"] = {list = "comp\1_qual", allow_holes = true}
		params["sup"] = {list = true} --superlative(s)
		params["sup_qual"] = {list = "sup\1_qual", allow_holes = true}
		params["dim"] = {list = true} --diminutive(s)
		params["dim_qual"] = {list = "dim\1_qual", allow_holes = true}
		params["aug"] = {list = true} --augmentative(s)
		params["aug_qual"] = {list = "aug\1_qual", allow_holes = true}
		params["fonly"] = {type = "boolean"} -- feminine only
		params["hascomp"] = {} -- has comparative
	end
	if adjtype == "sup" then
		params["irreg"] = {type = "boolean"}
	end
	return params
end

-- Display additional inflection information for an adjective
pos_functions["adjectives"] = {
	params = get_adjective_params("base"),
	func = function(args, data, is_suffix)
		do_adjective(args, data, "adjective", is_suffix)
	end,
}

pos_functions["past participles"] = {
	params = get_adjective_params("part"),
	func = function(args, data, is_suffix)
		do_adjective(args, data, "participle", is_suffix)
		data.pos_category = "past participles"
	end,
}

pos_functions["determiners"] = {
	params = get_adjective_params("det"),
	func = function(args, data, is_suffix)
		do_adjective(args, data, "determiner", is_suffix)
	end,
}

pos_functions["pronouns"] = {
	params = get_adjective_params("pron"),
	func = function(args, data, is_suffix)
		do_adjective(args, data, "pronoun", is_suffix)
	end,
}

-----------------------------------------------------------------------------------------
--                                          Nouns                                      --
-----------------------------------------------------------------------------------------

local allowed_genders = m_table.listToSet(
	{"m", "f", "mf", "mfbysense", "n", "m-p", "f-p", "mf-p", "mfbysense-p", "n-p", "?", "?-p"}
)


local function process_genders(data, genders, g_qual)
	for i, g in ipairs(genders) do
		if not allowed_genders[g] then
			error("Unrecognized gender: " .. g)
		end
		if g_qual[i] then
			table.insert(data.genders, {spec = g, qualifiers = {g_qual[i]}})
		else
			table.insert(data.genders, g)
		end
	end
end

local function do_noun(args, data, pos, is_suffix, is_proper)
	local is_plurale_tantum = false
	local has_singular = false
	if is_suffix then
		pos = "suffix"
	end
	local plpos = require("Module:string utilities").pluralize(pos)

	data.genders = {}
	local saw_m = false
	local saw_f = false
	local gender_for_default_plural, gender_for_irreg_ending
	process_genders(data, args[1], args.g_qual)
	-- Check for specific genders and pluralia tantum.
	for _, g in ipairs(args[1]) do
		if g:find("-p$") then
			is_plurale_tantum = true
		else
			has_singular = true
			if g == "m" or g == "mf" or g == "mfbysense" then
				saw_m = true
			end
			if g == "f" or g == "mf" or g == "mfbysense" then
				saw_f = true
			end
		end
	end
	if saw_m and saw_f then
		gender_for_default_plural = "m"
		gender_for_irreg_ending = "mf"
	elseif saw_f then
		gender_for_default_plural = "f"
		gender_for_irreg_ending = "f"
	else
		gender_for_default_plural = "m"
		gender_for_irreg_ending = "m"
	end

	local lemma = data.pagename

	local function insert_inflection(list, term, accel, qualifiers, no_inv)
		local infl = {q = qualifiers, accel = accel}
		--if term == lemma and not no_inv then
		--	infl.label = glossary_link("invariable")
		--else
			infl.term = term
		--end
		infl.term_for_further_inflection = term
		table.insert(list, infl)
	end

	-- Plural
	local plurals = {}
	local args_mpl = args.mpl
	local args_fpl = args.fpl
	local args_pl = args[2]

	if is_plurale_tantum and not has_singular then
		if #args_pl > 0 then
			error("Can't specify plurals of plurale tantum " .. pos)
		end
		table.insert(data.inflections, {label = glossary_link("plural only")})
	else
		if is_plurale_tantum then
			-- both singular and plural
			table.insert(data.inflections, {label = "sometimes " .. glossary_link("plural only") .. ", in variation"})
		end
		-- If no plurals, use the default plural if not a proper noun.
		if #args_pl == 0 and not is_proper then
			args_pl = {"+"}
		end
		-- If only ~ given (countable and uncountable), add the default plural after it.
		if #args_pl == 1 and args_pl[1] == "~" then
			args_pl = {"~", "+"}
		end
		-- Gather plurals, handling requests for default plurals
		for i, pl in ipairs(args_pl) do
			local function insert_pl(term)
				local quals = fetch_qualifiers(args.pl_qual[i])
				if term == lemma and i == 1 and #args_pl == 1 then
					table.insert(data.inflections, {label = glossary_link("invariable"), q = quals})
					table.insert(data.categories, langname .. " indeclinable " .. plpos)
				else
					insert_inflection(plurals, term, nil, quals)
				end
				table.insert(data.categories, langname .. " countable " .. plpos)
			end
			local function make_plural_and_insert(form, special)
				local pls = com.make_plural(lemma, gender_for_default_plural, special)
				if pls then
					for _, pl in ipairs(pls) do
						insert_pl(pl)
					end
				end
			end

			if pl == "+" then
				make_plural_and_insert(lemma)
			elseif pl:find("^%+") then
				pl = require(romut_module).get_special_indicator(pl)
				make_plural_and_insert(lemma, pl)
			elseif pl == "?" or pl == "!" then
				if i > 1 or #args_pl > 1 then
					error("Can't specify ? or ! with other plurals")
				end
				if pl == "?" then
					-- Plural is unknown
					-- Better not to display anything
					-- table.insert(data.inflections, {label = "plural unknown or uncertain"})
					table.insert(data.categories, langname .. " " .. plpos .. " with unknown or uncertain plurals")
				else
					-- Plural is not attested
					table.insert(data.inflections, {label = "plural not attested"})
					table.insert(data.categories, langname .. " " .. plpos .. " with unattested plurals")
				end
			elseif pl == "-" then
				if i > 1 then
					error("Plural specifier - must be first")
				end
				-- Uncountable noun; may occasionally have a plural
				table.insert(data.categories, langname .. " uncountable " .. plpos)

				-- If plural forms were given explicitly, then show "usually"
				if #args_pl > 1 then
					table.insert(data.inflections, {label = "usually " .. glossary_link("uncountable")})
					table.insert(data.categories, langname .. " countable " .. plpos)
				else
					table.insert(data.inflections, {label = glossary_link("uncountable")})
				end
			elseif pl == "~" then
				if i > 1 then
					error("Plural specifier ~ must be first")
				end
				-- Countable and uncountable noun; will have a plural
				table.insert(data.categories, langname .. " countable " .. plpos)
				table.insert(data.categories, langname .. " uncountable " .. plpos)
				table.insert(data.inflections, {label = glossary_link("countable") .. " and " .. glossary_link("uncountable")})
			else
				insert_pl(replace_hash_with_lemma(pl, lemma))
			end
		end
	end

	if #plurals > 1 then
		table.insert(data.categories, langname .. " " .. plpos .. " with multiple plurals")
	end

	-- Gather masculines/feminines.
	local function handle_mf(mfs, qualifiers, inflect)
		local retval = {}
		for i, mf in ipairs(mfs) do
			local function insert_infl(list, term, accel, existing_qualifiers)
				insert_inflection(list, term, accel, fetch_qualifiers(qualifiers[i], existing_qualifiers), "no inv")
			end
			local function call_inflect(special)
				if inflect then
					-- Generate default feminine.
					return inflect(lemma, special)
				else
					-- FIXME
					error("Can't generate default masculine currently")
				end
			end

			if mf == "+" then
				mf = call_inflect()
			else
				mf = replace_hash_with_lemma(mf, lemma)
			end
			local special = require(romut_module).get_special_indicator(mf)
			if special then
				mf = call_inflect(special)
			end
			insert_infl(retval, mf)
		end
		return retval
	end

	local feminines = handle_mf(args.f, args.f_qual, com.make_feminine)
	-- FIXME, write make_masculine()
	local masculines = handle_mf(args.m, args.m_qual)

	check_all_missing(data, plurals, plpos)
	check_all_missing(data, feminines, plpos)

	if #plurals > 0 then
		plurals.label = "plural"
		plurals.accel = {form = "p"}
		table.insert(data.inflections, plurals)
	end

	if #masculines > 0 then
		masculines.label = "masculine"
		table.insert(data.inflections, masculines)
	end

	if #feminines > 0 then
		feminines.label = "feminine"
		feminines.accel = {form = "f"}
		table.insert(data.inflections, feminines)
	end

	-- Is this a noun with an unexpected ending (for its gender)?
	-- Only check if the term is one word (there are no spaces in the term).
	local irreg_gender_lemma = rsub(lemma, " .*", "") -- only look at first word
	if (gender_for_irreg_ending == "m" or gender_for_irreg_ending == "mf") and irreg_gender_lemma:find("a$") then
		table.insert(data.categories, langname .. " masculine " .. plpos .. " ending in -a")
	elseif (gender_for_irreg_ending == "f" or gender_for_irreg_ending == "mf") and not (
		irreg_gender_lemma:find("a$") or irreg_gender_lemma:find("ió$") or irreg_gender_lemma:find("tat$") or
		irreg_gender_lemma:find("tud$") or irreg_gender_lemma:find("[dt]riu$")) then
		table.insert(data.categories, langname .. " feminine " .. plpos .. " with no feminine ending")
	end
end

local function get_noun_params()
	return {
		[1] = {list = "g", required = true, default = "?"},
		[2] = {list = "pl"},
		["g_qual"] = {list = "g\1_qual", allow_holes = true},
		["pl_qual"] = {list = "pl\1_qual", allow_holes = true},
		["m"] = {list = true},
		["m_qual"] = {list = "m\1_qual", allow_holes = true},
		["f"] = {list = true},
		["f_qual"] = {list = "f\1_qual", allow_holes = true},
		["mpl"] = {list = true},
		["mpl_qual"] = {list = "mpl\1_qual", allow_holes = true},
		["fpl"] = {list = true},
		["fpl_qual"] = {list = "fpl\1_qual", allow_holes = true},
	}
end

pos_functions["nouns"] = {
	params = get_noun_params(),
	func = function(args, data, is_suffix)
		do_noun(args, data, "noun", is_suffix)
	end,
}

pos_functions["proper nouns"] = {
	params = get_noun_params(),
	func = function(args, data, is_suffix)
		do_noun(args, data, "noun", is_suffix, "is proper")
	end,
}

-----------------------------------------------------------------------------------------
--                                         Verbs                                       --
-----------------------------------------------------------------------------------------

pos_functions["verbs"] = {
	params = {
		[1] = {},
		["pres"] = {list = true}, --present
		["pres_qual"] = {list = "pres\1_qual", allow_holes = true},
		["pres3s"] = {list = true}, --third-singular present
		["pres3s_qual"] = {list = "pres3s\1_qual", allow_holes = true},
		["pret"] = {list = true}, --preterite
		["pret_qual"] = {list = "pret\1_qual", allow_holes = true},
		["part"] = {list = true}, --participle
		["part_qual"] = {list = "part\1_qual", allow_holes = true},
		["short_part"] = {list = true}, --short participle
		["short_part_qual"] = {list = "short_part\1_qual", allow_holes = true},
		["noautolinktext"] = {type = "boolean"},
		["noautolinkverb"] = {type = "boolean"},
		["attn"] = {type = "boolean"},
		["pres_1_sg"] = {}, -- accept any ignore old-style param
		["past_part"] = {}, -- accept any ignore old-style param
		["root"] = {}, -- FIXME: Implement root-stressed vowel quality
	},
	func = function(args, data, tracking_categories, frame)
		local preses, preses_3s, prets, parts, short_parts

		if args.attn then
			table.insert(tracking_categories, "Requests for attention concerning " .. langname)
			return
		end

		local ca_verb = require(ca_verb_module)
		local alternant_multiword_spec = ca_verb.do_generate_forms(args, "ca-verb", data.heads[1])

		local specforms = alternant_multiword_spec.forms
		local function slot_exists(slot)
			return specforms[slot] and #specforms[slot] > 0
		end

		local function do_finite(slot_tense, label_tense)
			-- Use pres_3s if it exists and pres_1s doesn't exist (e.g. impersonal verbs); similarly for pres_3p (only3p verbs);
			-- but fall back to pres_1s if neither pres_1s nor pres_3s nor pres_3p exist (e.g. [[empedernir]]).
			local has_1s = slot_exists(slot_tense .. "_1s")
			local has_3s = slot_exists(slot_tense .. "_3s")
			local has_3p = slot_exists(slot_tense .. "_3p")
			if has_1s or (not has_3s and not has_3p) then
				return {
					slot = slot_tense .. "_1s",
					label = ("first-person singular %s"):format(label_tense),
				}, true
			elseif has_3s then
				return {
					slot = slot_tense .. "_3s",
					label = ("third-person singular %s"):format(label_tense),
				}, false
			else
				return {
					slot = slot_tense .. "_3p",
					label = ("third-person plural %s"):format(label_tense),
				}, false
			end
		end

		local did_pres_1s
		preses, did_pres_1s = do_finite("pres", "present")
		preses_3s = {
			slot = "pres_3s",
			label = "third-person singular present",
		}
		prets = do_finite("pret", "preterite")
		parts = {
			slot = "pp_ms",
			label = "past participle",
		}
		short_parts = {
			slot = "short_pp_ms",
			label = "short past participle",
		}

		if #args.pres > 0 or #args.pres3s > 0 or #args.pret > 0 or #args.part > 0 or #args.short_part > 0 then
			track("verb-old-multiarg")
		end

		local function strip_brackets(qualifiers)
			if not qualifiers then
				return nil
			end
			local stripped_qualifiers = {}
			for _, qualifier in ipairs(qualifiers) do
				local stripped_qualifier = qualifier:match("^%[(.*)%]$")
				if not stripped_qualifier then
					error("Internal error: Qualifier should be surrounded by brackets at this stage: " .. qualifier)
				end
				table.insert(stripped_qualifiers, stripped_qualifier)
			end
			return stripped_qualifiers
		end

		local function do_verb_form(args, qualifiers, slot_desc, skip_if_empty)
			local forms
			local to_insert

			if #args == 0 then
				forms = specforms[slot_desc.slot]
				if not forms or #forms == 0 then
					if skip_if_empty then
						return
					end
					forms = {{form = "-"}}
				end
			elseif #args == 1 and args[1] == "-" then
				forms = {{form = "-"}}
			else
				forms = {}
				for i, arg in ipairs(args) do
					local qual = qualifiers[i]
					if qual then
						-- FIXME: It's annoying we have to add brackets and strip them out later. The inflection
						-- code adds all footnotes with brackets around them; we should change this.
						qual = {"[" .. qual .. "]"}
					end
					local form = arg
					if not args.noautolinkverb then
						-- [[Module:inflection utilities]] already loaded by [[Module:ca-verb]]
						form = require(inflection_utilities_module).add_links(form)
					end
					table.insert(forms, {form = form, footnotes = qual})
				end
			end

			if forms[1].form == "-" then
				to_insert = {label = "no " .. slot_desc.label}
			else
				local into_table = {label = slot_desc.label}
				for _, form in ipairs(forms) do
					local qualifiers = strip_brackets(form.footnotes)
					-- Strip redundant brackets surrounding entire form. These may get generated e.g.
					-- if we use the angle bracket notation with a single word.
					local stripped_form = rmatch(form.form, "^%[%[([^%[%]]*)%]%]$") or form.form
					-- Don't include accelerators if brackets remain in form, as the result will be wrong.
					-- FIXME: For now, don't include accelerators. We should use the new {{ca-verb form of}}.
					-- local this_accel = not stripped_form:find("%[%[") and accel or nil
					local this_accel = nil
					table.insert(into_table, {term = stripped_form, q = qualifiers, accel = this_accel})
				end
				to_insert = into_table
			end

			table.insert(data.inflections, to_insert)
		end

		local skip_pres_if_empty
		if alternant_multiword_spec.no_pres1_and_sub then
			table.insert(data.inflections, {label = "no first-person singular present"})
			table.insert(data.inflections, {label = "no present subjunctive"})
		end
		if alternant_multiword_spec.no_pres_stressed then
			table.insert(data.inflections, {label = "no stressed present indicative or subjunctive"})
			skip_pres_if_empty = true
		end
		if alternant_multiword_spec.only3s then
			table.insert(data.inflections, {label = glossary_link("impersonal")})
		elseif alternant_multiword_spec.only3sp then
			table.insert(data.inflections, {label = "third-person only"})
		elseif alternant_multiword_spec.only3p then
			table.insert(data.inflections, {label = "third-person plural only"})
		end
		local has_vowel_alt
		if alternant_multiword_spec.vowel_alt then
			for _, vowel_alt in ipairs(alternant_multiword_spec.vowel_alt) do
				if vowel_alt ~= "+" and vowel_alt ~= "í" and vowel_alt ~= "ú" then
					has_vowel_alt = true
					break
				end
			end
		end

		do_verb_form(args.pres, args.pres_qual, preses, skip_pres_if_empty)
		-- We want to include both the pres_1s and pres_3s if there is a vowel alternation in the present singular. But we
		-- don't want to redundantly include the pres_3s if we already included it.
		if did_pres_1s and has_vowel_alt then
			do_verb_form(args.pres3s, args.pres3s_qual, preses_3s, skip_pres_if_empty)
		end
		do_verb_form(args.pret, args.pret_qual, prets)
		do_verb_form(args.part, args.part_qual, parts)
		do_verb_form(args.short_part, args.short_part_qual, short_parts, "skip if empty")

		-- Add categories.
		for _, cat in ipairs(alternant_multiword_spec.categories) do
			table.insert(data.categories, cat)
		end

		-- If the user didn't explicitly specify head=, or specified exactly one head (not 2+) and we were able to
		-- incorporate any links in that head into the 1= specification, use the infinitive generated by
		-- [[Module:ca-verb]] in place of the user-specified or auto-generated head. This was copied from
		-- [[Module:it-headword]], where doing this gets accents marked on the verb(s). We don't have accents marked on
		-- the verb but by doing this we do get any footnotes on the infinitive propagated here. Don't do this if the
		-- user gave multiple heads or gave a head with a multiword-linked verbal expression such as Italian
		-- '[[dare esca]] [[al]] [[fuoco]]' (FIXME: give Catalan equivalent).
		if #data.user_specified_heads == 0 or (
			#data.user_specified_heads == 1 and alternant_multiword_spec.incorporated_headword_head_into_lemma
		) then
			data.heads = {}
			for _, lemma_obj in ipairs(alternant_multiword_spec.forms.infinitive_linked) do
				local quals, refs = require(inflection_utilities_module).
					convert_footnotes_to_qualifiers_and_references(lemma_obj.footnotes)
				table.insert(data.heads, {term = lemma_obj.form, q = quals, refs = refs})
			end
		end

		if args.root then
			local m_ca_IPA = require(ca_IPA_module)

			local parsed_respellings = {}
			local function set_parsed_respelling(dialect, parsed)
				-- Validate the individual root vowel specs.
				for _, termobj in ipairs(parsed.terms) do
					if not rfind(termobj.words[1].term, "^" .. m_ca_IPA.mid_vowel_hint_c .. "$") then
						error(("Root vowel spec '%s' should be one of the vowels %s"):format(
							termobj.words[1].term, m_ca_IPA.mid_vowel_hints))
					end
				end

				if not dialect then
					for _, dial in ipairs(m_ca_IPA.dialects) do
						-- Need to clone as we destructively modify each one later with the pronun.
						parsed_respellings[dial] = m_table.deepcopy(parsed)
					end
				elseif m_ca_IPA.dialect_groups[dialect] then
					for _, dial in ipairs(m_ca_IPA.dialect_groups[dialect]) do
						-- Need to clone as we destructively modify each one later with the pronun.
						parsed_respellings[dial] = m_table.deepcopy(parsed)
					end
				else
					parsed_respellings[dialect] = parsed
				end
			end

			local function check_dialect_or_dialect_group(dialect)
				if not m_table.contains(m_ca_IPA.dialects, dialect) and not
					m_ca_IPA.dialect_groups[dialect] then
					local dialect_list = {}
					for _, dial in ipairs(m_ca_IPA.dialects) do
						table.insert(dialect_list, "'" .. dial .. "'")
					end
					dialect_list = list_to_text(dialect_list, nil, " or ")
					local dialect_group_list = {}
					for dialect_group, _ in pairs(m_ca_IPA.dialect_groups) do
						table.insert(dialect_group_list, "'" .. dialect_group .. "'")
					end
					dialect_group_list = list_to_text(dialect_group_list, nil, " or ")
					error(("Unrecognized dialect '%s': Should be a dialect %s or a dialect group %s"):format(
						dialect, dialect_list, dialect_group_list))
				end
			end

			-- Parse the root vowel specs.
			if args.root:find("[<%[]") then
				local put = require(parse_utilities_module)
				-- Parse balanced segment runs involving either [...] (substitution notation) or <...> (inline
				-- modifiers). We do this because we don't want commas or semicolons inside of square or angle brackets
				-- to count as respelling delimiters. However, we need to rejoin square-bracketed segments with nearby
				-- ones after splitting alternating runs on comma and semicolon.
				local segments = put.parse_multi_delimiter_balanced_segment_run(args.root, {{"<", ">"}, {"[", "]"}})

				local semicolon_separated_groups = put.split_alternating_runs(segments, "%s*;%s*")
				for _, group in ipairs(semicolon_separated_groups) do
					local first_element = group[1]
					local dialect
					if first_element:find("^[a-z]+:") then
						-- a dialect-specific spec
						local rest
						dialect, rest = first_element:match("^([a-z]+):(.*)$")
						check_dialect_or_dialect_group(dialect)
						group[1] = rest
					end

					local comma_separated_groups = put.split_alternating_runs_on_comma(group)

					-- Process each value.
					local outer_container = m_ca_IPA.parse_comma_separated_groups(comma_separated_groups, true, args.root,
						"root")
					set_parsed_respelling(dialect, outer_container)
				end
			else
				local function parse_err(msg)
					error(msg .. ": root=" .. args.root)
				end
				for _, dialect_spec in ipairs(rsplit(args.root, "%s*;%s*")) do
					local dialect
					if dialect_spec:find("^[a-z]+:") then
						-- a dialect-specific spec
						local rest
						dialect, rest = dialect_spec:match("^([a-z]+):(.*)$")
						check_dialect_or_dialect_group(dialect)
						dialect_spec = rest
					end
					local termobjs = {}
					for _, word in ipairs(rsplit(dialect_spec, ",")) do
						table.insert(termobjs, {words = {{term = word}}})
					end
					set_parsed_respelling(dialect, {
						terms = termobjs,
					})
				end
			end

			-- Convert each canonicalized respelling to phonemic/phonetic IPA.
			m_ca_IPA.generate_phonemic_phonetic(parsed_respellings)

			-- Group the results.
			local grouped_pronuns = m_ca_IPA.group_pronuns_by_dialect(parsed_respellings)

			-- Format for display.
			for _, grouped_pronun_spec in pairs(grouped_pronuns) do
				local pronunciations = {}

				local function ins(text)
					table.insert(pronunciations, text)
				end

				-- Loop through each pronunciation. For each one, format the phonetic version "raw".
				for j, pronun in ipairs(grouped_pronun_spec.pronuns) do
					-- Add dialect tags to left accent qualifiers if first one
					local as = pronun.a
					if j == 1 then
						if as then
							as = m_table.deepcopy(as)
						else
							as = {}
						end
						for _, dialect in ipairs(grouped_pronun_spec.dialects) do
							table.insert(as, m_ca_IPA.dialects_to_names[dialect])
						end
					else
						ins(", ")
					end

					local slash_pron = "/" .. pronun.phonetic:gsub("ˈ", "") .. "/"
					if as or pronun.q or pronun.qq or pronun.aa then
						ins(require("Module:pron qualifier").format_qualifiers {
							lang = lang,
							text = slash_pron,
							q = pronun.q,
							a = as,
							qq = pronun.qq,
							aa = pronun.aa
						})
					else
						ins(slash_pron)
					end
					if pronun.refs then
						-- FIXME: Copied from [[Module:IPA]]. Should be in a module.
						local refs = {}
						if #pronun.refs > 0 then
							for _, refspec in ipairs(pronun.refs) do
								if type(refspec) ~= "table" then
									refspec = {text = refspec}
								end
								local refargs
								if refspec.name or refspec.group then
									refargs = {name = refspec.name, group = refspec.group}
								end
								table.insert(refs, mw.getCurrentFrame():extensionTag("ref", refspec.text, refargs))
							end
							ins(table.concat(refs))
						end
					end
				end

				grouped_pronun_spec.formatted = table.concat(pronunciations)
			end

			-- Concatenate formatted results.
			local formatted = {}
			for _, grouped_pronun_spec in ipairs(grouped_pronuns) do
				table.insert(formatted, grouped_pronun_spec.formatted)
			end
			data.post_note = "''root stress'': " .. table.concat(formatted, "; ")
		end
	end
}

-----------------------------------------------------------------------------------------
--                                        Numerals                                     --
-----------------------------------------------------------------------------------------

-- Display additional inflection information for a numeral
pos_functions["numerals"] = {
	params = {
		[1] = {},
		[2] = {},
		},
	func = function(args, data, is_suffix)
		local feminine = args[1]
		local noun_form = args[2]
		
		if feminine then
			table.insert(data.genders, "m")
			table.insert(data.inflections, {label = "feminine", feminine})
			
			if noun_form then
				table.insert(data.inflections, {label = "noun form", noun_form})
			end
		else
			table.insert(data.genders, "m")
			table.insert(data.genders, "f")
		end
	end
}

-----------------------------------------------------------------------------------------
--                                       Phrases                                       --
-----------------------------------------------------------------------------------------

pos_functions["phrases"] = {
	params = {
		["g"] = {list = true},
		["g_qual"] = {list = "g\1_qual", allow_holes = true},
		["m"] = {list = true},
		["m_qual"] = {list = "m\1_qual", allow_holes = true},
		["f"] = {list = true},
		["f_qual"] = {list = "f\1_qual", allow_holes = true},
	},
	func = function(args, data, is_suffix)
		data.genders = {}
		process_genders(data, args.g, args.g_qual)
		insert_ancillary_inflection(data, args.m, args.m_qual, "masculine", "phrases")
		insert_ancillary_inflection(data, args.f, args.f_qual, "feminine", "phrases")
	end,
}

-----------------------------------------------------------------------------------------
--                                    Suffix forms                                     --
-----------------------------------------------------------------------------------------

pos_functions["suffix forms"] = {
	params = {
		[1] = {required = true, list = true},
		["g"] = {list = true},
		["g_qual"] = {list = "g\1_qual", allow_holes = true},
	},
	func = function(args, data, is_suffix)
		data.genders = {}
		process_genders(data, args.g, args.g_qual)
		local suffix_type = {}
		for _, typ in ipairs(args[1]) do
			table.insert(suffix_type, typ .. "-forming suffix")
		end
		table.insert(data.inflections, {label = "non-lemma form of " .. m_table.serialCommaJoin(suffix_type, {conj = "or"})})
	end,
}

return export