မဝ်ဂျူ:vot-pronunciation

နူ ဝိက်ရှေန်နရဳ

Documentation for this module may be created at မဝ်ဂျူ:vot-pronunciation/doc

local export = {}
local m_vot = require("Module:vot")
local m_IPA = require("Module:IPA")
local gsub_lookahead = require("Module:gsub lookahead")

local lang = m_vot.lang
local U = mw.ustring.char

--- <<< DATA START >>> ---

local LONG = "ː"
local STRESS_PRIMARY = "ˈ"
local STRESS_SECONDARY = "ˌ"
local FRONTAL = U(0x0308)
local NONSYLLABIC = U(0x032F)
local TIE = U(0x0361)
local VERYSHORT = U(0x0306)
local PALATAL = "ʲ"
local IPA_VOWELS = "ɑeiouyæøɤɨə"
local AUTO_STRESS = U(0xEEEE)

local IPA_CONSONANTS = m_vot.consonants .. "ɫcčCɟɕʑɲʎï"
local IPA_CONSONANTS_GEMINATABLE = m_vot.consonants_geminatable .. "ɫcčCɕʑɲʎ"

local PALATALIZE = m_vot.palatalize
local UNGEMINATE = "/"
local SHIFT_STRESS = "*"
local ANY_DIACRITICS = "[" .. U(0x0300) .. "-" .. U(0x036F) .. "]*"
local SOME_DIACRITICS = "[" .. U(0x0300) .. "-" .. U(0x036F) .. "]+"

local IPA_VOWEL = "[aõäöü" .. IPA_VOWELS .. "]"

local sep_symbols = m_vot.sep_symbols .. "%*"

--- <<< DATA END >>> ---

--- <<< COMMON START >>> ---

local function split_syllables(word, keep_sep_symbols)
	local res = {}
	local syllable = ""
	local pos = 1
	local found_vowel = false

    -- the following consonants stick together
	
	while pos <= #word do
        if mw.ustring.find(mw.ustring.lower(word), "^[" .. IPA_CONSONANTS .. "][" .. PALATAL .. PALATALIZE .. "]?" .. IPA_VOWEL, pos) then
			-- CV: end current syllable if we have found a vowel
			if found_vowel then
				if syllable then table.insert(res, syllable) end
				found_vowel = false
				syllable = ""
			end
			syllable = syllable .. mw.ustring.sub(word, pos, pos)
			pos = pos + 1
		elseif mw.ustring.find(mw.ustring.lower(word), "^[" .. IPA_CONSONANTS .. "]", pos) then
			-- C: continue
			syllable = syllable .. mw.ustring.sub(word, pos, pos)
			pos = pos + 1
		elseif mw.ustring.find(mw.ustring.lower(word), "^" .. IPA_VOWEL, pos) then
			if found_vowel then
				-- already found a vowel, end current syllable
				if syllable then
					table.insert(res, syllable)
				end
				syllable = ""
			end	
			found_vowel = true
			
			-- check for diphthongs or long vowels
			local seq_ok = false
			for k, v in pairs(m_vot.vowel_sequences) do
				if mw.ustring.find(mw.ustring.lower(word), "^" .. v, pos) then
					seq_ok = true
					break
				end
			end
			
			if seq_ok then
				syllable = syllable .. mw.ustring.sub(word, pos, pos + 1)
				pos = pos + 2
			else
				syllable = syllable .. mw.ustring.sub(word, pos, pos)
				pos = pos + 1
			end
		elseif mw.ustring.find(mw.ustring.lower(word), "^[" .. PALATALIZE .. PALATAL .. "]", pos) then
			syllable = syllable .. PALATALIZE
			pos = pos + 1
		elseif mw.ustring.find(mw.ustring.lower(word), "^[" .. sep_symbols .. "]", pos) then
			-- separates syllables
			if syllable then
				table.insert(res, syllable)
			end
			
			local sepchar = mw.ustring.sub(word, pos, pos)
            syllable = keep_sep_symbols and sepchar or ""
			pos = pos + 1
			found_vowel = false
		else
			-- ?: continue
			syllable = syllable .. mw.ustring.sub(word, pos, pos)
			pos = pos + 1
		end
	end
	
	if syllable then
		table.insert(res, syllable)
	end
	
	return res
