Jump to content

Module:gmq-headword

From Wiktionary, the free dictionary


local export = {}
local pos_functions = {}

--[=[
Author: Benwing2

This module is eventually intended for all the headword templates of all North Germanic languages, especially the ones
that still maintain complex inflection systems (Icelandic, Faroese, Old Norse, Old Swedish, Elfdalian, etc.), but could
definitely be extended to support other North Germanic languages (Swedish, Danish, Norwegian *). Currently it only
supports Icelandic.
]=]

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

local rfind = mw.ustring.find

local require_when_needed = require("Module:utilities/require when needed")
local m_table = require("Module:table")
local headword_utilities_module = "Module:headword utilities"
local m_headword_utilities = require_when_needed(headword_utilities_module)
local glossary_link = require_when_needed(headword_utilities_module, "glossary_link")
local inflection_utilities_module = "Module:inflection utilities"
local string_utilities_module = "Module:string utilities"
local singularize = require_when_needed(string_utilities_module, "singularize")
local pluralize = require_when_needed(string_utilities_module, "pluralize")
local m_inflection_utilities = require_when_needed(inflection_utilities_module)
local is_adjective_module = "Module:is-adjective"
local is_common_module = "Module:is-common"
local is_noun_module = "Module:is-noun"
local dump = mw.dumpObject

local list_param = {list = true, disallow_holes = true}

-- Table of all valid genders by language, mapping user-specified gender specs to canonicalized versions.
local valid_number_suffixes = {"", "-p"}
local valid_genders = {"m", "f", "n"}
local valid_gender_specs = {}

for _, gender in ipairs(valid_genders) do
	for _, number in ipairs(valid_number_suffixes) do
		local spec = gender .. number
		valid_gender_specs[spec] = spec
	end
end

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

-- The main entry point.
-- This is the only function that can be invoked from a template.
function export.show(frame)
	local iparams = {
		[1] = {required = true},
		["lang"] = {required = true},
		["def"] = {},
	}
	local iargs = require("Module:parameters").process(frame.args, iparams)
	local args = frame:getParent().args
	local poscat = iargs[1]
	local langcode = iargs.lang
	if langcode ~= "is" then
		error("This module currently only works for lang=is")
	end
	local lang = require("Module:languages").getByCode(langcode, true)
	local langname = lang:getCanonicalName()
	local def = iargs.def

	local parargs = frame:getParent().args

	local params = {
		["head"] = list_param,
		["id"] = {},
		["sort"] = {},
		["nolinkhead"] = {type = "boolean"},
		["json"] = {type = "boolean"},
		["pagename"] = {}, -- for testing
	}

	if pos_functions[poscat] then
		local posparams = pos_functions[poscat].params
		if type(posparams) == "function" then
			posparams = posparams(lang)
		end
		for key, val in pairs(posparams) do
			params[key] = val
		end
	end

    local args = require("Module:parameters").process(parargs, params)

	local pagename = args.pagename or mw.title.getCurrentTitle().text

	local data = {
		lang = lang,
		langname = langname,
		pos_category = poscat,
		categories = {},
		heads = args.head,
		genders = {},
		inflections = {},
		pagename = pagename,
		id = args.id,
		sort_key = args.sort,
		force_cat_output = force_cat,
		def = def,
		is_suffix = false,
	}

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

	local multiple_data = nil
	if pos_functions[poscat] then
		multiple_data = pos_functions[poscat].func(args, data)
	end

	-- mw.ustring.toNFD performs decomposition, so letters that decompose
	-- to an ASCII vowel and a diacritic, such as é, are counted as vowels and
	-- do not need to be included in the pattern.
	if not pagename:find("[ %-]") and not rfind(mw.ustring.lower(mw.ustring.toNFD(pagename)), "[aeiouyæœø]")
		and not pagename:find("%.$") then
		local cat = langname .. " words without vowels"
		if multiple_data then
			table.insert(multiple_data[1].categories, cat)
		else
			table.insert(data.categories, cat)
		end
	end

	if multiple_data then
		if args.json then
			return require("Module:JSON").toJSON(multiple_data)
		end
		return pos_functions[poscat].process_multiple_headword_data(multiple_data)
	else
		if args.json then
			return require("Module:JSON").toJSON(data)
		end
		return require("Module:headword").full_headword(data)
	end
