Module:mr-verb

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


local export = {}


--[=[

Authorship: Aryaman Arora <AryamanA> based on work by Ben Wing <benwing2>

]=]

--[=[

TERMINOLOGY:

-- "slot" = A particular combination of person/number/gender/tense/etc.
	 Example slot names for verbs are "inf_mp" (masculine plural infinitive),
	 "prog" (undeclined progressive form), "pfv_ind_fut_2sm" (second-person singular
	 masculine perfective indicative future). Each slot is filled with zero or
	 more forms.

-- "form" = The conjugated Marathi form representing the value of a given slot.

-- "lemma" = The dictionary form of a given Marathi term. Generally the direct
     masculine singular infinitive, but may occasionally be another form if
	 that form is missing.
]=]

--[=[

FIXME:
]=]

local lang = require("Module:languages").getByCode("mr")
local m_table = require("Module:table")
local m_links = require("Module:links")
local m_string_utilities = require("Module:string utilities")
local m_script_utilities = require("Module:script utilities")
local iut = require("Module:inflection utilities")
local m_para = require("Module:parameters")
local com = require("Module:mr-common")

local u = require("Module:string/char")
local rsplit = mw.text.split
local rfind = mw.ustring.find
local rmatch = mw.ustring.match
local rgmatch = mw.ustring.gmatch
local rsubn = mw.ustring.gsub
local ulen = mw.ustring.len
local usub = mw.ustring.sub
local uupper = mw.ustring.upper
local ulower = mw.ustring.lower

-- vowel diacritics; don't display nicely on their own
local M = u(0x0901)
local N = u(0x0902)
local AA = u(0x093e)
local AI = u(0x0948)
local AU = u(0x094c)
local E = u(0x0947)
local EN = E .. N
local I = u(0x093f)
local II = u(0x0940)
local IIN = II .. N
local O = u(0x094b)
local U = u(0x0941)
local UU = u(0x0942)
local UUM = UU .. M
local R = u(0x0943)
local VIRAMA = u(0x094d)
local TILDE = u(0x0303)

-- version of rsubn() that discards all but the first return value
local function rsub(term, foo, bar)
	local retval = rsubn(term, foo, bar)
	return retval
end

-- version of rsubn() that returns a 2nd argument boolean indicating whether
-- a substitution was made.
local function rsubb(term, foo, bar)
	local retval, nsubs = rsubn(term, foo, bar)
	return retval, nsubs > 0
end


local function tag_text(text)
    return m_script_utilities.tag_text(text, lang)
end


local function term_link(hi, tr)
    return m_links.full_link({term = hi, tr = tr, lang = lang}, "term")
end


local irreg_perf = {
}

local irreg_subj = {
}

local irreg_polite_imp = {
}

local verb_slots_impers = {
	inf = "inf",
	stem = "stem",
	compl = "compl",
	pros = "pros",
	inc = "inc",
	desid = "desid",
}

local verb_slots_pers = {
	subj_neg = "subj_neg",
}

-- Add entries for a slot with only gender/number variants.
-- `slot_prefix` is the prefix of the slot, typically specifying the tense/aspect;
-- `tag_suffix` is the set of inflection tags to add after the gender/number tags,
-- or "-" to use "-" as the inflection tags (which indicates that no accelerator entry
-- should be generated); and `verb_slots` is the table (personal or impersonal) to
-- add the entries to.
local function add_slot_gendered(slot_prefix, tag_suffix, verb_slots)
	verb_slots = verb_slots or verb_slots_pers
	verb_slots[slot_prefix .. "_ms"] = tag_suffix == "-" and "-" or "m|s|" .. tag_suffix
	verb_slots[slot_prefix .. "_mp"] = tag_suffix == "-" and "-" or "m|p|" .. tag_suffix
	verb_slots[slot_prefix .. "_fs"] = tag_suffix == "-" and "-" or "f|s|" .. tag_suffix
	verb_slots[slot_prefix .. "_fp"] = tag_suffix == "-" and "-" or "f|p|" .. tag_suffix
	verb_slots[slot_prefix .. "_ns"] = tag_suffix == "-" and "-" or "n|s|" .. tag_suffix
	verb_slots[slot_prefix .. "_np"] = tag_suffix == "-" and "-" or "n|p|" .. tag_suffix
end

-- Same as add_slot_gendered() but specifically for participles. This changes the inflection
-- tags used, because the masculine singular entry is really only for direct masculine singular,
-- and the masculine plural entry is also for oblique masculine singular.
local function add_slot_gendered_part(slot_prefix, tag_suffix, verb_slots)
	verb_slots = verb_slots or verb_slots_pers
	verb_slots[slot_prefix .. "_ms"] = tag_suffix == "-" and "-" or "dir|m|s|" .. tag_suffix
	verb_slots[slot_prefix .. "_mp"] = tag_suffix == "-" and "-" or "m|p|" .. tag_suffix .. "|;|obl|m|s|" .. tag_suffix
	verb_slots[slot_prefix .. "_fs"] = tag_suffix == "-" and "-" or "f|s|" .. tag_suffix
	verb_slots[slot_prefix .. "_fp"] = tag_suffix == "-" and "-" or "f|p|" .. tag_suffix
end