end

local function zeroth_round_of_common_replacements(text, narrow)
	if narrow then
		text = mw.ustring.gsub(text, "l([aouõ])", "ɫ%1")
		text = mw.ustring.gsub(text, "lɫ", "ɫɫ")
		text = mw.ustring.gsub(text, "([^" .. STRESS_PRIMARY .. STRESS_SECONDARY .. "%*-]+)", function (word)
			if mw.ustring.find(word, "[aouõ]") then
				return mw.ustring.gsub(word, "l(.?)", function (v) return (mw.ustring.match(v, "[eäöü]") and "l" or "ɫ") .. v end)
			else
				return word
			end
		end)
	end
	text = mw.ustring.gsub(text, "tts", "cc")
	text = mw.ustring.gsub(text, "ts", "c")
	text = mw.ustring.gsub(text, "ttš", "čč")
	text = mw.ustring.gsub(text, "tš", "č")
	text = mw.ustring.gsub(text, PALATALIZE .. "(" .. m_vot.consonants .. PALATALIZE .. ")", "%1")
	return text
end

local function first_round_of_common_replacements(text)
	text = mw.ustring.gsub(text, "n([-" .. AUTO_STRESS .. "]?[kg])", "ŋ%1")
	text = mw.ustring.gsub(text, "[aäöõü’]", {
		["a"] = "ɑ",
		["ä"] = "æ",
		["ö"] = "ø",
		["õ"] = "ɤ",
		["ü"] = "y",
		["’"] = ".",
--		["-"] = STRESS_SECONDARY,
	})

	return text
end

local function second_round_of_common_replacements(text, narrow)
	text = mw.ustring.gsub(text, "%" .. SHIFT_STRESS, STRESS_PRIMARY)
	text = mw.ustring.gsub(text, "[cčCgïjšž]", {
		["c"] = "t͡s",
		["č"] = "t͡ʃ",
		["C"] = "c",
		["g"] = "ɡ",
		["ï"] = "j",
		["j"] = "ʝ",
		["š"] = "ʃ",
		["ž"] = "ʒ"
	})
	return text
end

local function automatic_palatalization(text, filter)
	return mw.ustring.gsub(text, "(" .. filter .. ")(" .. m_vot.consonants .. "?)i", function (c1, c2)
		if #c2 == 0 then
			return c1 .. PALATAL .. "i"
		elseif mw.ustring.match(c2, filter) then
			return c1 .. PALATAL .. c2 .. PALATAL .. "i"
		else
			return c1 .. PALATAL .. c2 .. "i"
		end
	end)
end

local full_palatal = {
	["d"] = "ɟ", ["t"] = "C", -- workaround
	["z"] = "ʑ", ["n"] = "ɲ", ["l"] = "ʎ", ["s"] = "ɕ"
}

local function manual_palatalization(text)
	if not mw.ustring.find(text, PALATALIZE) then return text end
	text = mw.ustring.gsub(text, "([" .. IPA_CONSONANTS .. "])" .. PALATALIZE, function(c) 
		return full_palatal[c] or c .. PALATAL
	end)
	text = mw.ustring.gsub(text, PALATALIZE, "")
	text = mw.ustring.gsub(text, PALATAL .. PALATAL, PALATAL)
	return text
end

local IPA_diphthongs = {
	"[aeouɤæøy]i",
	"[iouɤ]a",
	"[iøye]æ",
	"[aoɤei]u",
	"[au]ɤ",
	"[æøie]y",
	"[aiæy]e",
	"[ai]o",
}