end

-- Parse user-specified arguments in 1=, 2=, etc. for @@ scraping specs. If found, look up the appropriate
-- inflection(s). Returns three values. If there is no scraping error, the first value is a list of inflection
-- specs, either directly specified by the user or scraped if an @... spec was given), the second value is the
-- singular part of speeh (taken from passed-in `args_pos` if given, else from the pos= argument of the scraped
-- inflection template if found, else nil), and the third value is nil. Otherwise, the first and second values
-- are nil and the third value is a scraping-error data structure, which the calling function should directly
-- return upstream.
local function parse_noun_adj_scraping_specs(data, declspecs, args_pos, infl_template, allow_empty_infl)
	if not declspecs[2] and declspecs[1]:find("^@@") then
		local declid
		if declspecs[1] ~= "@@" then
			declid = declspecs[1]:match("^@@%s*:%s*(.+)$")
			if not declid then
				error(("Syntax error in self-scraping spec '%s'"):format(declspecs[1]))
			end
		end
		local decls = require(is_common_module).find_inflection(data.pagename, infl_template, false, false, declid)
		if type(decls) == "string" then
			data.alternant_multiword_spec = {scrape_errors = {decls}}
			return nil, nil, {data}
		end
		for i, declobj in ipairs(decls.infls) do
			decls.infls[i] = declobj.infl
		end
		args_pos = args_pos or decls.pos
		declspecs = decls.infls
	end
	return declspecs, args_pos, nil
end

local function do_noun_adj_form(this_data, alternant_multiword_spec, slot, label, label_for_not_present, accel_form)
	local forms = alternant_multiword_spec.forms[slot]
	local retval
	if not forms then
		if not label_for_not_present then
			return
		end
		retval = {label = "no " .. label_for_not_present}
	else
		retval = {label = label, accel = accel_form and {form = accel_form} or nil}
		local prev_footnotes
		for _, form in ipairs(forms) do
			local footnotes = form.footnotes
			if footnotes and prev_footnotes and m_table.deepEquals(footnotes, prev_footnotes) then
				footnotes = nil
			end
			prev_footnotes = form.footnotes
			local quals, refs
			if footnotes then
				quals, refs = m_inflection_utilities.fetch_headword_qualifiers_and_references(footnotes)
			end
			local term = form.form
			table.insert(retval, {term = term, q = quals, refs = refs})
		end
	end

	table.insert(this_data.inflections, retval)
end

local function insert_noun_adj_linked_lemma_if_needed(this_data, alternant_multiword_spec, infl_module_name)
	-- Use the "linked" form of the lemma as the head if no head= explicitly given.
	if #this_data.heads == 0 then
		this_data.heads = {}
		local lemmas = require(infl_module_name).get_lemmas(alternant_multiword_spec, "linked variant")
		for _, lemma_obj in ipairs(lemmas) do
			local head = lemma_obj.form
			--local head = alternant_multiword_spec.args.nolinkhead and lemma_obj.form or
			--	m_headword_utilities.add_lemma_links(lemma_obj.form, alternant_multiword_spec.args.splithyph)
			local quals, refs
			if lemma_obj.footnotes then
				quals, refs = m_inflection_utilities.fetch_headword_qualifiers_and_references(lemma_obj.footnotes)
			end
			table.insert(this_data.heads, {term = head, q = quals, refs = refs})
		end
	end
end