-- Compute the inflection tags associated with a given person/number/gender combination.
local function personal_tags(slot_prefix, tag_suffix, persnum, gender)
	gender = gender and gender .. "|" or ""
	local suffix = gender .. tag_suffix
	if persnum == "2s" then -- only for imperatives
		return "2|s|intim|" .. suffix
	elseif persnum == "23s" then
		return "2|s|intim|" .. suffix .. "|;|3|s|" .. suffix
	elseif persnum == "13p" then
		return "13|p|" .. suffix .. "|;|2|formal|" .. suffix
	elseif persnum == "2p" then
		return "2|fam|" .. suffix
	elseif persnum == "3p" then -- only for imperatives
		return "3|p|" .. suffix .. "|;|2|formal|" .. suffix
	else
		-- 1s, 3s or 1p
		return persnum:gsub("^(.)(.)$", "%1|%2") .. "|" .. suffix
	end
end

-- Return the possible person/number combinations given the slot prefix.
local function persnum_values(slot_prefix)
	if slot_prefix:find("^imp_") then
		return {"2s", "2p"}
	else
		return {"1s", "2s", "3s", "1p", "2p", "3p"}
	end
end

-- Add entries for a slot with only person/number variants. See `add_slot_gendered()`.
local function add_slot_personal(slot_prefix, tag_suffix, verb_slots)
	verb_slots = verb_slots or verb_slots_pers
	for _, persnum in ipairs(persnum_values(slot_prefix)) do
		local slot = slot_prefix .. "_" .. persnum
		if tag_suffix == "-" then
			verb_slots[slot] = "-"
		else
			verb_slots[slot] = personal_tags(slot_prefix, tag_suffix, persnum)
		end
	end
end

-- Add entries for a slot with person/number/gender variants. See `add_slot_gendered()`.
local function add_slot_gendered_personal(slot_prefix, tag_suffix, verb_slots)
	verb_slots = verb_slots or verb_slots_pers
	for _, persnum in ipairs(persnum_values(slot_prefix)) do
		for _, gender in ipairs({"m", "f", "n"}) do
			local slot = slot_prefix .. "_" .. persnum .. gender
			if tag_suffix == "-" then
				verb_slots[slot] = "-"
			else
				verb_slots[slot] = personal_tags(slot_prefix, tag_suffix, persnum, gender)
			end
		end
	end
end

add_slot_gendered_personal("ind_hab", "hab|ind")
add_slot_personal("ind_hab_neg", "hab|ind|neg")
add_slot_gendered_personal("ind_perf", "perf|ind")
add_slot_gendered_personal("ind_perf_neg", "perf|ind|neg")
add_slot_gendered("subj", "subj")
add_slot_gendered_personal("prs_prog", "prs|prog")
add_slot_personal("prs_prog_neg", "prs|prog|neg")
add_slot_gendered_personal("pst_prog", "pst|prog")
add_slot_gendered_personal("pst_prog_neg", "pst|prog|neg")

local all_verb_slots = {}
for k, v in pairs(verb_slots_impers) do
	all_verb_slots[k] = v
end
for k, v in pairs(verb_slots_pers) do
	all_verb_slots[k] = v
end

-- Add one inflected form to `base.forms`, specifically to the list of forms associated with
-- the slot `slot` in the table in `base.forms`. Each element of the list is an object of the
-- form {form=FORM, translit=TRANSLIT, footnotes=FOOTNOTES}, where TRANSLIT is missing if no
-- manual translit needs to be given and FOOTNOTES is missing if there aren't any footnotes.
-- If FOOTNOTES is present it is a list of footnotes, where each footnote is e.g. "[rare or archaic]",
-- i.e. surrounded by brackets, with the first character lowercase and no final period.
-- (The brackets are automatically removed, the first character capitalized and a final period added.)
--
-- `stem` is the Devanagari stem to add the ending to.
-- `translit_stem` is the transliteration of `stem`, or nil to use the default transliteration.
-- `ending` is the Devanagari ending to add to the stem, possibly with sandhi changes to the
-- stem or ending.
-- `footnotes` is a list of associated footnotes in the same format as FOOTNOTES above, or nil.
-- `double_word` if given causes the resulting form to be doubled with a hyphen in between the two
-- parts, for use with the progressive form (e.g. करते-करते of verb करना).
local function add(base, stem, translit_stem, slot, ending, footnotes, double_word)
	local function doadd(new_stem, new_translit_stem, new_ending, slot_footnotes)
		new_ending = new_ending or ending
		if new_ending and base.notlast then
			-- If we're not the last verb in a multiword expression, chop off
			-- anything after a space. This is to handle verbs like [[हिलना-डुलना]],
			-- which have e.g. nonaspectual subjunctive 1sg masc हिलूँ-डुलूँ
			-- but perfective future indicative 1sg masc हिला-डुला हूँगा. Also, there
			-- are some special cases:
			-- (1) the conjunctive form should be e.g. हिल-डुलकर or हिल-डुलके
			-- (2) the future indicative should be e.g. 1sg हिलूँ-डुलूँगा
			-- (3) the future imperative 3pl should be e.g. हिलिये-डुलियेगा
			-- (4) the agentive should be e.g. हिलने-डुलनेवाला
			if slot == "conj" then
				new_ending = ""
			elseif slot:find("^ind_fut") then
				new_ending = rsub(new_ending, "ग.$", "") -- गा, गे or गी
			elseif slot == "imp_fut_3p" then
				new_ending = rsub(new_ending, "गा$", "")
			elseif slot:find("^agent") then
				new_ending = rsub(new_ending, "वाल." .. N .. "?$", "") -- वाला, वाले, वाली, वालीं
			else
				new_ending = rsub(new_ending, " .*", "")
			end
		end
		com.add_form(base, new_stem or stem, new_translit_stem or translit_stem, slot,
			new_ending, iut.combine_footnotes(slot_footnotes, footnotes), "link words", double_word)
	end

	doadd()