local function long_vowels_and_diphthongs(text)
	text = mw.ustring.gsub(text, "([" .. IPA_VOWELS .. "])%1", "%1" .. LONG)
	for _, diphthong in ipairs(IPA_diphthongs) do
		local mod_diphthong
		if mw.ustring.find(diphthong, "%]$") then
			mod_diphthong = mw.ustring.gsub(diphthong, "(.)(%[[^%]]-%])", "%1" .. VERYSHORT .. "?%2")
			mod_diphthong = mw.ustring.gsub(diphthong, "(%[[^%]]-%])(%[[^%]]-%])", "%1" .. VERYSHORT .. "?%2")
		else
			mod_diphthong = mw.ustring.sub(diphthong, 1, -2) .. VERYSHORT .. "?" .. mw.ustring.sub(diphthong, -1, -1)
		end
		text = mw.ustring.gsub(text, "(" .. mod_diphthong .. ")", "%1" .. NONSYLLABIC)
	end
	return text
end

local function long_consonants(text)
	text = mw.ustring.gsub(text, "(%a)" .. PALATAL .. "%1" .. PALATAL, "%1" .. PALATAL .. LONG)
	text = mw.ustring.gsub(text, "(%a)%1", "%1" .. LONG)
	text = mw.ustring.gsub(text, LONG .. PALATAL, PALATAL .. LONG)
	return text
end

local function add_primary_stress(text)
	text = mw.ustring.gsub(text, AUTO_STRESS, "-")
	text = mw.ustring.gsub(text, "-%.", "-")
	text = mw.ustring.gsub(text, "-", STRESS_SECONDARY)
	text = STRESS_PRIMARY .. mw.ustring.gsub(text, " ", " " .. STRESS_PRIMARY)
	text = mw.ustring.gsub(text, STRESS_PRIMARY .. "([^ ]+" .. STRESS_PRIMARY .. ")", "%1")
	return mw.ustring.toNFC(text)
end

local function is_stressed_syllable(syllable)
	return mw.ustring.find(syllable, "^[ " .. AUTO_STRESS .. "%*-]")
end

local function add_secondary_stress(syllables)
	local distance = 0
	for index, syllable in ipairs(syllables) do
		if index == #syllables then break end
		local stressed = index == 1 or is_stressed_syllable(syllable)
		if stressed then
			distance = 0
		else
			distance = distance + 1
			if distance == 2 then
				distance = 0
				if not is_stressed_syllable(syllables[index + 1]) then
					syllables[index] = AUTO_STRESS .. syllable
				end
			end
		end
	end
end

local function clean_ungeminate(text)
	return mw.ustring.gsub(text, UNGEMINATE, "")
end

local function do_gemination(syllables, diacritic)
	local try_to_geminate = false
	for index, syllable in ipairs(syllables) do
		local stressed = index == 1 or is_stressed_syllable(syllable)
		if try_to_geminate and not stressed then
			-- check if the initial consonant in this syllable is followed by two vowels
			local rest = syllable .. (syllables[index + 1] or "")
			if mw.ustring.find(rest, "^[" .. IPA_CONSONANTS_GEMINATABLE .. "]" .. PALATALIZE .. "?" .. m_vot.vowel .. m_vot.vowel) then
				-- CVCVV -> CVC:VV
				local cg = select(3, mw.ustring.find(syllable, "^([" .. IPA_CONSONANTS_GEMINATABLE .. "]" .. PALATALIZE .. "?)"))
				syllables[index - 1] = syllables[index - 1] .. cg
				syllables[index] = mw.ustring.gsub(syllable, "^" .. cg, diacritic)
			end
		end
		try_to_geminate = stressed and mw.ustring.find(syllable, "^[ " .. AUTO_STRESS .. "-]?[" .. IPA_CONSONANTS .. PALATALIZE .. TIE .. "]*" .. m_vot.vowel .. "$")
	end
end

local function split_syllables_by_words(syllables)
	local i = 1
	return function()
		local r = {}
		local e = i
		if e <= #syllables then
			table.insert(r, (mw.ustring.gsub(syllables[e], "^%s+", "")))
			e = e + 1
			while e <= #syllables and not mw.ustring.find(syllables[e], "^%s") do
				table.insert(r, syllables[e])
				e = e + 1
			end
			i = e
			return r
		end
	end
end