local function get_process_noun_adj_multiple_data(infl_template)
	return function(multiple_data)
		local parts = {}
		local function ins(txt)
			table.insert(parts, txt)
		end
		for i, data in ipairs(multiple_data) do
			local alternant_multiword_spec = data.alternant_multiword_spec
			if alternant_multiword_spec.header then
				ins(("'''%s:'''<br />"):format(alternant_multiword_spec.header))
			elseif alternant_multiword_spec.q then
				ins((" ''%s''<br />"):format(alternant_multiword_spec.q))
			elseif i > 1 then
				ins(" ''or''<br />")
			end
			if alternant_multiword_spec.scrape_errors then
				local errmsgs = {}
				for _, scrape_error in ipairs(alternant_multiword_spec.scrape_errors) do
					table.insert(errmsgs, '<span style="font-weight: bold; color: #CC2200;">' .. scrape_error .. "</span>")
				end
				ins(table.concat(errmsgs, "<br />"))
				ins(require("Module:utilities").format_categories(
					{("Icelandic scraping errors in Template:%s"):format(infl_template)}, data.lang, nil, nil,
					force_cat))
			else
				ins(require("Module:headword").full_headword(data))
			end
		end
		return table.concat(parts)
	end
end

local function get_manual_noun_params(lang, is_proper)
	return {
		[1] = {alias_of = "g"},
		["g"] = list_param,
		["g_qual"] = {list = "g\1_qual", allow_holes = true},
		["indecl"] = {type = "boolean"},
		["m"] = list_param,
		["f"] = list_param,
		["dim"] = list_param,
		["aug"] = list_param,
		["pej"] = list_param,
		["dem"] = list_param,
		["fdem"] = list_param,
		["gen"] = list_param,
		["pl"] = list_param,
	}
end

local function do_manual_nouns(is_proper, args, data)
	local specs = valid_gender_specs[data.lang:getCode()]
	for i, g in ipairs(args.g) do
		local canon_g = specs[g]
		if canon_g then
			g = canon_g
		else
			error("Unrecognized gender: '" .. g .. "'")
		end
		track("gender-" .. g)
		if args.g_qual[i] then
			table.insert(data.genders, {spec = g, qualifiers = {args.g_qual[i]}})
		else
			table.insert(data.genders, g)
		end
	end
	if #data.genders == 0 then
		table.insert(data.genders, "?")
	end
	if args.indecl then
		table.insert(data.inflections, {label = glossary_link("indeclinable")})
		table.insert(data.categories, data.langname .. " indeclinable nouns")
	end

	-- Parse and insert an inflection not requiring additional processing into `data.inflections`. The raw arguments
	-- come from `args[field]`, which is parsed for inline modifiers. If there is a corresponding qualifier field
	-- `FIELD_qual`, qualifiers may additionally come from there. `label` is the label that the inflections are given,
	-- which is linked to the glossary if surrounded by <<..>> (which is removed). `plpos` is the plural part of speech,
	-- used in [[Category:LANGNAME PLPOS with red links in their headword lines]]. `accel` is the accelerator form, or
	-- nil.
	local function handle_infl(field, label, frob)
		m_headword_utilities.parse_and_insert_inflection {
			headdata = data,
			forms = args[field],
			paramname = field,
			label = label,
			frob = frob,
		}
	end

	handle_infl("gen", "genitive singular")
	handle_infl("pl", "nominative plural")
	handle_infl("m", "male equivalent")
	handle_infl("f", "female equivalent")
	handle_infl("dim", "<<diminutive>>")
	handle_infl("aug", "<<augmentative>>")
	handle_infl("pej", "<<pejorative>>")
	handle_infl("dem", "<<demonym>>")
	handle_infl("fdem", "female <<demonym>>")
end

local function get_auto_noun_params(lang, is_proper)
	return {
        [1] = {required = true, list = true, default = "akur<m.#>"},
        ["pos"] = {},
		["m"] = list_param,
		["f"] = list_param,
		["dim"] = list_param,
		["aug"] = list_param,
		["pej"] = list_param,
		["dem"] = list_param,
		["fdem"] = list_param,
	}
end

