Module:category tree/poscatboiler/utilities


local export = {}

--[=[
Generate labels for inflection classes. `data` is a table with the following fields:
* `labels`: The table into which the labels are written.
* `pos`: The singular part of speech, e.g. "noun".
* `stem_classes`: Table of possible stem classes and associated properties. See below.
* `principal_parts`: List of the principal part fields and descriptions. Each list element is a two-element list
  consisting of {"FIELD", "DESCRIPTION"} where FIELD is the field in the element in `stem_classes` (e.g. "nom_sg",
  "gen_sg", "pl", "sup") containing the detailed description of what this principal part looks like, and DESCRIPTION
  is the corresponding English description of the principal part (e.g. "nominative singular").
* `mark_up_spec`: Optional function to add markup to a spec. Takes two arguments, the spec and a flag `nolink`; if the
  flag is true, links should not be present in the resulting markup. The default just converts literal text enclosed
  in <...> into italics.
* `make_spec_bare`: Optional function to make a spec (stem class or sortkey) free of markup. The default just converts
  literal text enclosed in <...> into bare text.
* `addl`: Optional additional text to be displayed in the footer of each category page.

`stem_classes` is a table describing the various stem classes and how to format the category description of each. It is
a table with keys specifying the stem classes and values consisting of an object containing properties of the stem
class. If the stem class contains the word 'GENDER' in it, it expands into labels both for a parent category that
subsumes several genders (obtained by removing the word 'GENDER' and following whitespace) as well as gender-specific
children categories (obtained by replacing the word 'GENDER' with the genders specified in the `possible_genders`
field). The stem class can contain literal text (e.g. suffixes), which will be marked up appropriately (e.g. italicized)
in breadcrumbs and titles. The fields of the property object for a given stem class are as follows:
* `gender`: The description of the gender(s) of the stem class. If preceded by ~, the description is preceded by
  "most commonly". This appears in the `additional` field of the label properties. It is not used in gender-specific
  children categories; instead the gender of that category is used.
* `possible_genders`: The possible genders this class occurs in. If this is specified, the word 'GENDER' must occur in
  the stem class, and gender-specific variants of the stem class (with GENDER replaced by the possible genders) are
  handled along with a parent category subsuming all genders. 
* `PRINCIPAL_PART`: The ending for the specified principal part. Use <...> to enclose literal Latin-script text (e.g.
  suffixes), which will be italicized. There will be one field for each principal part listed in the `principal_parts`
  table described above.
* `GENDER_PRINCIPAL_PART`: The ending for the GENDER variant of the specified principal part. If not specified, the
  value of `PRINCIPAL_PART` is used.
* `breadcrumb`: The breadcrumb for the category, appearing in the trail of breadcrumbs at the top of the page. If this
  stem has gender-specific variants, the breadcrumb specified here is used only for the parent category, while the
  gender-specific child categories use the gender as the breadcrumb. If not specified, it defaults to `sortkey`. If that
  is also not specified, or if the breadcrumb has the value "+", the stem class (without the word 'GENDER') is used.
  (Use "+" when a sortkey is specified but the stem class should be used as the breadcrumb.)
* `parent`: The parent category or categories. If specified, the actual category label is formed by appending the part
  of speech (e.g. "nouns"). Defaults to "POS by inflection type" where POS is the part of speech. Note that
  gender-specific child categories do not use this, but always have the gender-subsuming parent stem class category as
  their parent.
* `sortkey`: The sort key used for sorting this category among its parent's children. Defaults to the stem class
  (without the word 'GENDER'). Note that gender-specific child categories do nto use this, but always use the gender
  as the sort key.
* `addl`: Optional additional text to be displayed in the footer of the category page.
* `GENDER_addl`: Optional additional text to be displayed in the footer of a gender-specific category page, defaulting
  to `addl`. Use the value `false` to cancel out a non-gender-specific value.
]=]

