Jump to content

Module:roots

From Wiktionary, the free dictionary


local export = {}

local force_cat = false -- for debugging

local M = require("Module:module loader").init({
	require = {
		str_utils = "Module:string utilities",
		utilities = "Module:utilities",
		links = "Module:links",
		headword = "Module:headword",
		languages = "Module:languages",
		parameters = "Module:parameters",
		inflection_table = "Module:inflection-table",
	},
	loadData = {
		radicals = "Module:roots/data/radicals",
	},
	lazy = {
		concat = function() return table.concat end,
		insert = function() return table.insert end,
	},
})

local UI_STRINGS = {
	no_langcode        = "Please provide a language code.",
	invalid_langcode   = "Invalid language code: %s",
	not_a_table        = "rootTable is not a table",
	min_radicals       = "Root must have at least %s %s.",
	max_radicals       = "Root must have at most %s %s.",
	unrecognized_pos   = "Unrecognized %s radical %s in %s",
	unrecognized_mem   = "Unrecognized radical %s in %s",
	not_single_letter  = "'%s' in the root '%s' should be a single letter.",
	invalid_validation = "Unknown validation model: %s",
	only_one_root      = "There should be only one root.",
	bad_root           = "Invalid %s root '%s': %s",
	wrong_separator    = "Radicals should be separated by '%s'.",
	root_vs_pagename   = "Root is different from the pagename.",
	move_to_appendix   = "The root page should be in the Appendix namespace. Please move it to : [[%s]]",
	appendix_subpage   = "Appendix:%s roots/",
	root_category      = "%s terms belonging to the root %s%s",
	derived_category   = "%s terms derived from the %s root %s%s",
	derived_language   = "%s terms derived from %s",
	root_heading       = "\n! [[w:%s|%s]]",
	sense_wrapper      = "&lrm; <span class=\"gloss-content\">(%s)</span>",
	article_semitic    = "Semitic root",
	article_generic    = "Root (linguistics)",
}

local DEFAULT_LINK_TARGET = "bare"
local DEFAULT_HEADWORD    = {}

local function split_root(root, separator)
	if not separator then
		return { root }
	end
	return mw.text.split(root, separator, true)
end

local function radicals_word(count)
	return count == 1 and "radical" or "radicals"
end

function export.get_data_module_name(langcode)
	return "Module:roots/data/" .. langcode
end

local function try_load_data(langcode)
	local ok, data = pcall(mw.loadData, export.get_data_module_name(langcode))
	if ok then
		return data
	end
	return nil
end

function export.get_data(langcode)
	return try_load_data(langcode)
end

local function get_lang_and_data(frame)
	local code = frame.args["lang"]
	if not code or code == "" then
		error(UI_STRINGS.no_langcode)
	end
	local lang = M.languages.getByCode(code)
	if not lang then
		error(UI_STRINGS.invalid_langcode:format(code))
	end
	return lang, try_load_data(code) or {}
end

local function resolve_radicals(data)
	if data.validation == "positional" or data.radicals then
		return data.radicals
	end
	if data.shared_radicals then
		return M.radicals[data.shared_radicals]
	end
	return nil
end

local function transliterate_root(root_string, romanization)
	if not romanization then
		return nil
	end
	local out = root_string
	for _, pair in ipairs(romanization) do
		out = M.str_utils.gsub(out, pair[1], pair[2])
	end
	return out
end

export.transliterate = transliterate_root

local function validate_root(rootTable, joined_root, data, radical_set)
	if type(rootTable) ~= "table" then
		return nil, UI_STRINGS.not_a_table
	end

	local len = #rootTable
	if data.min_radicals and len < data.min_radicals then
		return nil, UI_STRINGS.min_radicals:format(data.min_radicals, radicals_word(data.min_radicals))
	end
	if data.max_radicals and len > data.max_radicals then
		return nil, UI_STRINGS.max_radicals:format(data.max_radicals, radicals_word(data.max_radicals))
	end

	if data.validation == "positional" then
		for i, radical in ipairs(rootTable) do
			local set, label
			if i == 1 then
				set, label = radical_set.initial, "initial"
			elseif i == len then
				set, label = radical_set.final, "final"
			else
				set, label = radical_set.medial, "medial"
			end
			if not set[radical] then
				return nil, UI_STRINGS.unrecognized_pos:format(label, radical, joined_root)
			end
		end
	elseif data.validation == "membership" then
		for _, radical in ipairs(rootTable) do
			if not radical_set[radical] then
				return nil, UI_STRINGS.unrecognized_mem:format(radical, joined_root)
			end
		end
	elseif data.validation == "single_letter" then
		for _, radical in ipairs(rootTable) do
			if mw.ustring.len(radical) ~= 1 then
				return nil, UI_STRINGS.not_single_letter:format(radical, joined_root)
			end
		end
	elseif data.validation ~= nil then
		return nil, UI_STRINGS.invalid_validation:format(tostring(data.validation))
	end

	return true