local function do_auto_nouns(is_proper, args, data)
	if data.lang:getCode() ~= "is" then
		error("Internal error: Only Icelandic supported at the moment")
	end
	local m_is_noun = require(is_noun_module)
	local alternant_multiword_specs = {}
	local multiple_data = {}
	local all_genders = {}

	local declspecs, pos, scrape_error_data = parse_noun_adj_scraping_specs(data, args[1], args.pos, "is-ndecl", false)
	if scrape_error_data then
		return scrape_error_data
	end
	if pos then
		data.pos_category = pluralize(pos)
		args.pos = pos
	end
	for i, declspec in ipairs(declspecs) do
		local alternant_multiword_spec =
			m_is_noun.do_generate_forms(args, declspec, is_proper and "is-proper noun" or "is-noun")
		alternant_multiword_specs[i] = alternant_multiword_spec
		local this_data
		if i < #declspecs then
			this_data = m_table.shallowCopy(data)
			this_data.categories = {}
			this_data.inflections = {}
			-- genders gets overwritten just below
		else
			-- for the last (or usually only) spec, save memory by not cloning
			this_data = data
		end
		multiple_data[i] = this_data
		this_data.alternant_multiword_spec = alternant_multiword_spec
		this_data.heads = args.head
		if not alternant_multiword_spec.scrape_errors then
			this_data.genders = alternant_multiword_spec.genders
			for _, gender in ipairs(this_data.genders) do
				m_table.insertIfNot(all_genders, gender)
			end

			local function do_noun_form(slot, label, label_for_not_present, accel_form)
				do_noun_adj_form(this_data, alternant_multiword_spec, slot, label, label_for_not_present, accel_form)
			end

			local function insert_label(label)
				table.insert(this_data.inflections, {label = label})
			end

			if is_proper then
				insert_label(glossary_link("proper noun"))
			end
			if not alternant_multiword_spec.first_noun and alternant_multiword_spec.first_adj then
				insert_label("adjectival")
			end
			local def_prefix
			if alternant_multiword_spec.definiteness == "def" then
				def_prefix = "def_"
				insert_label(glossary_link("definite") .. " only")
			else
				def_prefix = "ind_"
			end
			if alternant_multiword_spec.number == "pl" then
				insert_label(glossary_link("plural only"))
			end
			if alternant_multiword_spec.saw_indecl and not alternant_multiword_spec.saw_non_indecl then
				insert_label(glossary_link("indeclinable"))
			elseif alternant_multiword_spec.saw_unknown_decl and not alternant_multiword_spec.saw_non_unknown_decl then
				insert_label("unknown declension")
			elseif alternant_multiword_spec.number == "pl" then
				do_noun_form(def_prefix .. "gen_p", "genitive plural", "genitive plural")
			else
				do_noun_form(def_prefix .. "gen_s", "genitive singular", "genitive singular")
				do_noun_form(def_prefix .. "nom_p", "nominative plural", not is_proper and "plural" or nil)
			end

			if i == 1 then
				-- Parse and insert an inflection not requiring additional processing into `this_data.inflections`. The raw
				-- arguments come from `args[field]`, which is parsed for inline modifiers. If there is a corresponding
				-- qualifier field `FIELD_qual`, qualifiers may additionally come from there. `label` is the label that the
				-- inflections are given, which is linked to the glossary if surrounded by <<..>> (which is removed).
				-- `plpos` is the plural part of speech, used in [[Category:LANGNAME PLPOS with red links in their headword
				-- lines]]. `accel` is the accelerator form, or nil.
				local function handle_infl(field, label, frob)
					m_headword_utilities.parse_and_insert_inflection {
						headdata = this_data,
						forms = args[field],
						paramname = field,
						label = label,
						frob = frob,
					}
				end

				handle_infl("m", "male equivalent")
				handle_infl("f", "female equivalent")
				handle_infl("dim", "<<diminutive>>")
				handle_infl("aug", "<<augmentative>>")
				handle_infl("pej", "<<pejorative>>")
				handle_infl("dem", "<<demonym>>")
				handle_infl("fdem", "female <<demonym>>")
			end

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

			insert_noun_adj_linked_lemma_if_needed(this_data, alternant_multiword_spec, is_noun_module)
		end
	end

	if #all_genders > 1 then
		table.insert(multiple_data[1].categories, data.langname .. " " .. pluralize(pos or "noun") ..
			" with multiple genders")
	end

	return multiple_data
end