local function do_by_word_syllables(out_syllables, fn)
	local old_syllables = {}
	for k, v in pairs(out_syllables) do
		old_syllables[k] = v
		out_syllables[k] = nil
	end
	local next_word = false
	for syllables in split_syllables_by_words(old_syllables) do
		fn(syllables)
		for i, syllable in ipairs(syllables) do
			if next_word and i == 1 then
				table.insert(out_syllables, " " .. syllable)
			else
				table.insert(out_syllables, syllable)
			end
		end
		next_word = true
	end
end

local function reduce_final_syllable(syl)
	local allowed_finals = {
		"(" .. m_vot.consonant .. ")%1",
		"g[lɫnr]",
		"mp",
		"šk",
		"lt"
	}

	if not mw.ustring.find(syl, m_vot.consonant .. m_vot.consonant .. PALATALIZE .. "?$") then
		return syl
	end
	for _, allowed_final in ipairs(allowed_finals) do
		if not mw.ustring.find(syl, allowed_final .. PALATALIZE .. "?$") then
			return mw.ustring.gsub(syl, PALATALIZE .. "$", "")
		end
	end
	return mw.ustring.sub(mw.ustring.gsub(syl, PALATALIZE .. "$", ""), 1, -2)
end

local function is_syllable_stressed_at(syllable, index)
	return index == 1 or is_stressed_syllable(syllable)
end