end


-- Add the conjugation for a tense/aspect row with gender/number variants only.
local function add_conj_gendered(base, slot_prefix, stem, translit_stem, m_s, m_p, f_s, f_p, n_s, n_p, footnotes)
	if not stem then
		stem = base.stem
		translit_stem = base.stem_translit
	end
	add(base, stem, translit_stem, slot_prefix .. "_ms", m_s, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_mp", m_p, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_fs", f_s, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_fp", f_p, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_ns", n_s, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_np", n_p, footnotes)
end


-- Add the conjugation for a tense/aspect row with person/number variants only.
local function add_conj_personal(base, slot_prefix, stem, translit_stem, s1, s2, s3, p1, p2, p3, footnotes)
	if not stem then
		stem = base.stem
		translit_stem = base.stem_translit
	end
	add(base, stem, translit_stem, slot_prefix .. "_1s", s1, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_2s", s2, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_3s", s3, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_1p", p1, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_2p", p2, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_3p", p3, footnotes)
end

-- Add the conjugation for a tense/aspect row with person/number/gender variants.
local function add_conj_gendered_personal(base, slot_prefix, stem, translit_stem,
	s1m, s2m, s3m, p1m, p2m, p3m, s1f, s2f, s3f, p1f, p2f, p3f, s1n, s2n, s3n, p1n, p2n, p3n, footnotes)
	if not stem then
		stem = base.stem
		translit_stem = base.stem_translit
	end
	add(base, stem, translit_stem, slot_prefix .. "_1sm", s1m, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_2sm", s2m, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_3sm", s3m, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_1pm", p1m, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_2pm", p2m, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_3pm", p3m, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_1sf", s1f, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_2sf", s2f, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_3sf", s3f, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_1pf", p1f, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_2pf", p2f, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_3pf", p3f, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_1sn", s1n, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_2sn", s2n, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_3sn", s3n, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_1pn", p1n, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_2pn", p2n, footnotes)
	add(base, stem, translit_stem, slot_prefix .. "_3pn", p3n, footnotes)
end

local function handle_derived_slots_and_overrides(base)
	-- No overrides implemented currently.
	-- process_slot_overrides(base)

	-- Compute linked versions of potential lemma slots, for use in {{hi-verb}}.
	-- We substitute the original lemma (before removing links) for forms that
	-- are the same as the lemma, if the original lemma has links.
	-- (NOTE: Not currently used by {{hi-verb}}.)
	for _, slot in ipairs({"inf_ms"}) do
		iut.insert_forms(base.forms, slot .. "_linked", iut.map_forms(base.forms[slot], function(form, translit)
			if form == base.orig_lemma_no_links and translit == base.lemma_translit
				and rfind(base.orig_lemma, "%[%[") then
				return base.orig_lemma, base.lemma_translit
			else
				return form, translit
			end
		end))
	end
end


local conjs = {}
local conjprops = {}

conjs["normal"] = function(base)
	local function fetch_irreg(irreg_table)
		if irreg_table[base.stem] then
			local stem = irreg_table[base.stem]
			return stem, nil
		else
			return base.stem, base.stem_translit
		end
	end

	local perf, trperf = fetch_irreg(irreg_perf)
	local subj, trsubj = fetch_irreg(irreg_subj)
	local polite_imp, tr_polite_imp = fetch_irreg(irreg_polite_imp)

	-- Undeclined forms
	add(base, base.stem, base.stem_translit, "stem", "")
	add(base, base.stem, base.stem_translit, "inf", "णे")
	add(base, base.stem, base.stem_translit, "inf", "णं")
	add(base, base.stem, base.stem_translit, "compl", UU .. "न")
	add(base, base.stem, base.stem_translit, "pros", "णार")
	add(base, base.stem, base.stem_translit, "inc", UU)
	add(base, base.stem, base.stem_translit, "desid", AA .. "यला")
	
	add_conj_gendered_personal(base, "ind_hab", nil, nil, "तो", "तोस", "तो", "तो", "ता", "तात", "ते", "तेस", "ते", "तो", "ता", "तात", nil, nil, "ते", "तो", "ता", "तात")
	add_conj_personal(base, "ind_hab_neg", nil, nil, "त नाही", "त नाहीस", "त नाही", "त नाही", "त नाही", "त नाहीत")
	add(base, base.stem, base.stem_translit, "ind_hab_3sn", "तं")
	
	add_conj_gendered_personal(base, "ind_perf", nil, nil, "लो", "लास", "ला", "लो", "ला", "ले", "ले", "लीस", "ली", "लो", "ला", "ल्या", nil, nil, "ले", nil, nil, "ली")
	add(base, base.stem, base.stem_translit, "ind_perf_3sn", "लं")
	add(base, base.stem, base.stem_translit, "ind_perf_2pm", "लात")
	add(base, base.stem, base.stem_translit, "ind_perf_2pf", "लात")
	
	add_conj_gendered_personal(base, "ind_perf_neg", nil, nil, "लो नाही", "ला नाहीस", "ला नाही", "लो नाही", "ला नाहीत", "ले नाहीत", "ले नाही", "ली नाहीस", "ली नाही", "लो नाही", "ला नाहीत", "ल्या नाहीत", nil, nil, "ले नाही", nil, nil, "ली नाहीत")
	add(base, base.stem, base.stem_translit, "ind_perf_neg_3sn", "लं नाही")
	
	add_conj_gendered(base, "subj", nil, nil, AA .. "वा", AA .. "वे", AA .. "वी", AA .. "व्या", AA .. "वे", AA .. "वी")
	add(base, base.stem, base.stem_translit, "subj_mp", AA .. "वेत")
	add(base, base.stem, base.stem_translit, "subj_fp", AA .. "व्यात")
	add(base, base.stem, base.stem_translit, "subj_ns", AA .. "वं")
	add(base, base.stem, base.stem_translit, "subj_np", AA .. "वीत")
	add(base, base.stem, base.stem_translit, "subj_neg", UU .. " नए")
	
	add_conj_gendered_personal(base, "prs_prog", nil, nil, "तो आहे", "तो आहेस", "तो आहे", "तो आहोत", "ता आहात", "ता आहेत", "ते आहे", "ते आहेस", "ते आहे", "तो आहोत", "ता आहात", "ता आहेत", nil, nil, "ते आहे", "तो आहोत", "ता आहात", "ता आहेत")
	-- add_conj_gendered_personal(base, "prs_prog", nil, nil, "तोय", "तोयस", "तोय", "तोय", "ताय", "तायत", "त्येय", "त्येस", "त्येय", "तोय", "ताय", "तायत", nil, nil, "तंय", "तोय", "ताय", "तायत")
	add_conj_personal(base, "prs_prog_neg", nil, nil, "त नाहीये", "त नाहीयेस", "त नाहीये", "त नाहीयोत", "त नाहीयात", "त नाहीयेत")
	
	add_conj_gendered_personal(base, "pst_prog", nil, nil, "त होतो", "त होतास", "त होता", "त होतो", "त होता", "त होते", "त होते", "त होतीस", "त होती", "त होतो", "त होता", "त होत्या", nil, nil, "त होते", nil, nil, "त होती")
	add(base, base.stem, base.stem_translit, "pst_prog_3sn", "त होतं")
	add_conj_gendered_personal(base, "pst_prog_neg", nil, nil, "त नव्हतो", "त नव्हतास", "त नव्हता", "त नव्हतो", "त नव्हता", "त नव्हते", "त नव्हते", "त नव्हतीस", "त नव्हती", "त नव्हतो", "त नव्हता", "त नव्हत्या", nil, nil, "त नव्हते", nil, nil, "त नव्हती")
	add(base, base.stem, base.stem_translit, "pst_prog_neg_3sn", "त नव्हतं")
end


--[=[
Parse an indicator spec (text consisting of angle brackets and zero or more
dot-separated indicators within them). Return value is an object of the form

{
  forms = {}, -- forms for a single spec alternant; see `forms` below

  -- The following additional fields are added by other functions:
  orig_lemma = "ORIGINAL-LEMMA", -- as given by the user or taken from pagename
  orig_lemma_no_links = "ORIGINAL-LEMMA-NO-LINKS", -- links removed
  lemma = "LEMMA", -- `orig_lemma_no_links`, converted to singular form if plural
  phon_lemma = "LEMMA-PHONETIC-RESPELLING", -- as specified by the user; may be missing
  lemma_translit = "LEMMA-TRANSLIT", -- translit of phon_lemma (if present)
  forms = {
	SLOT = {
	  {
		form = "FORM",
		footnotes = {"FOOTNOTE", "FOOTNOTE", ...} -- may be missing
	  },
	  ...
	},
	...
  },
  conj = "CONJ", -- declension, e.g. "normal" (the only one currently implemented)
}
]=]
local function parse_indicator_spec(angle_bracket_spec)
	local inside = rmatch(angle_bracket_spec, "^<(.*)>$")
	assert(inside)
	local base = {overrides = {}, forms = {}}
	if inside ~= "" then
		local segments = iut.parse_balanced_segment_run(inside, "[", "]")
		local dot_separated_groups = iut.split_alternating_runs(segments, "%.")
		for i, dot_separated_group in ipairs(dot_separated_groups) do
			-- No indicators allowed currently.
			local part = dot_separated_group[1]
			error("Unrecognized indicator '" .. part .. "': '" .. inside .. "'")
		end
	end
	return base
end


local function detect_indicator_spec(base)
	base.conj = "normal"
	base.stem, base.stem_translit = com.strip_ending(base, "णे")
end


local function detect_all_indicator_specs(alternant_multiword_spec)
	iut.map_word_specs(alternant_multiword_spec, function(base)
		detect_indicator_spec(base)
	end)

	-- Set notlast=true on verbs that aren't the last one in a multiword expression, and
	-- multiword=true on all verbs in multiword expressions (as well as at top level), so
	-- we can properly handle verbs like [[हिलना-डुलना]].
	for i, alternant_or_word_spec in ipairs(alternant_multiword_spec.alternant_or_word_specs) do
		if alternant_or_word_spec.alternants then
			for _, multiword_spec in ipairs(alternant_or_word_spec.alternants) do
				for j, word_spec in ipairs(multiword_spec.word_specs) do
					if j < #multiword_spec.word_specs then
						word_spec.notlast = true
					end
					if #multiword_spec.word_specs > 1 then
						word_spec.multiword = true
						alternant_multiword_spec.multiword = true
					end
				end
			end
		else
			if i < #alternant_multiword_spec.alternant_or_word_specs then
				alternant_or_word_spec.notlast = true
			end
			if #alternant_multiword_spec.alternant_or_word_specs > 1 then
				alternant_or_word_spec.multiword = true
				alternant_multiword_spec.multiword = true
			end
		end
	end
end


local function conjugate_verb(base)
	if not conjs[base.conj] then
		error("Internal error: Unrecognized conjugation type '" .. base.conj .. "'")
	end
	conjs[base.conj](base)
	if base.multiword then
		-- See comment in add_variant_codes() for the purpose of this.
		com.add_variant_codes(base)
	end
	handle_derived_slots_and_overrides(base)
end


-- Compute the categories to add the verb to, as well as the annotation to display in the
-- conjugation title bar. We combine the code to do these functions as both categories and
-- title bar contain similar information.
local function compute_categories_and_annotation(alternant_multiword_spec)
	local cats = {}
	local function insert(cattype)
		cattype = rsub(cattype, "~", alternant_multiword_spec.pos)
		m_table.insertIfNot(cats, "Marathi " .. cattype)
	end
	local annotation
	if alternant_multiword_spec.manual then
		alternant_multiword_spec.annotation = ""
	else
		local function do_word_spec(base)
			if base.lemma_translit and (lang:transliterate(base.lemma)) ~= base.lemma_translit then
				insert("~ with phonetic respelling")
			end
		end
		iut.map_word_specs(alternant_multiword_spec, function(base)
			do_word_spec(base)
		end)
	end
	alternant_multiword_spec.categories = cats
end


-- Convert forms from their list/object form (see the comments to add() for how this works)
-- to strings that can be directly filled into the table. Approximately, each form is converted
-- to a formatted link with accelerators and the results are concatenated, followed by an newline
-- and then the formatted transliterations.
local function show_forms(alternant_multiword_spec)
	local lemmas = alternant_multiword_spec.forms.inf_ms or {}
	local props = {
		lemmas = lemmas,
		slot_table = verb_slots_impers,
		lang = lang,
		include_translit = true,
	}
	if alternant_multiword_spec.multiword then
		-- Remove variant codes that were added to ensure only parallel variants in
		-- multiword expressions like [[हिलना-डुलना]] get generated. See com.add_variant_codes()
		-- for more information.
		props.canonicalize = function(form)
			return com.remove_variant_codes(form)
		end
	end
	iut.show_forms(alternant_multiword_spec.forms, props)
	alternant_multiword_spec.forms.footnote_impers = alternant_multiword_spec.forms.footnote
	props.slot_table = verb_slots_pers
	iut.show_forms(alternant_multiword_spec.forms, props)
	alternant_multiword_spec.forms.footnote_pers = alternant_multiword_spec.forms.footnote
end


-- Generate the conjugation table. This should be called after show_forms() has converted
-- each form to a formatted string.
local function make_table(alternant_multiword_spec)
	local table_spec_impersonal = [=[
<div class="NavFrame" style="display: table;">
<div class="NavHead hi-table-title" style="background: #d9ebff;">Impersonal forms of {inf_raw}</div>
<div class="NavContent">
{\op}| class="inflection-table inflection-hi inflection-verb" data-toggle-category="inflection"
|-
| class="hi-tense-aspect-cell"colspan=100% | ''Undeclined''
|-
| class="hi-tense-aspect-cell" colspan=2 | ''Stem''
| colspan="100%" | {stem}
|-
| class="hi-tense-aspect-cell" colspan=2 | ''Infinitive''
| colspan="100%" | {inf}
|-
| class="hi-tense-aspect-cell" colspan=2 | ''Completive''
| colspan="100%" | {compl}
|-
| class="hi-tense-aspect-cell" colspan=2 | ''Prospective''
| colspan="100%" | {pros}
|-
| class="hi-tense-aspect-cell" colspan=2 | ''Inceptive''
| colspan="100%" | {inc}
|-
| class="hi-tense-aspect-cell" colspan=2 | ''Desiderative''
| colspan="100%" | {desid}
|{\cl}{notes_clause}</div></div>]=]

	local person_number_header_two_row = [=[
|- class="hi-table-header"
| rowspan=2 |
| rowspan=2 |
| class="hi-mf-cell" rowspan=2 |
]=]

	local person_number_header_sg_pl_headers = [=[
| colspan=3 | '''Singular'''
| colspan=3 | '''Plural'''
]=]

	local person_number_header_table_div = [=[
|- class="hi-table-header"
]=]

	local person_number_header_pers_num_headers = [=[
| '''1<sup>st</sup>'''<br><span lang="mr" class="Deva">[[मी]]</span>
| '''2<sup>nd</sup> intimate'''<br><span lang="mr" class="Deva">[[तू]]</span>
| '''3<sup>rd</sup>'''<br><span lang="mr" class="Deva">[[यह]]/[[वह]]</span>
| '''1<sup>st</sup>'''<br><span lang="mr" class="Deva">[[आम्ही]]/[[आपण]]</span>
| '''2<sup>nd</sup>'''<br><span lang="mr" class="Deva">[[तुम्ही]]</span>
| '''3<sup>rd</sup>'''<br><span lang="mr" class="Deva">[[हा]]/[[तो]]</span>
]=]

	-- Regular person-number header used at the top of the table and in the middle.
	local person_number_header =
		person_number_header_table_div .. person_number_header_two_row .. person_number_header_sg_pl_headers ..
		person_number_header_table_div .. person_number_header_pers_num_headers
	-- Reversed person-number header used at the bottom of the table. "Reversed" means that
	-- the two rows are in reversed order; but internally we can't switch the order of everything
	-- (e.g. the double-row cells at the left side), so we need to break up the header into multiple
	-- parts and only reverse certain parts.
	local reversed_person_number_header =
		person_number_header_table_div .. person_number_header_two_row .. person_number_header_pers_num_headers ..
		person_number_header_table_div .. person_number_header_sg_pl_headers

	local table_spec_personal = [=[
<div class="NavFrame" style="display: table;>
<div class="NavHead hi-table-title" style="background: #d9ebff;">Personal forms of {inf_raw}</div>
<div class="NavContent">
{\op}| class="inflection-table inflection-hi inflection-verb" data-toggle-category="inflection"
|-
| class="hi-sec-div" rowspan=1 colspan=100% | ''Non-Aspectual''
{person_number_header}
|-
| class="hi-tense-aspect-cell" rowspan=4 colspan=1 | Subjunctive
| class="hi-polarity-cell" rowspan=3 colspan=1 | +
| class="hi-mf-cell" | {m}
| colspan=3 | {subj_ms}
| colspan=3 | {subj_mp}
|-
| class="hi-mf-cell" | {f}
| colspan=3 | {subj_fs}
| colspan=3 | {subj_fp}
|-
| class="hi-mf-cell" | {n}
|
|
| {subj_ns}
|
|
| {subj_np}
|-
| class="hi-polarity-cell" rowspan=1 colspan=1 | –
| class="hi-mf-cell" | {mfn}
| colspan=6 | {subj_neg}
|-
| class="hi-sec-div" rowspan=1 colspan=100% | ''Perfective''
|-
| class="hi-tense-aspect-cell" rowspan=6 colspan=1 | Unmarked
| class="hi-polarity-cell" rowspan=3 colspan=1 | +
| class="hi-mf-cell" | {m}
| {ind_perf_1sm}
| {ind_perf_2sm}
| {ind_perf_3sm}
| {ind_perf_1pm}
| {ind_perf_2pm}
| {ind_perf_3pm}
|- class="hi-row-f"
| class="hi-mf-cell" | {f}
| {ind_perf_1sf}
| {ind_perf_2sf}
| {ind_perf_3sf}
| {ind_perf_1pf}
| {ind_perf_2pf}
| {ind_perf_3pf}
|- class="hi-row-f"
| class="hi-mf-cell" | {n}
|
|
| {ind_perf_3sn}
|
|
| {ind_perf_3pn}
|-
| class="hi-polarity-cell" rowspan=3 colspan=1 | –
| class="hi-mf-cell" | {m}
| {ind_perf_neg_1sm}
| {ind_perf_neg_2sm}
| {ind_perf_neg_3sm}
| {ind_perf_neg_1pm}
| {ind_perf_neg_2pm}
| {ind_perf_neg_3pm}
|- class="hi-row-f"
| class="hi-mf-cell" | {f}
| {ind_perf_neg_1sf}
| {ind_perf_neg_2sf}
| {ind_perf_neg_3sf}
| {ind_perf_neg_1pf}
| {ind_perf_neg_2pf}
| {ind_perf_neg_3pf}
|- class="hi-row-f"
| class="hi-mf-cell" | {n}
|
|
| {ind_perf_neg_3sn}
|
|
| {ind_perf_neg_3pn}
|-
| class="hi-sec-div" rowspan=1 colspan=100% | ''Imperfective''
|-
| class="hi-tense-aspect-cell" rowspan=4 colspan=1 | {HAB}
| class="hi-polarity-cell" rowspan=3 colspan=1 | +
| class="hi-mf-cell" | {m}
| {ind_hab_1sm}
| {ind_hab_2sm}
| {ind_hab_3sm}
| rowspan = 2 | {ind_hab_1pm}
| rowspan = 2 | {ind_hab_2pm}
| rowspan = 3 | {ind_hab_3pm}
|- class="hi-row-f"
| class="hi-mf-cell" | {f}
| {ind_hab_1sf}
| {ind_hab_2sf}
| {ind_hab_3sf}
|- class="hi-row-f"
| class="hi-mf-cell" | {n}
|
|
| {ind_hab_3sn}
|
|
|-
| class="hi-polarity-cell" rowspan=1 colspan=1 | –
| class="hi-mf-cell" | {mfn}
| {ind_hab_neg_1s}
| {ind_hab_neg_2s}
| {ind_hab_neg_3s}
| {ind_hab_neg_1p}
| {ind_hab_neg_2p}
| {ind_hab_neg_3p}
|-
| class="hi-tense-aspect-cell" rowspan=4 colspan=1 | {PRS}
| class="hi-polarity-cell" rowspan=3 colspan=1 | +
| class="hi-mf-cell" | {m}
| {prs_prog_1sm}
| {prs_prog_2sm}
| {prs_prog_3sm}
| rowspan=2 | {prs_prog_1pm}
| rowspan=2 | {prs_prog_2pm}
| rowspan=3 | {prs_prog_3pm}
|- class="hi-row-f"
| class="hi-mf-cell" | {f}
| {prs_prog_1sf}
| {prs_prog_2sf}
| {prs_prog_3sf}
|- class="hi-row-f"
| class="hi-mf-cell" | {n}
|
|
| {prs_prog_3sn}
|
|
|-
| class="hi-polarity-cell" rowspan=1 colspan=1 | –
| class="hi-mf-cell" | {mfn}
| {prs_prog_neg_1s}
| {prs_prog_neg_2s}
| {prs_prog_neg_3s}
| {prs_prog_neg_1p}
| {prs_prog_neg_2p}
| {prs_prog_neg_3p}
|-
| class="hi-tense-aspect-cell" rowspan=6 colspan=1 | {PST}
| class="hi-polarity-cell" rowspan=3 colspan=1 | +
| class="hi-mf-cell" | {m}
| {pst_prog_1sm}
| {pst_prog_2sm}
| {pst_prog_3sm}
| {pst_prog_1pm}
| {pst_prog_2pm}
| {pst_prog_3pm}
|- class="hi-row-f"
| class="hi-mf-cell" | {f}
| {pst_prog_1sf}
| {pst_prog_2sf}
| {pst_prog_3sf}
| {pst_prog_1pf}
| {pst_prog_2pf}
| {pst_prog_3pf}
|- class="hi-row-f"
| class="hi-mf-cell" | {n}
|
|
| {pst_prog_3sn}
|
|
| {pst_prog_3pn}
|-
| class="hi-polarity-cell" rowspan=3 colspan=1 | —
| class="hi-mf-cell" | {m}
| {pst_prog_neg_1sm}
| {pst_prog_neg_2sm}
| {pst_prog_neg_3sm}
| {pst_prog_neg_1pm}
| {pst_prog_neg_2pm}
| {pst_prog_neg_3pm}
|- class="hi-row-f"
| class="hi-mf-cell" | {f}
| {pst_prog_neg_1sf}
| {pst_prog_neg_2sf}
| {pst_prog_neg_3sf}
| {pst_prog_neg_1pf}
| {pst_prog_neg_2pf}
| {pst_prog_neg_3pf}
|- class="hi-row-f"
| class="hi-mf-cell" | {n}
|
|
| {pst_prog_neg_3sn}
|
|
| {pst_prog_neg_3pn}
{reversed_person_number_header}|{\cl}{notes_clause}</div></div>]=]

	local pres_impf_table = [=[]=]

	local pres_impf_table_missing = [=[]=]

	local presum_table = [=[]=]

	local combined_subj = [=[]=]

	local split_subj = [=[]=]

	local notes_template = [===[
<div class="hi-footnote-outer-div">
<div class="hi-footnote-inner-div">
{footnote}
</div></div>
]===]

	local forms = alternant_multiword_spec.forms

	local function make_gender_abbr(title, text)
		return '<span class="gender"><abbr title="' .. title .. '">' .. text .. '</abbr></span>'
	end
	local function make_tense_aspect_abbr(title, text)
		local template = [=[
''<abbr style="font-variant: small-caps; text-transform: lowercase;" title="{title}">{text}</abbr>'']=]
		return m_string_utilities.format(template, {title = title, text = text})
	end
	forms.m = make_gender_abbr("masculine gender", "m")
	forms.f = make_gender_abbr("feminine gender", "f")
	forms.n = make_gender_abbr("feminine gender", "n")
	forms.mfn = forms.m .. " " .. forms.f .. " " .. forms.n
	forms.s = make_gender_abbr("singular number", "s")
	forms.p = make_gender_abbr("plural number", "p")
	forms.dir = make_gender_abbr("direct", "dir")
	forms.obl = make_gender_abbr("oblique", "obl")
	forms.HAB = make_tense_aspect_abbr("Habitual", "HAB")
	forms.PERF = make_tense_aspect_abbr("Perfect", "PERF")
	forms.SUBJ = make_tense_aspect_abbr("Subjunctive", "SUBJ")
	forms.IMPF = make_tense_aspect_abbr("Imperfect", "IMPF")
	forms.PST = make_tense_aspect_abbr("Past", "PST")
	forms.FUT = make_tense_aspect_abbr("Future", "FUT")
	forms.PRS = make_tense_aspect_abbr("Present", "PRS")
	forms.PRS_FUT = make_tense_aspect_abbr("Present/Future", "PRS<br />FUT")
	forms.PRS_PST = make_tense_aspect_abbr("Present/Past", "PRS<br />PST")
	forms.PRS_PST_FUT = make_tense_aspect_abbr("Present/Past/Future", "PRS<br />PST<br />FUT")
	forms.inf_raw = tag_text(forms.lemma)

	-- Now format the impersonal table.
	forms.footnote = forms.footnote_impers
	forms.notes_clause = forms.footnote ~= "" and
		m_string_utilities.format(notes_template, forms) or ""
	local formatted_table_impers = m_string_utilities.format(table_spec_impersonal, forms)

	-- Now format the personal table.
	forms.footnote = forms.footnote_pers
	forms.notes_clause = forms.footnote ~= "" and
		m_string_utilities.format(notes_template, forms) or ""
	if forms.ind_pres_1s ~= "—" then -- होना
		forms.subj_table = m_string_utilities.format(split_subj, forms)
		forms.pres_impf_table = m_string_utilities.format(pres_impf_table, forms)
		forms.presum_table = m_string_utilities.format(presum_table, forms)
	else
		forms.subj_table = m_string_utilities.format(combined_subj, forms)
		forms.pres_impf_table = pres_impf_table_missing
		forms.presum_table = ""
	end
	forms.person_number_header = person_number_header
	forms.reversed_person_number_header = reversed_person_number_header
	local formatted_table_pers = m_string_utilities.format(table_spec_personal, forms)

	-- Concatenate both.
	return require("Module:TemplateStyles")("Module:hi-verb/style.css") .. formatted_table_impers .. formatted_table_pers
end


-- Implementation of template 'hi-verb cat'.
-- NOTE: Not currently used.
function export.catboiler(frame)
	local SUBPAGENAME = mw.title.getCurrentTitle().subpageText
	local params = {
		[1] = {},
	}
	local args = m_para.process(frame:getParent().args, params)

	local function get_pos()
		local pos = rmatch(SUBPAGENAME, "^Marathi.- ([^ ]*)s ")
		if not pos then
			pos = rmatch(SUBPAGENAME, "^Marathi.- ([^ ]*)s$")
		end
		if not pos then
			error("Invalid category name, should be e.g. \"Marathi verbs with ...\" or \"Marathi ... verbs\"")
		end
		return pos
	end

	local function get_sort_key()
		local pos, sort_key = rmatch(SUBPAGENAME, "^Marathi.- ([^ ]*)s with (.*)$")
		if sort_key then
			return sort_key
		end
		pos, sort_key = rmatch(SUBPAGENAME, "^Marathi ([^ ]*)s (.*)$")
		if sort_key then
			return sort_key
		end
		return rsub(SUBPAGENAME, "^Marathi ", "")
	end

	local cats = {}, pos

	-- Insert the category CAT (a string) into the categories. String will
	-- have "Marathi " prepended and ~ substituted for the plural part of speech.
	local function insert(cat, atbeg)
		local fullcat = "Marathi " .. rsub(cat, "~", pos .. "s")
		if atbeg then
			table.insert(cats, 1, fullcat)
		else
			table.insert(cats, fullcat)
		end
	end

	local maintext
	while true do
		if args[1] then
			maintext = "~ " .. args[1]
			pos = get_pos()
			break
		end

		error("Unrecognized Marathi verb category name")
	end

	insert("~|" .. get_sort_key(), "at beginning")

	local categories = {}
	for _, cat in ipairs(cats) do
		table.insert(categories, "[[Category:" .. cat .. "]]")
	end

	return "This category contains Marathi " .. rsub(maintext, "~", pos .. "s")
		.. "\n" ..
		mw.getCurrentFrame():expandTemplate{title="mr-categoryTOC", args={}}
		.. table.concat(categories, "")
end

-- Externally callable function to parse and conjugate a verb given user-specified arguments.
-- Return value is WORD_SPEC, an object where the conjugated forms are in `WORD_SPEC.forms`
-- for each slot. If there are no values for a slot, the slot key will be missing. The value
-- for a given slot is a list of objects {form=FORM, translit=TRANSLIT, footnotes=FOOTNOTES}.
function export.do_generate_forms(parent_args, pos, from_headword, def)
	local params = {
		[1] = {},
		title = {},
	}

	if from_headword then
		params["lemma"] = {list = true}
		params["id"] = {}
	end

	local args = m_para.process(parent_args, params)
	local PAGENAME = mw.title.getCurrentTitle().text

	if not args[1] then
		if PAGENAME == "mr-conj" then
			args[1] = def or "ऐकणे"
		else
			args[1] = PAGENAME
			-- If pagename has spaces in it, add links around each word
			if args[1]:find(" ") then
				args[1] = "[[" .. rsub(args[1], " ", "]] [[") .. "]]"
			end
		end
	end
	local parse_props = {
		parse_indicator_spec = parse_indicator_spec,
		lang = lang,
		transliterate_respelling = com.transliterate_respelling,
		allow_default_indicator = true,
		allow_blank_lemma = true,
	}
	local alternant_multiword_spec = iut.parse_inflected_text(args[1], parse_props)
	alternant_multiword_spec.title = args.title
	alternant_multiword_spec.pos = pos or "verbs"
	alternant_multiword_spec.args = args
	com.normalize_all_lemmas(alternant_multiword_spec)
	detect_all_indicator_specs(alternant_multiword_spec)
	local inflect_props = {
		slot_table = all_verb_slots,
		lang = lang,
		inflect_word_spec = conjugate_verb,
		-- Return the variant code that was added to ensure only parallel variants in
		-- multiword expressions like [[हिलना-डुलना]] get generated. See com.add_variant_codes()
		-- for more information.
		get_variants = alternant_multiword_spec.multiword and com.get_variants or nil,
		-- We add links around the generated verbal forms rather than allow the entire multiword
		-- expression to be a link, so ensure that user-specified links get included as well.
		include_user_specified_links = true,
	}
	iut.inflect_multiword_or_alternant_multiword_spec(alternant_multiword_spec, inflect_props)
	compute_categories_and_annotation(alternant_multiword_spec)
	return alternant_multiword_spec
end


-- Entry point for {{hi-conj}}. Template-callable function to parse and conjugate a verb given
-- user-specified arguments and generate a displayable table of the conjugated forms.
function export.show(frame)
	local parent_args = frame:getParent().args
	local alternant_multiword_spec = export.do_generate_forms(parent_args)
	show_forms(alternant_multiword_spec)
	return make_table(alternant_multiword_spec) .. require("Module:utilities").format_categories(alternant_multiword_spec.categories, lang)
end


-- Concatenate all forms of all slots into a single string of the form
-- "SLOT=FORM,FORM,...|SLOT=FORM,FORM,...|...". Each FORM is either a string in Devanagari or
-- (if manual translit is present) a specification of the form "FORM//TRANSLIT" where FORM is the
-- Devanagari representation of the form and TRANSLIT its manual transliteration. Embedded pipe symbols
-- (as might occur in embedded links) are converted to <!>. If INCLUDE_PROPS is given, also include
-- additional properties (currently, none). This is for use by bots.
local function concat_forms(alternant_spec, include_props)
	local ins_text = {}
	for slot, _ in pairs(verb_slots_with_linked) do
		local formtext = iut.concat_forms_in_slot(alternant_spec.forms[slot])
		if formtext then
			table.insert(ins_text, slot .. "=" .. formtext)
		end
	end
	return table.concat(ins_text, "|")
end


-- Template-callable function to parse and conjugate a verb given user-specified arguments and return
-- the forms as a string of the same form as documented in concat_forms() above.
function export.generate_forms(frame)
	local include_props = frame.args["include_props"]
	local parent_args = frame:getParent().args
	local alternant_spec = export.do_generate_forms(parent_args)
	return concat_forms(alternant_spec, include_props)
end

return export