local function get_noun_pos_functions(is_proper)
	return {
		params = function(lang)
			if lang:getCode() == "is" then
				return get_auto_noun_params(lang, is_proper)
			else
				return get_manual_noun_params(lang, is_proper)
			end
		end,
		func = function(args, data)
			if data.lang:getCode() == "is" then
				return do_auto_nouns(is_proper, args, data)
			else
				return do_manual_nouns(is_proper, args, data)
			end
		end,
		process_multiple_headword_data = get_process_noun_adj_multiple_data("is-ndecl"),
	}
end

pos_functions["nouns"] = get_noun_pos_functions(false)
pos_functions["proper nouns"] = get_noun_pos_functions("proper noun")

local function do_comparative_superlative(args, data, plpos)
	if args[1][1] == "-" then
		table.insert(data.inflections, {label = "not " .. glossary_link("comparable")})
		table.insert(data.categories, data.langname .. " uncomparable " .. plpos)
	elseif args[1][1] or args[2][1] then
		local comp = m_headword_utilities.parse_term_list_with_modifiers {
			paramname = {1, "comp"},
			forms = args[1],
		}
		local sup = m_headword_utilities.parse_term_list_with_modifiers {
			paramname = {2, "sup"},
			forms = args[2],
		}
		if comp[1] then
			comp.label = glossary_link("comparative")
			-- comp.accel = {form = "comparative"}
			table.insert(data.inflections, comp)
		end
		if sup[1] then
			sup.label = glossary_link("superlative")
			-- sup.accel = {form = "superlative"}
			table.insert(data.inflections, sup)
		end
		table.insert(data.categories, data.langname .. " comparable " .. plpos)
	end
end

local function get_manual_adjective_params(lang)
	return {
		[1] = {list = "comp", disallow_holes = true},
		[2] = {list = "sup", disallow_holes = true},
		["adv"] = list_param,
		["indecl"] = {type = "boolean"},
	}
end

local function do_manual_adjectives(args, data)
	if args.indecl then
		table.insert(data.inflections, {label = glossary_link("indeclinable")})
		table.insert(data.categories, data.langname .. " indeclinable adjectives")
	end
	do_comparative_superlative(args, data, "adjectives")
	m_headword_utilities.parse_and_insert_inflection {
		headdata = data,
		forms = args.adv,
		paramname = "adv",
		label = "adverb",
	}
end

local function get_auto_adjective_params(lang)
	return {
        [1] = {required = true, list = true, default = "djúpur<comp:^>"},
        ["pos"] = {},
		["adv"] = list_param,
	}
end

