Jump to content

Module:vot-pronunciation

From Wiktionary, the free dictionary


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 NEVER_STRESSED = "#"
local FRONTAL = U(0x0308)
local NONSYLLABIC = U(0x032F)
local TIE = U(0x0361)
local VERYSHORT = U(0x0306)
local SCHWA_BACK = U(0xEEE0)
local SCHWA_FRONT = U(0xEEE1)
local PALATAL = "ʲ"
local IPA_VOWELS = "ɑeiouyæøɤɨ" .. SCHWA_BACK .. SCHWA_FRONT
local AUTO_STRESS = U(0xEEEE)

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

local PALATALIZE = m_vot.palatalize
local PALATALIZE_WEAK = '"'
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 broken_vowel_sequences = { "i" .. UNGEMINATE .. "i" }

--- <<< 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 <= mw.ustring.len(word) do
        if mw.ustring.find(mw.ustring.lower(word), "^[" .. IPA_CONSONANTS .. "][" .. PALATAL .. PALATALIZE .. PALATALIZE_WEAK .. "]?" .. IPA_VOWEL, pos) then
			-- CV: end current syllable if we have found a vowel
			if found_vowel then
				if #syllable > 0 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 > 0 then table.insert(res, syllable) end
				syllable = ""
			end	
			found_vowel = true
			
			-- check for diphthongs or long vowels
			local seq_ok = false
			local seq_ok3 = false
			for k, v in pairs(broken_vowel_sequences) do
				if mw.ustring.find(mw.ustring.lower(word), "^" .. v, pos) then
					seq_ok3 = true
					break
				end
			end
			
			if not seq_ok3 then
				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
			end

			if seq_ok3 then
				syllable = syllable .. mw.ustring.sub(word, pos, pos + 2)
				pos = pos + 3
			elseif 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 .. PALATALIZE_WEAK .. PALATAL .. "]", pos) then
			syllable = syllable .. mw.ustring.sub(word, pos, pos)
			pos = pos + 1
		elseif mw.ustring.find(mw.ustring.lower(word), "^[" .. UNGEMINATE .. "]", pos) then
			syllable = syllable .. UNGEMINATE
			pos = pos + 1
		elseif mw.ustring.find(mw.ustring.lower(word), "^[" .. m_vot.sep_symbols .. AUTO_STRESS .. NEVER_STRESSED .. "%*]", pos) then
			-- separates syllables
			if #syllable > 0 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 > 0 then
		table.insert(res, syllable)
	end
	
	return res
end

local function zeroth_round_of_common_replacements(text, narrow)
	text = mw.ustring.gsub(text, "'", PALATALIZE)
	text = mw.ustring.gsub(text, PALATALIZE_WEAK, PALATAL)
	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, "[ieäöü" .. PALATALIZE .. "]") 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, "n([-" .. AUTO_STRESS .. "]?[pb])", "m%1")
	text = mw.ustring.gsub(text, "[aäöõü’]", {
		["a"] = "ɑ",
		["ä"] = "æ",
		["ö"] = "ø",
		["õ"] = "ɤ",
		["ü"] = "y",
--		["-"] = STRESS_SECONDARY,
	})

	return text
end

local function second_round_of_common_replacements(text, narrow, apical)
	text = mw.ustring.gsub(text, "%" .. SHIFT_STRESS, STRESS_PRIMARY)
	text = mw.ustring.gsub(text, LONG .. PALATAL, PALATAL .. LONG)
	text = mw.ustring.gsub(text, PALATAL .. "+", PALATAL)
	text = mw.ustring.gsub(text, "[cčCgïjšž" .. SCHWA_BACK .. SCHWA_FRONT .. "]", {
		["c"] = "t͡s",
		["č"] = "t͡ʃ",
		["C"] = "c",
		["g"] = "ɡ",
		["ï"] = narrow and "ʝ" or "j",
		["j"] = narrow and "ʝ" or "j",
		["š"] = apical and "ʂ" or "ʃ",
		["ž"] = apical and "ʐ" or "ʒ",
		[SCHWA_BACK] = "ə̠",
		[SCHWA_FRONT] = "ə̟",
	})
	text = mw.ustring.gsub(text, "[lɫ]([lɫ])", "%1" .. LONG)
	return text
end

local function automatic_palatalization(text, filter) -- , regressive_filter)
	text = mw.ustring.gsub(text, "(" .. filter .. "+)([äöüi" .. SCHWA_FRONT .. "])", function (c1, v1)
		return c1 .. PALATAL .. v1
	end)
	-- text = mw.ustring.gsub(text, "(" .. regressive_filter .. "+)([" .. IPA_CONSONANTS .. "]" .. PALATAL .. ")", function (c1, c2)
	-- 	return c1 .. PALATAL .. c2
	-- end)
	return text
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 = {
	"[ɑeouɤæøy]i",
	"[iouɤ]ɑ",
	"[iøye]æ",
	"[ɑoɤei]u",
	"[ɑu]ɤ",
	"[æøie]y",
	"[ɑiæy]e",
	"[ɑi]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, stress_last)
	local distance = 0
	for index, syllable in ipairs(syllables) do
		if not stress_last and 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 (index == #syllables or not is_stressed_syllable(syllables[index + 1])) and not mw.ustring.find(syllable, NEVER_STRESSED) 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"] = SCHWA_BACK, ["ä"] = SCHWA_FRONT
				}
				
				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 mw.ustring.find(nucleus, "ä") and PALATAL or ""
				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, always_devoiced)
	local devoice = { ["g"] = "k", ["b"] = "p", ["d"] = "t", ["z"] = "s", ["ž"] = "š", ["ʑ"] = "ɕ" }
	local semivoice = { ["g"] = "g̊", ["b"] = "b̥", ["d"] = "d̥", ["z"] = "z̥", ["ž"] = "ž̥", ["ʑ"] = "ɕ̊" }
	if always_devoiced then semivoice = devoice end

	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