end

local function link(term, alt, translit, id, lang)
	if term == "" or term == "&mdash;" then
		return term
	end
	return M.links.full_link({
		term = term,
		alt = alt ~= term and alt or nil,
		tr = translit,
		lang = lang,
		id = id,
	})
end

local function root_category(lang, source, root, qualifier)
	local suffix = (qualifier and qualifier ~= "") and (" (" .. qualifier .. ")") or ""
	if lang:getCode() == source:getCode() then
		return UI_STRINGS.root_category:format(lang:getFullName(), root, suffix)
	end
	return UI_STRINGS.derived_category:format(
		lang:getFullName(), source:getCanonicalName(), root, suffix)
end

local function check_root(source, root)
	local data = try_load_data(source:getCode())
	if not data then
		return true
	end

	local bare = (root:gsub("^%*", ""))
	local radicals = split_root(bare, data.separator)

	if data.separator and #radicals == 1 and mw.ustring.len(bare) > 1 then
		return nil, UI_STRINGS.wrong_separator:format(data.separator)
	end

	return validate_root(radicals, bare, data, resolve_radicals(data))
end

function export.get_link_page(lang, root)
	local data = try_load_data(lang:getCode())
	if not data or data.link_target ~= "appendix_subpage" then
		return nil
	end
	export.assert_root(lang, root)
	return UI_STRINGS.appendix_subpage:format(lang:getCanonicalName()) .. (root:gsub("^%*", ""))
end

function export.assert_root(source, root)
	local ok, err = check_root(source, root)
	if not ok then
		error(UI_STRINGS.bad_root:format(source:getCanonicalName(), root, err))
	end
	return true
end

function export.get_categories(lang, source, roots, ids)
	local categories = {}
	ids = ids or {}

	if lang:getCode() ~= source:getCode() then
		M.insert(categories,
			UI_STRINGS.derived_language:format(lang:getFullName(), source:getCanonicalName()))
	end

	for index, root in ipairs(roots) do
		M.insert(categories, root_category(lang, source, root, ids[index]))
		export.assert_root(source, root)
	end

	return categories
end

function export.categorize(lang, source, roots, ids, sort_key)
	return M.utilities.format_categories(
		export.get_categories(lang, source, roots, ids), lang, sort_key, nil, force_cat)
end

function export.show(frame)
	local params = {
		[1] = { required = true, type = "language" },
		[2] = { required = true, type = "language" },
		[3] = { required = true, list = true },
		["id"] = { list = true, allow_holes = true },
		["sort"] = {},
	}

	local args = M.parameters.process(frame:getParent().args, params)
	return export.categorize(args[1], args[2], args[3], args.id, args.sort)
end