local function do_auto_adjectives(args, data)
	if data.lang:getCode() ~= "is" then
		error("Internal error: Only Icelandic supported at the moment")
	end
	local m_is_adjective = require(is_adjective_module)
	local m_is_common = require(is_common_module)
	local alternant_multiword_specs = {}
	local multiple_data = {}
	local declspecs, pos, scrape_error_data = parse_noun_adj_scraping_specs(data, args[1], args.pos, "is-adecl",
		"allow empty infl")
	if scrape_error_data then
		return scrape_error_data
	end
	if pos then
		data.pos_category = pluralize(pos)
		args.pos = pos
	end
	for i, declspec in ipairs(declspecs) do
		local alternant_multiword_spec = m_is_adjective.do_generate_forms(args, declspec, "is-adj")
		alternant_multiword_specs[i] = alternant_multiword_spec
		local this_data
		if i < #declspecs then
			this_data = m_table.shallowCopy(data)
			this_data.categories = {}
			this_data.inflections = {}
		else
			-- for the last (or usually only) spec, save memory by not cloning
			this_data = data
		end
		multiple_data[i] = this_data
		this_data.alternant_multiword_spec = alternant_multiword_spec
		this_data.heads = args.head
		if not alternant_multiword_spec.scrape_errors then
			local function do_adjective_form(slot, label, label_for_not_present, accel_form)
				do_noun_adj_form(this_data, alternant_multiword_spec, slot, label, label_for_not_present, accel_form)
			end

			local function insert_label(label)
				table.insert(this_data.inflections, {label = label})
			end

			if alternant_multiword_spec.saw_indecl and not alternant_multiword_spec.saw_non_indecl then
				insert_label(glossary_link("indeclinable"))
			elseif alternant_multiword_spec.saw_unknown_decl and not alternant_multiword_spec.saw_non_unknown_decl then
				insert_label("unknown declension")
			end
			if alternant_multiword_spec.saw_pred and not alternant_multiword_spec.saw_non_pred then
				insert_label("predicate-only")
			end

			local function do_superlative()
				do_adjective_form("sup_str_nom_m", glossary_link("superlative"), nil, "superlative")
			end

			if alternant_multiword_spec.haspos == "has" then
				if alternant_multiword_spec.number.pos == "sg" then
					insert_label(glossary_link("singular only"))
				elseif alternant_multiword_spec.number.pos == "pl" then
					insert_label(glossary_link("plural only"))
				end
				if alternant_multiword_spec.state.pos == "strong" and alternant_multiword_spec.hascomp ~= "has" and
					alternant_multiword_spec.hassup ~= "has" then
					local pl = alternant_multiword_spec.number.pos == "pl" and "p" or ""
					do_adjective_form("str_nom_f" .. pl, glossary_link("feminine"))
					do_adjective_form("str_nom_n" .. pl, glossary_link("neuter"))
				else
					if alternant_multiword_spec.state.pos == "strong" or alternant_multiword_spec.state.pos == "weak" then
						-- not "both" or "none"
						insert_label(alternant_multiword_spec.state.pos .. " only")
					end
					if alternant_multiword_spec.hascomp == "has" then
						do_adjective_form("comp_wk_nom_m", glossary_link("comparative"), nil, "comparative")
						if alternant_multiword_spec.hassup == "has" then
							do_superlative()
						elseif alternant_multiword_spec.hassup == "hasnot" then
							insert_label("no " .. glossary_link("superlative"))
						end
					elseif alternant_multiword_spec.hascomp == "hasnot" then
						if alternant_multiword_spec.hassup == "has" then
							insert_label("no " .. glossary_link("comparative"))
							do_superlative()
						else
							insert_label("not " .. glossary_link("comparable"))
						end
					end
				end
			elseif alternant_multiword_spec.hascomp == "has" then
				insert_label(glossary_link("comparative") .. " only")
				if alternant_multiword_spec.hassup == "has" then
					do_superlative()
				end
			else
				insert_label(glossary_link("superlative") .. " only")
			end

			if i == 1 then
				-- Parse and insert an inflection not requiring additional processing into `this_data.inflections`. The raw
				-- arguments come from `args[field]`, which is parsed for inline modifiers. If there is a corresponding
				-- qualifier field `FIELD_qual`, qualifiers may additionally come from there. `label` is the label that the
				-- inflections are given, which is linked to the glossary if surrounded by <<..>> (which is removed).
				-- `plpos` is the plural part of speech, used in [[Category:LANGNAME PLPOS with red links in their headword
				-- lines]]. `accel` is the accelerator form, or nil.
				local function handle_infl(field, label, frob)
					m_headword_utilities.parse_and_insert_inflection {
						headdata = this_data,
						forms = args[field],
						paramname = field,
						label = label,
						frob = frob,
					}
				end

				handle_infl("adv", "adverb")
			end

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

			insert_noun_adj_linked_lemma_if_needed(this_data, alternant_multiword_spec, is_adjective_module)
		end
	end

	return multiple_data
end

local function get_adjective_pos_functions()
	return {
		params = function(lang)
			if lang:getCode() == "is" then
				return get_auto_adjective_params(lang)
			else
				return get_manual_adjective_params(lang)
			end
		end,
		func = function(args, data)
			if data.lang:getCode() == "is" then
				return do_auto_adjectives(args, data)
			else
				return do_manual_adjectives(args, data)
			end
		end,
		process_multiple_headword_data = get_process_noun_adj_multiple_data("is-adecl"),
	}
end

pos_functions["adjectives"] = get_adjective_pos_functions()

pos_functions["adverbs"] = {
	params = {
		[1] = {list = "comp", disallow_holes = true},
		[2] = {list = "sup", disallow_holes = true},
	},
	func = function(args, data)
		do_comparative_superlative(args, data, "adverbs")
	end,
}


return export