local palatalize_filter = "[dfghkmnprstvz]"
local kattila_palatalize_filter = "[dlnrstz]"
-- local regressive_palatalize_filter = "[dnrstz]"

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

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

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

-- Luutsa, Liivtšülä
local function IPA_luutsa_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
	text = mw.ustring.gsub(text, NEVER_STRESSED, "")

	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, palatalize_filter) -- , regressive_palatalize_filter) -- 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
	text = mw.ustring.gsub(text, NEVER_STRESSED, "")

	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, palatalize_filter) -- , regressive_palatalize_filter) -- 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, true)
		text = table.concat(syllables)
	end
	text = mw.ustring.gsub(text, NEVER_STRESSED, "")

	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, true) end

	if narrow_level > 1 then
		text = mw.ustring.gsub(text, "h([kpt])", "H%1")
		text = mw.ustring.gsub(text, "[hH]", {["h"] = "ɦ", ["H"] = "h"})
	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, true)

	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(mw.ustring.gsub(text, no_hyph_symbols, ""), "%*", ".")
end

local function run_reductions(text)
	local syllables = split_syllables(text, true)
	add_secondary_stress(syllables)
	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 mw.ustring.gsub(table.concat(syllables, ""), "[" .. NEVER_STRESSED .. AUTO_STRESS .. "]", "")
end

local function match_spelling_with_title_for_hyphenation(sp, title)
	return title
end

local function hyphenate_matches(sp, title)
	local resp = run_reductions(mw.ustring.lower(mw.ustring.gsub(sp, "%*", ".")))
	resp = mw.ustring.gsub(resp, "'", PALATALIZE)
	resp = mw.ustring.gsub(resp, "([bdgzž])([kpsšt])", function(c1, c2) return ({b = "p", d = "t", g = "k", z = "s", ["ž"] = "š"})[c1] .. c2 end)
	resp = mw.ustring.gsub(cleanup_for_hyphenate(resp), "%.", "")
	resp = mw.ustring.gsub(resp, "([bdgzž])$", function(c) return ({b = "p", d = "t", g = "k", z = "s", ["ž"] = "š"})[c] end)
	title = mw.ustring.lower(title)
	title = mw.ustring.gsub(title, "([bdgzž])$", function(c) return ({b = "p", d = "t", g = "k", z = "s", ["ž"] = "š"})[c] end)
	return resp == 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, varieties)
	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 = varieties
	}
	return result
end

local function link_varieties(varieties)
	local result = {}
	for _, variety in ipairs(varieties) do
		table.insert(result, "[[Appendix:Votic dialects#" .. variety .. "|" .. variety .. "]]")
	end
	return result
end

local function format_IPAs(tuple, title, has_spaces)
	local dialects = require("Module:accent qualifier").format_qualifiers(lang, link_varieties(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 = lang, items = p, no_count = 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"] = { "Luutsa", IPA_luutsa_liivtsula },
	["Li"] = { "Liivtšülä", IPA_luutsa_liivtsula },
	["J"] = { "Jõgõperä", IPA_jogopera },
	["K"] = { "Kattila", 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
		local name, fn = unpack(varieties[variety_code])
		return name, fn, { name }
	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_name, subvariety_fn = unpack(varieties[subvariety_code])
			fn = subvariety_fn
			table.insert(names, subvariety_name)
		end
		return table.concat(names, ", "), fn, names
	end
	error("Unrecognized variety code: " .. variety_code)
end

function export.get_variety(variety_code)
	return (get_variety(variety_code))
end

function export.generate_one(form, variety_code, transcription)
	local name, fn = 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 name, fn = 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, variety_names = get_variety(main_code)
	local forms = get_arg_list(args, spellings, false, required)
	if forms then
		table.insert(IPAs, make_IPAs(fn, forms, variety_names))
	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 }, -- Luutsa-Liivtšülä
		["Lu"] = { list = true }, -- Luutsa
		["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(match_spelling_with_title_for_hyphenation(sp, 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 #rhymes > 0 then
		local sylkeys = {}
		local sylcounts = {}
		-- get all possible syllable counts from syllabifications
		for i, h in ipairs(hyphenation) do
			local hl = #h
			if hl > 0 and not sylkeys[hl] then
				table.insert(sylcounts, hl)
				sylkeys[hl] = true
			end
		end
		local rhymeobjs = {}
		for _, rhyme in ipairs(rhymes) do
			table.insert(rhymeobjs, {rhyme = rhyme})
		end
		table.insert(results, "* " .. require("Module:rhymes").format_rhymes(
			{ lang = lang, rhymes = rhymeobjs, num_syl = sylcounts }))
	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 = "Hyphenation" }))
	end

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

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

return export