function export.root(frame)
	local lang, data = get_lang_and_data(frame)
	local radical_set = resolve_radicals(data)
	local link_target = data.link_target or DEFAULT_LINK_TARGET

	local output = {}
	local categories = {}
	local title = mw.title.getCurrentTitle()
	local namespace = title.nsText

	local params = {
		[1] = { list = true },
		["nocat"] = { type = "boolean" },
		["plain"] = { type = "boolean" },
		["notext"] = { type = "boolean" },
		["sense"] = {},
	}
	if link_target == "bare" then
		params["head"] = true
		params["pagename"] = true
	end

	local args = M.parameters.process(frame:getParent().args, params)
	local roots = args[1]
	local roots_len = #roots

	local plain = args["plain"]
	if frame.args["plain"] then
		plain = true
	end

	local separator = data.separator

	local identity
	local subpage = ""
	if link_target == "appendix_subpage" then
		subpage = UI_STRINGS.appendix_subpage:format(lang:getCanonicalName())
		local fulltitle = title.fullText
		fulltitle = M.str_utils.gsub(fulltitle, M.str_utils.gsub(subpage, "([^%w])", "%%%1"), "")
		identity = fulltitle
	else
		identity = args["pagename"] or mw.loadData("Module:headword/data").pagename
	end

	local rootLetters = {}
	if roots_len == 0 and not args["pagename"] and namespace == "Template" and data.preview_root then
		M.insert(rootLetters, split_root(data.preview_root, separator))
	elseif roots_len ~= 0 then
		for _, root in ipairs(roots) do
			M.insert(rootLetters, split_root(root, separator))
		end
	else
		if link_target == "bare" then
			local head = frame.args["head"]
			if args["head"] ~= nil and identity ~= lang:stripDiacritics(args["head"]) then
				error(UI_STRINGS.root_vs_pagename)
			end
			if head and args["head"] ~= nil then
				identity = args["head"]
			end
		end
		M.insert(rootLetters, split_root(identity, separator))
	end

	local joined_roots = {}
	for i, rootLetter in ipairs(rootLetters) do
		M.insert(joined_roots, M.concat(rootLetter, separator))
		local ok, err = validate_root(rootLetter, joined_roots[i], data, radical_set)
		if not ok then
			error(err)
		end
	end

	local sense = args["sense"]
	local sense_formatted = ""
	if sense ~= nil and sense ~= "" then
		sense_formatted = UI_STRINGS.sense_wrapper:format(sense)
	end

	if identity == joined_roots[1] then
		if link_target == "appendix_subpage" and namespace == "" then
			error(UI_STRINGS.move_to_appendix:format(subpage .. joined_roots[1]))
		end

		if roots_len > 1 then
			error(UI_STRINGS.only_one_root)
		end

		local headword_args = {
			lang = lang,
			pos_category = "roots",
			categories = {},
			heads = { identity },
			nomultiwordcat = true,
		}
		local headword_opts = data.headword or DEFAULT_HEADWORD
		if headword_opts.translits then
			headword_args.translits = { transliterate_root(identity, data.romanization) }
		end
		if headword_opts.nolemmacat then
			headword_args.nolemmacat = true
		end
		M.insert(output, M.headword.full_headword(headword_args))

		if args["nocat"] then
			return M.concat(output)
		end
		return M.concat(output) .. M.concat(categories)
	else
		local link_texts = {}
		local term_counts = {}

		for i, joined_root in ipairs(joined_roots) do
			local link_text
			if link_target == "appendix_subpage" then
				link_text = subpage .. joined_root
			else
				link_text = joined_root
			end
			local translit = data.romanization and transliterate_root(joined_root, data.romanization) or nil
			M.insert(link_texts, link(link_text, joined_root, translit, sense, lang) .. sense_formatted)
			M.insert(categories, M.utilities.format_categories(
				{ root_category(lang, lang, joined_root, sense) }, lang, nil, nil, force_cat))
			M.insert(term_counts, mw.site.stats.pagesInCategory(
				root_category(lang, lang, joined_root, sense), "pages"))
		end

		if args["nocat"] or plain then
			if args["nocat"] then
				return M.concat(link_texts, ", ")
			end
			return M.concat(link_texts, ", ") .. M.concat(categories)
		else
			local link_text_output = ""
			for i, link_text in ipairs(link_texts) do
				link_text_output = link_text_output ..
					"\n|-\n| " .. link_text ..
					"\n|-\n| [[:Category:" ..
					root_category(lang, lang, joined_roots[i], sense) ..
					"|" .. term_counts[i] .. " term" .. (term_counts[i] == 1 and "" or "s") .. "]]\n"
			end

			local wikicode = M.inflection_table.make_top{
				title = "-",
				palette = data.palette,
				class = "floatright tr-alongside"
			}

			local article = lang:inFamily("sem") and UI_STRINGS.article_semitic or UI_STRINGS.article_generic
			wikicode = wikicode .. UI_STRINGS.root_heading:format(article, #term_counts == 1 and "Root" or "Roots")
			wikicode = wikicode .. link_text_output
			wikicode = wikicode .. M.inflection_table.make_bottom{}
			return wikicode .. M.concat(categories)
		end
	end
end

return export