local function do_reduction_word(syllables, narrow, reduce_completely)
	local prev_was_stressed = false
	local prev_was_long = false
	local syllables_since_last_stressed = 0
	local final_vowel_dropped = false
	for index, syllable in ipairs(syllables) do
		local stressed = is_syllable_stressed_at(syllable, index)
		local final = index == #syllables
		if stressed then
			syllables_since_last_stressed = 0
		else
			syllables_since_last_stressed = syllables_since_last_stressed + 1
		end
		prev_was_long = prev_was_long
		
		if not stressed and ((prev_was_stressed and prev_was_long) or (syllables_since_last_stressed > 1 or prev_was_long)) then
			syllables[index] = mw.ustring.gsub(syllable, "(" .. m_vot.vowel .. "+)(.*)", function (nucleus, coda)
				if mw.ustring.find(nucleus, "(" .. m_vot.vowel .. ")%1") then
					return mw.ustring.sub(nucleus, 1, 1) .. coda
				end

				if not narrow then
					local broad_reduce = { ["a"] = "õ", ["ä"] = "e" }
					return (broad_reduce[nucleus] or nucleus) .. coda
				end

				--if mw.ustring.find(nucleus, "i[aä]") then
					--return (syllable.find(PALATALIZE) and "" or PALATALIZE) .. mw.ustring.sub(nucleus, 2) .. coda
				--end

				if mw.ustring.find(nucleus, m_vot.vowel .. m_vot.vowel) then
					return nucleus .. coda
				end

				local reduced = {
					["a"] = "ə", ["ä"] = "ə"
				}
				
				if not reduced[nucleus] then
					return nucleus .. coda
				end
				
				if final and reduce_completely and #coda < 1 and mw.ustring.match(nucleus, "[aä]") then
					if mw.ustring.find(syllable, "j[aä]") then
						return reduced[nucleus] .. VERYSHORT
					end

					final_vowel_dropped = true
					return ""
				end

				return (reduced[nucleus] or nucleus) .. coda
			end)
		end
		-- reduce the next syllable only if the current syllable is stressed and heavy
		prev_was_stressed = stressed
		prev_was_long = mw.ustring.find(syllable, m_vot.vowel .. "[" .. IPA_CONSONANTS .. m_vot.vowels .. "]")
	end

	if final_vowel_dropped then
		syllables[#syllables - 1] = reduce_final_syllable(syllables[#syllables - 1] .. syllables[#syllables])
		syllables[#syllables] = nil
	end
end

local function do_reduction(syllables, narrow, reduce_completely)
	do_by_word_syllables(syllables, function(s) do_reduction_word(s, narrow, reduce_completely) end)
end

local diphthongize_broad = {
	["e"] = "ie", ["o"] = "uo", ["ø"] = "yø"
}
local diphthongize_narrow = {
	["e"] = "ɪ̆e", ["o"] = "ʊ̆o", ["ø"] = "ʏ̆ø"
}
local function do_diphthongization_word(syllables, narrow, reduce_completely)
	for index, syllable in ipairs(syllables) do
		local stressed = is_syllable_stressed_at(syllable, index)
		syllables[index] = mw.ustring.gsub(syllable, "([eoø])%1", function (v)
			return ((narrow and not stressed) and diphthongize_narrow or diphthongize_broad)[v]
		end)
	end
end

local function do_diphthongization(syllables, narrow)
	do_by_word_syllables(syllables, function(s) do_diphthongization_word(s, narrow) end)
end

local function pass_diacritics_through(map, consonant)
	local consonant, diacritics = mw.ustring.match(consonant, "([" .. IPA_CONSONANTS .. "])([" .. PALATAL .. "]?)")
	return map[consonant] .. diacritics
end

local voiceless_sounds = "kptcčfsšh"
local function do_voicing(text)
	local devoice = { ["g"] = "k", ["b"] = "p", ["d"] = "t", ["z"] = "s", ["ž"] = "š", ["ʑ"] = "ɕ" }
	local semivoice = { ["g"] = "g̊", ["b"] = "b̥", ["d"] = "d̥", ["z"] = "z̥", ["ž"] = "ž̥", ["ʑ"] = "ɕ̊" }

	local consonants_to_devoice = "[bdgzž][" .. PALATAL .. "]?"
	local vowel = "[" .. IPA_VOWELS .. "]"

	-- b/d/g/z/ž is semivoiced if it is not followed by anything
	text = mw.ustring.gsub(text, "(" .. consonants_to_devoice .. ")$",
		function (consonant)
			return pass_diacritics_through(semivoice, consonant)
		end)

	-- b/d/g/z/ž is devoiced if it is followed by a voiceless sound
	text = gsub_lookahead(text, "(" .. consonants_to_devoice .. ")([%s" .. AUTO_STRESS .. "-]+)([" .. voiceless_sounds .. "])",
		function (consonant, space, after)
			return pass_diacritics_through(devoice, consonant) .. space, after
		end)

	return text
end


--- <<< COMMON END >>> ---

--- <<< DIALECTS START >>> ---

-- narrow_level 0 = broad, 1 = rhyme, 2 = narrow

-- Luuditsa, Liivtšülä
local function IPA_luuditsa_liivtsula(text, narrow_level)
	text = zeroth_round_of_common_replacements(text, narrow_level > 1)

	if narrow_level > 0 then
		local syllables = split_syllables(text, true)
		add_secondary_stress(syllables)
		text = table.concat(syllables)
	end

	local syllables = split_syllables(text, true)
	if narrow_level > 1 then
		do_gemination(syllables, LONG)
		do_reduction(syllables, true, false)
	end
	text = table.concat(syllables)
	if narrow_level > 0 then text = do_voicing(text) end

	if narrow_level > 1 then
		text = automatic_palatalization(text, "[dnrstz]") -- palatalization
		text = mw.ustring.gsub(text, "h([kg])", "x%1")
	end

	text = clean_ungeminate(text)
	text = mw.ustring.gsub(text, "j" .. PALATALIZE, PALATALIZE)
	text = manual_palatalization(text)
	text = first_round_of_common_replacements(text)
	text = long_vowels_and_diphthongs(text)
	text = long_consonants(text)
	text = second_round_of_common_replacements(text, narrow_level > 1)

	return add_primary_stress(text)
end

-- Jõgõperä
local function IPA_jogopera(text, narrow_level)
	text = zeroth_round_of_common_replacements(text, narrow_level > 1)

	if narrow_level > 0 then
		local syllables = split_syllables(text, true)
		add_secondary_stress(syllables)
		text = table.concat(syllables)
	end

	local syllables = split_syllables(text, true)
	if narrow_level > 1 then
		do_gemination(syllables, LONG)
		do_reduction(syllables, true, true)
	end
	text = table.concat(syllables)
	if narrow_level > 0 then text = do_voicing(text) end

	if narrow_level > 1 then
		text = automatic_palatalization(text, "[dnrstz]") -- palatalization
		text = mw.ustring.gsub(text, "h([kg])", "x%1")
	end

	text = clean_ungeminate(text)
	text = mw.ustring.gsub(text, "j" .. PALATALIZE, PALATALIZE)
	text = manual_palatalization(text)
	text = first_round_of_common_replacements(text)
	text = long_vowels_and_diphthongs(text)
	text = long_consonants(text)
	text = second_round_of_common_replacements(text, narrow_level > 1)

	return add_primary_stress(text)
end

-- Kattila
local function IPA_kattila(text, narrow_level)
	text = zeroth_round_of_common_replacements(text, narrow_level > 1)

	if narrow_level > 0 then
		local syllables = split_syllables(text, true)
		add_secondary_stress(syllables)
		text = table.concat(syllables)
	end

	local syllables = split_syllables(text, true)
	if narrow_level > 1 then
		do_gemination(syllables, LONG)
	end
	text = table.concat(syllables)
	if narrow_level > 0 then text = do_voicing(text) end

	if narrow_level > 1 then
		text = automatic_palatalization(text, "[dnrstz]") -- palatalization
		text = mw.ustring.gsub(text, "h([kg])", "x%1")
	end

	text = clean_ungeminate(text)
	text = mw.ustring.gsub(text, "j" .. PALATALIZE, PALATALIZE)
	text = manual_palatalization(text)
	text = first_round_of_common_replacements(text)
	
	if narrow_level > 0 then
		local syllables = split_syllables(text, true)
		do_diphthongization(syllables, narrow_level > 1)
		text = table.concat(syllables)
	end
	
	text = long_vowels_and_diphthongs(text)
	text = long_consonants(text)
	text = second_round_of_common_replacements(text, narrow_level > 1)

	return add_primary_stress(text)
end

--- <<< DIALECTS END >>> ---

--- <<< INTERFACE START >>> ---

local function cleanup_for_hyphenate(text)
	local no_hyph_symbols = "[" .. UNGEMINATE .. "%*-]"
	return mw.ustring.gsub(text, no_hyph_symbols, "")
end

local function run_reductions(text)
	local syllables = m_vot.split_syllables(text, true)
	local prev_was_stressed = false
	local prev_was_long = false
	local syllables_since_last_stressed = 0
	for index, syllable in ipairs(syllables) do
		local stressed = is_syllable_stressed_at(syllable, index)
		local final = index == #syllables
		if stressed then
			syllables_since_last_stressed = 0
		else
			syllables_since_last_stressed = syllables_since_last_stressed + 1
		end
		prev_was_long = prev_was_long
		
		if not stressed and ((prev_was_stressed and prev_was_long) or (syllables_since_last_stressed > 1 or prev_was_long)) then
			syllables[index] = mw.ustring.gsub(syllable, "(" .. m_vot.vowel .. "+)(.*)", function (nucleus, coda)
				if mw.ustring.find(nucleus, "(" .. m_vot.vowel .. ")%1") then
					return mw.ustring.sub(nucleus, 1, 1) .. coda
				end
				
				local broad_reduce = { ["a"] = "õ", ["ä"] = "e" }
				return (broad_reduce[nucleus] or nucleus) .. coda
			end)
		end
		-- reduce the next syllable only if the current syllable is stressed and heavy
		prev_was_stressed = stressed
		prev_was_long = mw.ustring.find(syllable, m_vot.vowel .. "[" .. IPA_CONSONANTS .. m_vot.vowels .. "]")
	end

	return table.concat(syllables, "")
end

local function hyphenate_matches(sp, title)
	local resp = run_reductions(mw.ustring.lower(sp))
	resp = mw.ustring.gsub(cleanup_for_hyphenate(resp), "%.", "")
	resp = mw.ustring.gsub(resp, "([bdgzž])([kpsšt])", function(c1, c2) return ({b = "p", d = "t", g = "k", z = "s", ["ž"] = "š"})[c1] .. c2 end)
	return resp == mw.ustring.lower(title)
end

local function hyphenate(text)
	return m_vot.split_syllables(cleanup_for_hyphenate(text))
end

local function spell_long_consonants(text)
	text = mw.ustring.gsub(text, "(t[sš])" .. "(" .. PALATALIZE .. "?)" .. LONG,
			function (c, p) return "t" .. c .. p end)
	text = mw.ustring.gsub(text, "([" .. m_vot.consonants .. "])" .. "(" .. PALATALIZE .. "?)" .. LONG,
			function (c, p) return c .. c .. p end)
	text = mw.ustring.gsub(text, "iï", "i")
	return text
end

local function generate_rhyme(tuple)
	local text = tuple.rhyme

	local index = mw.ustring.find(text, "[" .. STRESS_PRIMARY .. STRESS_SECONDARY .. "][^" .. STRESS_PRIMARY .. STRESS_SECONDARY .. "]*$")
	if index ~= nil then text = mw.ustring.sub(text, index + 1) end

	index = mw.ustring.find(text, "[" .. IPA_VOWELS .. "]")
	if index == nil then return nil end

	return mw.ustring.sub(text, index)
end

local function make_IPAs(fn, forms, variety)
	local p = {}
	for _, form in ipairs(forms) do
		form = mw.ustring.lower(form)
		local suffix = mw.ustring.find(form, "^%-")
		local prefix = mw.ustring.find(form, "%-$")
		
		if suffix then form = mw.ustring.gsub(form, "^%-", "") end
		if prefix then form = mw.ustring.gsub(form, "%-$", "") end

		local broad = fn(form, 0)
		local rhyme = fn(form, 1)
		local narrow = fn(form, 2)
		
		if prefix then
			broad = broad .. "-"
			rhyme = nil
			narrow = narrow .. "-"
		end
		
		if suffix then
			broad = "-" .. mw.ustring.gsub(broad, "^" .. STRESS_PRIMARY, "")
			rhyme = nil
			narrow = "-" .. mw.ustring.gsub(narrow, "^" .. STRESS_PRIMARY, "")
		end

		table.insert(p, { broad = broad, rhyme = rhyme, narrow = narrow })
	end
	local result = {
		forms = p,
		varieties = { variety }
	}
	return result
end

local function format_IPAs(tuple, title, has_spaces)
	local dialects = require("Module:accent qualifier").format_qualifiers(tuple.varieties)
	local p = {}
	for _, form in ipairs(tuple.forms) do
		table.insert(p, {pron = "/" .. form.broad .. "/"})
		table.insert(p,	{pron = "[" .. form.narrow .. "]"})
	end
	return "* " .. dialects .. " " .. m_IPA.format_IPA_full(lang, p, nil, nil, nil, has_spaces)
end

local function get_arg_list(param, fallback, allow_dash, required)
	if not param or #param == 0 then return required and fallback or nil end
	if not allow_dash and #param == 1 and param[1] == "-" then return nil end
	if #param == 1 and param[1] == "+" then return fallback end
	return param
end

local varieties = {
	["Lu"] = { "ဠူဒေတ်သာ", IPA_luuditsa_liivtsula },
	["Li"] = { "လိယေတ်သဝ်လာ", IPA_luuditsa_liivtsula },
	["J"] = { "ဂျာန်ဂဝ်ဖေအ်ရာ", IPA_jogopera },
	["K"] = { "ကေတ္တဳလာ", IPA_kattila },
}

local variety_groups = {
	{ "LL", {"Lu", "Li"}, true },
	{ nil, "J", false },
	{ nil, "K", false },
}

local varieties_merged = {}
for _, group in ipairs(variety_groups) do
	if group[1] then
		varieties_merged[group[1]] = group[2]
	end
end

local function get_variety(variety_code)
	if varieties[variety_code] then
		return varieties[variety_code]
	end
	if varieties_merged[variety_code] then
		local subvarieties = varieties_merged[variety_code]
		local names = {}
		local fn = nil
		for _, subvariety_code in ipairs(subvarieties) do
			local subvariety = varieties[subvariety_code]
			fn = subvariety[2]
			table.insert(names, subvariety[1])
		end
		return {table.concat(names, ", "), fn}
	end
	error("Unrecognized variety code: " .. variety_code)
end

function export.get_variety(variety_code)
	return get_variety(variety_code)[1]
end

function export.generate_one(form, variety_code, transcription)
	local name, fn = unpack(get_variety(variety_code))
	local result = make_IPAs(fn, {form}, name).forms[1]
	if transcription then result = result[transcription] end
	return result
end

function export.generate_multiple(forms, variety_code, transcription)
	local param, name, fn = unpack(get_variety(variety_code))
	local result = make_IPAs(fn, forms, name).forms
	if transcription then
		for i, form in ipairs(result) do
			result[i] = form[transcription]
		end
	end
	return result
end

local function add_IPAs(IPAs, spellings, main_code, args, required)
	local name, fn = unpack(get_variety(main_code))
	local forms = get_arg_list(args, spellings, false, required)
	if forms then
		table.insert(IPAs, make_IPAs(fn, forms, name))
	end
end

function export.show(frame)
	local title = mw.title.getCurrentTitle().text
	local hyphenation = nil
	local rhymes = nil
	local categories = {}

	local params = {
		[1] = { list = true },

		["LL"] = { list = true }, -- Luuditsa-Liivtšülä
		["Lu"] = { list = true }, -- Luuditsa
		["Li"] = { list = true }, -- Liivtšülä
		["J"] = { list = true }, -- Jõgõperä
		["K"] = { list = true }, -- Kattila,
		
		["dial"] = { type = "boolean" },
		
		["title"] = {}, -- for debugging or demonstration only
	}
	
	local args = require("Module:parameters").process(frame:getParent().args, params)
	title = args["title"] or title
	local dialectal = args.dial

	local spellings = get_arg_list(args[1], { mw.ustring.lower(title) }, true, true)
	local IPAs = {}
	
	for _, variety_group in ipairs(variety_groups) do
		local param, codes, always = unpack(variety_group)
		if param then
			local split = false
			for _, code in ipairs(codes) do
				if #args[code] > 0 then
					split = true
					break
				end
			end
			
			if split then
				for _, code in ipairs(codes) do
					add_IPAs(IPAs, spellings, code, args[code] or (param and args[param] or nil), always and not dialectal)
				end
			else
				add_IPAs(IPAs, spellings, param, args[param], always and not dialectal)
			end
		else
			add_IPAs(IPAs, spellings, codes, args[codes], always and not dialectal)
		end
	end
	
	if #IPAs < 1 then
		error("No dialects to display IPA for")
	end

	local results = {}
	local has_spaces = mw.ustring.find(title, " ")
	
	for _, tuple in ipairs(IPAs) do
		table.insert(results, format_IPAs(tuple, title, has_spaces))
	end

	if not hyphenation then
		hyphenation = {}
		if not has_spaces then
			local sp = spellings[1]
			if not hyphenate_matches(sp, title) then
				-- try to geminate
				local syllables = m_vot.split_syllables(sp, true)
				do_gemination(syllables, LONG)
				sp = spell_long_consonants(clean_ungeminate(table.concat(syllables)))
			end
			if hyphenate_matches(sp, title) then
				table.insert(hyphenation, hyphenate(title))
			end
		end
	end

	if not rhymes then
		rhymes = {}
		if not has_spaces then
			local found_rhymes = {}
			for _, tuple in ipairs(IPAs) do
				for _, form in ipairs(tuple.forms) do
					if form.rhyme then
						local rhyme = generate_rhyme(form)
						if not found_rhymes[rhyme] then
							found_rhymes[rhyme] = true
							table.insert(rhymes, rhyme)
						end
					end
				end
			end
		end
	end



	if #hyphenation > 0 then
		local hyphs = {}
		for i, h in ipairs(hyphenation) do
			table.insert(hyphs, { ["hyph"] = h })
		end
		table.insert(results, "* " .. require("Module:hyphenation").format_hyphenations(
			{ lang = lang, hyphs = hyphs, caption = "ဗီုပြၚ်ဂၠေံဂၠေံ" }))
	end

	return table.concat(results, "\n") .. require("Module:utilities/format_categories")(categories, lang)
end

--- <<< INTERFACE END >>> ---

return export