function export.add_inflection_labels(data)
	for _, reqfield_spec in ipairs {
		{"labels", "table of labels"},
		{"pos", "singular part of speech"},
		{"stem_classes", "table of possible stem classes and associated properties"},
		{"principal_parts", "list of principal part fields and names"},
	} do
		local reqfield, gloss = unpack(reqfield_spec)
		if not data[reqfield] then
			error(("Internal error: Missing field '%s', which should containing the %s"):format(reqfield, gloss))
		end
	end

	local function default_mark_up_spec(spec, nolink)
		return (spec:gsub("<(.-)>", "''%1''"))
	end
	local function default_make_spec_bare(spec)
		return (spec:gsub("<(.-)>", "%1"))
	end
	local function mark_up_spec(spec, nolink)
		return (data.mark_up_spec or default_mark_up_spec)(spec, nolink)
	end
	local function make_spec_bare(spec)
		return (data.make_spec_bare or default_make_spec_bare)(spec)
	end
	local plpos = require("Module:string utilities").pluralize(data.pos)
	for full_infl, spec in pairs(data.stem_classes) do
		local subgenders = spec.possible_genders

		-- Get the stem type.
		local infl
		if subgenders then
			if not full_infl:find("GENDER") then
				error(("Internal error: Declension spec '%s' needs to have the word 'GENDER' in it, in all caps"):format(full_infl))
			end
			infl = full_infl:gsub("GENDER ", "")
		else
			infl = full_infl
		end

		-- Get the breadcrumb.
		local breadcrumb = spec.breadcrumb or spec.sortkey or "+"
		if breadcrumb == "+" then
			breadcrumb = infl
		end

		-- Generate the parents.
		local parents = {}
		local spec_parents = spec.parent
		if type(spec_parents) == "string" then
			spec_parents = {spec_parents}
		end
		local parent_sort = make_spec_bare(spec.sortkey or infl)
		if spec_parents then
			for _, parent in ipairs(spec_parents) do
				table.insert(parents, {name = make_spec_bare(parent) .. " " .. plpos, sort = parent_sort})
			end
		else
			table.insert(parents, {name = plpos .. " by inflection type", sort = parent_sort})
		end

		-- Generate the additional text, including principal part descriptions and footer.
		local function create_addl(gender_spec, subgender_prefix)
			local addl_parts = {}
			local function ins(txt)
				table.insert(addl_parts, txt)
			end
			local function insert_header(gender_spec)
				if gender_spec then
					local most_commonly, gender = gender_spec:match("^(~)(.*)$")
					most_commonly = most_commonly and "most commonly " or ""
					gender = gender or gender_spec
					ins(("These %s are %s%s, typically with the following endings:\n"):format(plpos, most_commonly,
						mark_up_spec(gender)))
				else
					ins(("These %s typically have the following endings:\n"):format(plpos))
				end
			end
			local function process_ending(field, desc, is_last)
				local ending_spec = spec[subgender_prefix .. field] or spec[field]
				if not ending_spec then
					error(("Internal error: for inflection '%s', field '%s' for principal part '%s' is missing"):format(
						full_infl, field, desc))
				end
				return ("* in the %s: %s%s"):format(desc, mark_up_spec(ending_spec), is_last and "." or ";\n")
			end
			local function insert_principal_part_info(subgender_prefix)
				for i, ppart_spec in ipairs(data.principal_parts) do
					local ppart_field, ppart_desc = unpack(ppart_spec)
					ins(process_ending(ppart_field, ppart_desc, i == #data.principal_parts))
				end
			end
			insert_header(gender_spec)
			insert_principal_part_info("")
			local spec_addl = spec[subgender_prefix .. "addl"]
			if spec_addl == nil then
				spec_addl = spec.addl
			end
			if data.addl or spec_addl then
				local footer_parts = {}
				if data.addl then
					table.insert(footer_parts, mark_up_spec(data.addl))
				end
				if spec_addl then
					table.insert(footer_parts, mark_up_spec(spec_addl))
				end
				ins("\n" .. table.concat(footer_parts, " "))
			end
			return table.concat(addl_parts)
		end

		data.labels[make_spec_bare(infl) .. " " .. plpos] = {
			description = "{{{langname}}} " .. mark_up_spec(infl) .. " " .. plpos .. ".",
			displaytitle = "{{{langname}}} " .. mark_up_spec(infl, "nolink") .. " " .. plpos,
			additional = create_addl(spec.gender, ""),
			breadcrumb = mark_up_spec(breadcrumb, "nolink"),
			parents = parents,
		}
		if subgenders then
			for _, subgender in ipairs(subgenders) do
				local gender_infl = full_infl:gsub("GENDER", subgender)
				data.labels[make_spec_bare(gender_infl) .. " " .. plpos] = {
					description = "{{{langname}}} " .. mark_up_spec(gender_infl) .. " " .. plpos .. ".",
					displaytitle = "{{{langname}}} " .. mark_up_spec(gender_infl, "nolink") .. " " .. plpos,
					additional = create_addl(subgender, subgender .. "_"),
					breadcrumb = subgender,
					parents = {{
						name = make_spec_bare(infl) .. " " .. plpos,
						sort = subgender,
					}},
				}
			end
		end
	end
end

return export