Module:eu-pron/new

Hello, you have come here looking for the meaning of the word Module:eu-pron/new. In DICTIOUS you will not only get to know all the dictionary meanings for the word Module:eu-pron/new, but we will also tell you about its etymology, its characteristics and you will know how to say Module:eu-pron/new in singular and plural. Everything you need to know about the word Module:eu-pron/new you have here. The definition of the word Module:eu-pron/new will help you to be more precise and correct when speaking or writing your texts. Knowing the definition ofModule:eu-pron/new, as well as those of other words, enriches your vocabulary and provides you with more and better linguistic resources.

This module is not to be directly used. It is used by Template:eu-pr, see there for usage.


--Based on Module:es-pronunc by Benwing2.

local export = {}

local m_IPA = require("Module:IPA")
local m_table = require("Module:table")
local put_module = "Module:parse utilities"

local force_cat = false -- for testing

local lang = require("Module:languages").getByCode("eu")

local u = mw.ustring.char
local rfind = mw.ustring.find
local rsubn = mw.ustring.gsub
local rmatch = mw.ustring.match
local rsplit = mw.text.split
local ulower = mw.ustring.lower
local uupper = mw.ustring.upper
local usub = mw.ustring.sub
local ulen = mw.ustring.len
local unfd = mw.ustring.toNFD
local unfc = mw.ustring.toNFC

local TILDE = u(0x0303) -- tilde =  ̃
local SYLDIV = u(0xFFF0) -- used to represent a user-specific syllable divider (.) so we won't change it
local vowel = "aeiouAEIOU" -- vowel; include y so we get single-word y correct and for syllabifying from spelling
local V = "" -- vowel class
local W = "" -- glides, used in a few loanwords
local sylsep = "%-." .. SYLDIV -- hyphen included for syllabifying from spelling
local sylsep_c = ""
local wordsep = "# "
local separator = sylsep .. wordsep
local separator_c = ""
local C = "" -- consonant class including h

-- version of rsubn() that discards all but the first return value
local function rsub(term, foo, bar)
	local retval = rsubn(term, foo, bar)
	return retval
end

-- version of rsubn() that returns a 2nd argument boolean indicating whether
-- a substitution was made.
local function rsubb(term, foo, bar)
	local retval, nsubs = rsubn(term, foo, bar)
	return retval, nsubs > 0
end

-- apply rsub() repeatedly until no change
local function rsub_repeatedly(term, foo, bar)
	while true do
		local new_term = rsub(term, foo, bar)
		if new_term == term then
			return term
		end
		term = new_term
	end
end

local function decompose(text)
	-- decompose everything but ñ and ü
	text = unfd(text)
	text = rsub(text, ".", {
		 = "ñ",
		 = "Ñ",
	})
	return text
end

local function split_on_comma(term)
	if term:find(",%s") then
		return require(put_module).split_on_comma(term)
	else
		return rsplit(term, ",")
	end
end

-- Remove any HTML from the formatted text and resolve links, since the extra characters don't contribute to the
-- displayed length.
local function convert_to_raw_text(text)
	text = rsub(text, "<.->", "")
	if text:find("%[%[") then
		text = require("Module:links").remove_links(text)
	end
	return text
end

-- Return the approximate displayed length in characters.
local function textual_len(text)
	return ulen(convert_to_raw_text(text))
end

local function construct_default_differences(dialect)
	if dialect == "gipuzkoan" then
		return {
			northern_different = false,
			j_different = false,
			need_bisc = false,
		}
	end
	return nil
end

-- Main syllable-division algorithm
local function syllabify_from_spelling_or_pronun(text)

	text = rsub_repeatedly(text, "(" .. V .. ")(" .. C .. W .. "?" .. V .. ")", "%1.%2")
	text = rsub_repeatedly(text, "(" .. V .. C .. ")(" .. C .. V .. ")", "%1.%2")
	text = rsub_repeatedly(text, "(" .. V .. C .. ")(" .. C .. C .. V .. ")", "%1.%2")
	text = rsub_repeatedly(text, "(" .. V .. C .. C .. ")(" .. C .. C .. V .. ")", "%1.%2")
	text = rsub(text, "()%.()", ".%1%2") --the first "ɡ" is the IPA one; the second "g" is the regular one
	text = rsub_repeatedly(text, "(" .. C .. ")%.s(" .. C .. ")", "%1s.%2")
	-- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	text = rsub_repeatedly(text, "(" .. ")ui", "%1u.i")
	text = rsub_repeatedly(text, "(" .. ")()", "%1.%2")
	text = rsub_repeatedly(text, "ii", "i.i")
	text = rsub_repeatedly(text, "(" .. ")u", "%1.u")

	return text
end

local function syllabify_from_spelling(text)
	text = decompose(text)
	-- start at FFF1 because FFF0 is used for SYLDIV
	-- Temporary replacements for characters we want treated as default consonants. The C and related consonant regexes
	-- treat all unknown characters as consonants.
	local TEMP_tz = u(0xFFF1)
	local TEMP_ts = u(0xFFF2)
	local TEMP_tx = u(0xFFF3)
	local TEMP_tx_CAPS = u(0xFFF4)
	local TEMP_ll = u(0xFFF5)
	local TEMP_ll_CAPS = u(0xFFF6)
	local TEMP_dd = u(0xFFF7)
	local TEMP_dd_CAPS = u(0xFFF8)
	local TEMP_tt = u(0xFFF9)
	local TEMP_tt_CAPS = u(0xFFFA)
	local TEMP_rr = u(0xFFB)
	
	-- Change user-specified . into SYLDIV so we don't shuffle it around when dividing into syllables.
	text = text:gsub("%.", SYLDIV)

	-- handle digraphs
	text = rsub(text, "tz", TEMP_tz)
	text = rsub(text, "ts", TEMP_ts)
	text = rsub(text, "tx", TEMP_tx)
	text = rsub(text, "Tx", TEMP_tx_CAPS)
	text = rsub(text, "ll", TEMP_ll)
	text = rsub(text, "Ll", TEMP_ll_CAPS)
	text = rsub(text, "tt", TEMP_tt)
	text = rsub(text, "Tt", TEMP_tt_CAPS)
	text = rsub(text, "dd", TEMP_dd)
	text = rsub(text, "Dd", TEMP_dd_CAPS)
	text = rsub(text, "rr", TEMP_rr)
	
	text = syllabify_from_spelling_or_pronun(text)

	text = text:gsub(SYLDIV, ".")
	text = text:gsub(TEMP_tz, "tz")
	text = text:gsub(TEMP_ts, "ts")
	text = text:gsub(TEMP_tx, "tx")
	text = text:gsub(TEMP_tx_CAPS, "Tx")
	text = text:gsub(TEMP_ll, "ll")
	text = text:gsub(TEMP_ll_CAPS, "Ll")
	text = text:gsub(TEMP_tt, "tt")
	text = text:gsub(TEMP_tt_CAPS, "Tt")
	text = text:gsub(TEMP_dd, "dd")
	text = text:gsub(TEMP_dd_CAPS, "Dd")
	text = text:gsub(TEMP_rr, "rr")
	
	text = unfc(text)
	return text
end


-- Generate the IPA of a given respelling, where a respelling is the representation of the pronunciation of a given
function export.IPA(text, dialect, phonetic)
	local northern = dialect == "northern"
	local southern = dialect == "gipuzkoan" or dialect == "navarrese" or dialect == "biscayan"
	local nav = dialect == "navarrese"
	local bisc = dialect == "biscayan"
	local northern_different = false
	local j_different = false
	local need_bisc = false
	local J = ""
	local N = ""
	-- start at FFF1 because FFF0 is used for SYLDIV
	local TEMP_Y = u(0xFFF1)
	local TEMP_W = u(0xFFF2)

	--function that gets the last two syllables (used for generating the correct rhymes)
	local function extract_last_syllables(text)
		local words = rsplit(text, " ")
		if #words > 1 then --no rhymes are generated when there are multiple words
			return ""
		end
		text = rsub(text, "()i", "%1")
		text = rsub(text, "()u", "%1")
		local length  = string.len(text)
		local rhyme   = ""
		local count   = -1
		local syl     = 0
		while count > -length do
			if string.find("aeiou", string.sub(text,count,count)) then
				rhyme = string.sub(text,count,-1)
				syl = syl + 1
				if syl == 2 then
					return rhyme
				end
			end
			count = count - 1
		end
		return rhyme
	end

	text = ulower(text or mw.title.getCurrentTitle().text)
	-- decompose everything but ñ and ü
	text = decompose(text)
	-- convert commas and en/en dashes to IPA foot boundaries
	text = rsub(text, "%s*%s*", " | ")
	-- question mark or exclamation point in the middle of a sentence -> IPA foot boundary
	text = rsub(text, "()%s*%s*()", "%1 | %2")

	-- canonicalize multiple spaces and remove leading and trailing spaces
	local function canon_spaces(text)
		text = rsub(text, "%s+", " ")
		text = rsub(text, "^ ", "")
		text = rsub(text, " $", "")
		return text
	end

	text = canon_spaces(text)

	-- Convert hyphens to spaces, to handle ], ], etc.
	text = rsub(text, "%-", " ")
	-- canonicalize multiple spaces again, which may have been introduced by hyphens
	text = canon_spaces(text)
	-- now eliminate punctuation
	text = rsub(text, "", "")
	-- put # at word beginning and end and double ## at text/foot boundary beginning/end
	text = rsub(text, " | ", "# | #")
	text = "##" .. rsub(text, " ", "# #") .. "##"
	
	--determining whether "h" denotes a phoneme, or the lack of palatalization, palatalization when needed
	text = rsub(text, "nh", "N")
	text = rsub(text, "lh", "L")
	text = rsub(text, "()il(*)", (northern and "%1ił%2" or "%1ĺ%2"))
	text = rsub(text, "()in(*)", (northern and "%1iň%2" or "%1ń%2"))
	text = rsub(text, "uin(*)", (northern and "uiň%1" or "uiń%1"))
	text = rsub(text, "uil(*)", (northern and "uił%1" or "uiĺ%1"))
	text = rsub(text, "il(*)", (northern and "ił%1" or "iĺ%1"))
	text = rsub(text, "in(*)", (northern and "iň%1" or "iń%1"))
	if text:find("") then
		northern_different = true
	end
	if extract_last_syllables(text):find("") then
		N = "N"
	end
	text = rsub(text, "ĺ", "ʎ")
	text = rsub(text, "ń", "ɲ")
	text = rsub(text, "N", "n")
	text = rsub(text, "L", "l")
	text = rsub(text, "ň", "n")
	text = rsub(text, "ł", "l")

	--c, g, q
	text = rsub(text, "ng()", "n%1") -- ], ], etc.
	text = rsub(text, "q", "k") -- ], ], ], ], etc.
	text = rsub(text, "c", "k") -- ], etc.

	--alphabet-to-phoneme
	text = rsub(text, "aje#", "aʒe#") -- final -aje is pronounced /aʒe/ in the North and /axe/ in the South, regardless of how native j is pronounced
	text = rsub(text, "ihoa", "iχoa") -- the <h> in conjugated forms of joan is pronounced in the South 
	text = rsub(text, "zh", "ź")
	text = rsub(text, "tx", "C") --not the real sound
	text = rsub(text, "tz", "Ś") --not the real sound
	text = rsub(text, "ts", "S") --not the real sound
	text = rsubb(text, "ll", "ʎ")
	text = rsubb(text, "dd", "ɟ")
	text = rsubb(text, "tt", "c")
	text = rsub(text, "x", "ʃ")
	text = rsub(text, "#p()", "#%1") -- ], ]
	text = rsub(text, "",
			{  = "ɡ",  = "ɲ",  = "b",  = "ś" })
	if text:find("ś") or text:find("S") then
		need_bisc = true
	end
	--/x/~/j/~/ʒ/ sounds
	if text:find("") then
		northern_different = true
		j_different = true
	end
	if extract_last_syllables(text):find("") then
		J = "J"
	end
	if text:find("ʒ") or text:find("χ") then
		northern_different = true
	end
	if extract_last_syllables(text):find("ʒ") then
		N = "N"
	end
	if text:find("kh") then
		northern_different = true
	end
	if extract_last_syllables(text):find("kh") then
		N = "N"
	end
	if text:find("ź") then
		northern_different = true
	end
	if extract_last_syllables(text):find("ź") then
		N = "N"
	end
	
	text = rsubb(text, "j", (northern and "ɟ" or "x"))
	text = rsubb(text, "y", (northern and "ɟ" or "j")) -- <y> is used for words which always have a palatal
	text = rsubb(text, "kh", (northern and "k" or "χ"))
	text = rsubb(text, "ź", (northern and "ʒ" or "ʃ"))

	-- trilled r (in all cases except between vowels)
	text = rsub(text, "()r()", "%1ɾ%2")
	text = rsub(text, "()r()", "%1ɾ%2") -- it has to be applied twice in order to handle VrVrV sequences
	text = rsub(text, "rr", "r")

	text = rsub(text, "n(*)", "m%1")
	
	-- lack of /h/ in Southern dialects
	text = rsub(text, "aha", (northern and "aha" or "a"))
	text = rsub(text, "ehe", (northern and "ehe" or "e"))
	text = rsub(text, "ihi", (northern and "ihi" or "i"))
	text = rsub(text, "oho", (northern and "oho" or "o"))
	text = rsub(text, "uhu", (northern and "uhu" or "u"))
	text = rsub(text, "h", (northern and "h" or ""))

	text = rsub(text, "n(*)", "m%1")

	-- handle i/u between vowels
	text = rsub_repeatedly(text, "ui", "U") --this dipthong requires special treatment
	local vowel_to_glide = {  = "i.",  = "u." }
	text = rsub_repeatedly(text, "(" .. V .. "*)()(" .. V .. ")",
			function(v1, iu, v2)
				return v1 .. vowel_to_glide .. v2
			end
	)
	text = rsub_repeatedly(text, "U", "ui")

	--syllable division
	text = syllabify_from_spelling_or_pronun(text)

	--dialectal differences
	if nav then
		text = rsub(text, "x", "j")
	elseif bisc then --fake symbols, to be replaced later
		text = rsub(text, "x", "đ")
		text = rsub(text, "j", "đ")
		text = rsub(text, "ś", "s")
		text = rsub(text, "S", "Ś")
	end

	text = rsubb(text, "ʒ", (southern and "x" or "ʒ"))
	text = rsubb(text, "χ", (southern and "x" or "h"))
	
	--phonetic transcription
	if phonetic then
		-- θ, s, f before voiced consonants
		local voiced = "mnɲbdɟɡʎljđ"
		local r = "ɾr"
		local tovoiced = {
			 = "z̺",
			 = "z̻",
		}
		local function voice(sound, following)
			return tovoiced .. following
		end
		text = rsub(text, "()(" .. separator_c .. "*)", voice)

		-- fricative vs. stop allophones; first convert stops to fricatives, then back to stops
		-- after nasals and sometimes after l
		local stop_to_fricative = { = "β",  = "ð",  = "ɣ"}
		local fricative_to_stop = { = "b",  = "d",  = "ɡ"}
		text = rsub(text, "", stop_to_fricative)
		text = rsub(text, "(" .. separator_c .. "*)()",
			function(nasal, fricative) return nasal .. fricative_to_stop end
		)
		text = rsub(text, "(" .. separator_c .. "*)()",
			function(nasal_l, fricative) return nasal_l .. fricative_to_stop end
		)
		text = rsub(text, "##β", "##b")
		text = rsub(text, "##ð", "##d")
		text = rsub(text, "##ɣ", "##ɡ")

		text = rsub(text, "", { = "t̪",  = "d̪"})

		-- nasal assimilation before consonants
		local labiodental, dentialveolar, alveolopalatal, palatal, velar = "ɱ", "n̪", "nʲ", "ɲ", "ŋ"
		local nasal_assimilation = {
			 = labiodental,
			 = dentialveolar,  = dentialveolar,
			 = alveolopalatal,
			 = alveolopalatal,
			 = alveolopalatal,
			 = palatal,  = palatal,  = palatal,  = palatal,
			 = velar,  = velar,  = velar,
		}
		text = rsub(text, "n(" .. separator_c .. "*)(.)",
				function(stress, following)
					return (nasal_assimilation or "n") .. stress .. following
				end
		)

		-- lateral assimilation before consonants
		text = rsub(text, "l(" .. separator_c .. "*)(.)",
				function(stress, following)
					local l = "l"
					if following == "t" or following == "d" then
						-- dentialveolar
						l = "l̪"
					elseif following == "C" or following == "ʃ" or following == "đ" then
						-- alveolopalatal
						l = "lʲ"
					elseif following == "ɟ" or following == "c" or following == "j" then
						-- alveolopalatal
						l = "ʎ"
					end
					return l .. stress .. following
				end)

		-- voiced fricatives are actually approximants
		text = rsub(text, "()", "%1̞")
		
		--nasalization of vowels
		text = rsub(text, "()i()", "%1" .. TILDE .. "i" .. TILDE .. "%2")
		text = rsub(text, "()u()", "%1" .. TILDE .. "u" .. TILDE .. "%2")
		text = rsub(text, "()()", "%1" .. TILDE .. "%2")
	end
	
	--semivowels
	text = rsub(text, "()" .. TILDE .. "()", "%1" .. TILDE .. "%2̯")
	text = rsub(text, "()" .. TILDE .. "()", "%1" .. TILDE .. "%2̯")
	text = rsub(text, "()", "%1̯")
	text = rsub(text, "()", "%1̯")
	text = rsub(text, "u̯i̯", "u̯i") --correct the mistake appeareing in words with aui, eui

	-- convert fake symbols to real ones
	local final_conversions = {
		 = "t͡s̺",
		 = "t͡s̻",
		 = "s̺",
		 = "s̻",
		 = "t͡ʃ",
		 = "x",
		 = "d͡ʒ",
		 = "j"
 	}
	text = rsub(text, "", final_conversions)

	-- remove # symbols at word and text boundaries
	text = rsub(text, "#", "")
	text = mw.ustring.toNFC(text)

	--add technical characters to the end of the rhyme in order to parse the rhymes correctly
	text = text .. J .. N

	local differences = nil
	if dialect == "gipuzkoan" then
		differences = {
			northern_different = northern_different,
			need_bisc = need_bisc,
			j_different = j_different,
		}
	end
	local ret = {
		text = text,
		differences = differences,
	}
	return ret
end

function export.IPA_string(frame)
	local iparams = {
		 = {},
		 = {required = true},
		 = {type = "boolean"},
	}
	local iargs = require("Module:parameters").process(frame.args, iparams)
	local retval = export.IPA(iargs, iargs.style, iargs.phonetic)
	return retval.text
end


-- Generate all relevant dialect pronunciations and group into styles.
local function express_all_styles(style_spec, dodialect)
	local ret = {
		pronun = {},
		expressed_styles = {},
	}

	local need_bisc

	local function express_style(hidden_tag, tag, representative_dialect, matching_styles)
		matching_styles = matching_styles or representative_dialect

		-- If style specified, make sure it matches the requested style.
		local style_matches
		if not style_spec then
			style_matches = true
		else
			local style_parts = rsplit(matching_styles, "%-")
			local or_styles = rsplit(style_spec, "%s*,%s*")
			for _, or_style in ipairs(or_styles) do
				local and_styles = rsplit(or_style, "%s*%+%s*")
				local and_matches = true
				for _, and_style in ipairs(and_styles) do
					local negate
					if and_style:find("^%-") then
						and_style = and_style:gsub("^%-", "")
						negate = true
					end
					local this_style_matches = false
					for _, part in ipairs(style_parts) do
						if part == and_style then
							this_style_matches = true
							break
						end
					end
					if negate then
						this_style_matches = not this_style_matches
					end
					if not this_style_matches then
						and_matches = false
					end
				end
				if and_matches then
					style_matches = true
					break
				end
			end
		end
		if not style_matches then
			return
		end

		-- Fetch the representative dialect's pronunciation if not already present.
		if not ret.pronun then
			dodialect(ret, representative_dialect)
		end
		-- Insert the new style into the style group, creating the group if necessary.
		local new_style = {
			tag = tag,
			pronun = ret.pronun,
		}
		for _, hidden_tag_style in ipairs(ret.expressed_styles) do
			if hidden_tag_style.tag == hidden_tag then
				table.insert(hidden_tag_style.styles, new_style)
				return
			end
		end
		table.insert(ret.expressed_styles, {
			tag = hidden_tag,
			styles = {new_style},
		})
	end

	-- For each type of difference, figure out if the difference exists in any of the given respellings. We do this by
	-- generating the pronunciation for the dialect "gipuzkoan", for each respelling. In the process of
	-- generating the pronunciation for a given respelling, it computes how the other dialects for that respelling
	-- differ. Then we take the union of these differences across the respellings.
	dodialect(ret, "gipuzkoan")
	local differences = {}
	for _, difftype in ipairs { "northern_different", "j_different", "need_bisc"} do
		for _, pronun in ipairs(ret.pronun) do
			if pronun.differences then
				differences = true
			end
		end
	end
	local northern_different = differences.northern_different
	local j_different = differences.j_different
	need_bisc = differences.need_bisc

	-- Now, based on the observed differences, figure out how to combine the individual dialects into styles and style groups.
	if northern_different then
		express_style("Navarro-Lapurdian","Navarro-Lapurdian","northern")
		if j_different then
			express_style("Southern","Gipuzkoan","gipuzkoan")
			express_style("Southern","Biscayan","biscayan")
			express_style("Southern","Navarrese","navarrese")
		else
			if need_bisc then
				express_style("Southern","Gipuzkoan, Navarrese","gipuzkoan")
				express_style("Southern","Biscayan","biscayan")
			else
				express_style("Southern","Southern","gipuzkoan")
			end
		end
	else
		if j_different then
			express_style("Navarro-Lapurdian","Navarro-Lapurdian","northern")
			express_style("Southern","Gipuzkoan","gipuzkoan")
			express_style("Southern","Biscayan","biscayan")
			express_style("Southern","Navarrese","navarrese")
		elseif need_bisc then
			express_style(false,"most dialects","gipuzkoan")
			express_style(false,"Biscayan","biscayan")
		else
			express_style(false,false,"gipuzkoan")
		end
	end

	return ret
end


local function format_all_styles(expressed_styles, format_style)
	for i, style_group in ipairs(expressed_styles) do
		if #style_group.styles == 1 then
			style_group.formatted, style_group.formatted_len =
				format_style(style_group.styles.tag, style_group.styles, i == 1)
		else
			style_group.formatted, style_group.formatted_len =
				format_style(style_group.tag, style_group.styles, i == 1)
			for j, style in ipairs(style_group.styles) do
				style.formatted, style.formatted_len =
					format_style(style.tag, style, i == 1 and j == 1)
			end
		end
	end

	local maxlen = 0
	for i, style_group in ipairs(expressed_styles) do
		local this_len = style_group.formatted_len
		if #style_group.styles > 1 then
			for _, style in ipairs(style_group.styles) do
				this_len = math.max(this_len, style.formatted_len)
			end
		end
		maxlen = math.max(maxlen, this_len)
	end

	local lines = {}

	local need_major_hack = false
	for i, style_group in ipairs(expressed_styles) do
		if #style_group.styles == 1 then
			table.insert(lines, style_group.formatted)
			need_major_hack = false
		else
			local inline = '\n<div class="vsShow" style="display:none">\n' .. style_group.formatted .. "</div>"
			local full_prons = {}
			for _, style in ipairs(style_group.styles) do
				table.insert(full_prons, style.formatted)
			end
			local full = '\n<div class="vsHide">\n' .. table.concat(full_prons, "\n") .. "</div>"
			local em_length = math.floor(maxlen * 0.68) -- from ]
			table.insert(lines, '<div class="vsSwitcher" data-toggle-category="pronunciations" style="width: ' .. em_length .. 'em; max-width:100%;"><span class="vsToggleElement" style="float: right;">&nbsp;</span>' .. inline .. full .. "</div>")
			need_major_hack = true
		end
	end

	-- major hack to get bullets working on the next line after a div box
	return table.concat(lines, "\n") .. (need_major_hack and "\n<span></span>" or "")
end


local function dodialect_pronun(args, ret, dialect)
	ret.pronun = {}
	for i, term in ipairs(args.terms) do
		local phonemic, phonetic, differences
		if term.raw then
			phonemic = term.raw_phonemic
			phonetic = term.raw_phonetic
			differences = construct_default_differences(dialect)
		else
			phonemic = export.IPA(term.term, dialect, false)
			phonetic = export.IPA(term.term, dialect, true)
			differences = phonemic.differences
			phonemic = phonemic.text
			phonetic = phonetic.text
		end
		local refs
		if not term.ref then
			refs = nil
		else
			refs = {}
			for _, refspec in ipairs(term.ref) do
				local this_refs = require("Module:references").parse_references(refspec)
				for _, this_ref in ipairs(this_refs) do
					table.insert(refs, this_ref)
				end
			end
		end

		ret.pronun = {
			raw = term.raw,
			phonemic = phonemic,
			phonetic = phonetic,
			refs = refs,
			q = term.q,
			qq = term.qq,
			a = term.a,
			aa = term.aa,
			differences = differences,
		}
	end
end

local function generate_pronun(args)
	local function remove_technical_chars(text)
		return rsub(text, "()", "")
	end

	local function this_dodialect_pronun(ret, dialect)
		dodialect_pronun(args, ret, dialect)
	end

	local ret = express_all_styles(args.style, this_dodialect_pronun)

	local function format_style(tag, expressed_style, is_first)
		local pronunciations = {}
		local formatted_pronuns = {}

		local function ins(formatted_part)
			table.insert(formatted_pronuns, formatted_part)
		end

		-- Loop through each pronunciation. For each one, add the phonemic and phonetic versions to `pronunciations`,
		-- for formatting by ], and also create an approximation of the formatted version so that we can
		-- compute the appropriate width of the HTML switcher div box that holds the different per-dialect variants.
		-- NOTE: The code below constructs the formatted approximation out-of-order in some cases but that doesn't
		-- currently matter because we assume all characters have the same width. If we change the width computation
		-- in a way that requires the correct order, we need changes to the code below.
		for j, pronun in ipairs(expressed_style.pronun) do
			-- Add tag to left qualifiers if first one
			-- FIXME: Consider using accent qualifier for the tag instead.
			local qs = pronun.q
			if j == 1 and tag then
				if qs then
					qs = m_table.deepcopy(qs)
					table.insert(qs, tag)
				else
					qs = {tag}
				end
			end

			local first_pronun = #pronunciations + 1

			if not pronun.phonemic and not pronun.phonetic then
				error("Internal error: Saw neither phonemic nor phonetic pronunciation")
			end

			if pronun.phonemic then -- missing if 'raw:' given
				-- don't display syllable division markers in phonemic
				local slash_pron = "/" .. remove_technical_chars(pronun.phonemic:gsub("%.", "")) .. "/"
				table.insert(pronunciations, {
					pron = slash_pron,
				})
				ins(slash_pron)
			end

			if pronun.phonetic then -- missing if 'raw:/.../' given
				local bracket_pron = ""
				table.insert(pronunciations, {
					pron = bracket_pron,
				})
				ins(bracket_pron)
			end

			local last_pronun = #pronunciations

			if qs then
				pronunciations.q = qs
			end
			if pronun.a then
				pronunciations.a = pronun.a
			end
			if j > 1 then
				pronunciations.separator = ", "
				ins(", ")
			end
			if pronun.qq then
				pronunciations.qq = pronun.qq
			end
			if pronun.aa then
				pronunciations.aa = pronun.aa
			end
			if qs or pronun.qq or pronun.a or pronun.aa then
				-- Note: This inserts the actual formatted qualifier text, including HTML and such, but the later call
				-- to textual_len() removes all HTML and reduces links.
				ins(require("Module:pron qualifier").format_qualifiers {
					lang = lang,
					text = "",
					q = qs,
					qq = pronun.qq,
					a = pronun.a,
					aa = pronun.aa,
				})
			end

			if pronun.refs then
				pronunciations.refs = pronun.refs
				-- Approximate the reference using a footnote notation. This will be slightly inaccurate if there are
				-- more than nine references but that is rare.
				ins(string.rep("", #pronun.refs))
			end
			if first_pronun ~= last_pronun then
				pronunciations.separator = " "
				ins(" ")
			end
		end

		local bullet = string.rep("*", args.bullets) .. " "
		-- Here we construct the formatted line in `formatted`, and also try to construct the equivalent without HTML
		-- and wiki markup in `formatted_for_len`, so we can compute the approximate textual length for use in sizing
		-- the toggle box with the "more" button on the right.
		local pre = is_first and args.pre and args.pre .. " " or ""
		local post = is_first and args.post and " " .. args.post or ""
		local formatted = bullet .. pre .. m_IPA.format_IPA_full { lang = lang, items = pronunciations, separator = "" } .. post
		local formatted_for_len = bullet .. pre .. "IPA(key): " .. table.concat(formatted_pronuns) .. post
		return formatted, textual_len(formatted_for_len)
	end

	ret.text = format_all_styles(ret.expressed_styles, format_style)

	return ret
end


local function parse_respelling(respelling, pagename, parse_err)
	local raw_respelling = respelling:match("^raw:(.*)$")
	if raw_respelling then
		local raw_phonemic, raw_phonetic = raw_respelling:match("^/(.*)/ %$")
		if not raw_phonemic then
			raw_phonemic = raw_respelling:match("^/(.*)/$")
		end
		if not raw_phonemic then
			raw_phonetic = raw_respelling:match("^%$")
		end
		if not raw_phonemic and not raw_phonetic then
			parse_err(("Unable to parse raw respelling '%s', should be one of /.../,  or /.../ ")
				:format(raw_respelling))
		end
		return {
			raw = true,
			raw_phonemic = raw_phonemic,
			raw_phonetic = raw_phonetic,
		}
	end
	if respelling == "+" then
		respelling = pagename
	end
	return {term = respelling}
end

-- Return the number of syllables of a phonemic representation, which should have syllable dividers in it but no
-- hyphens.
local function get_num_syl_from_phonemic(phonemic)
	-- Maybe we should just count vowels instead of the below code.
	phonemic = rsub(phonemic, "|", " ") -- remove IPA foot boundaries
	local words = rsplit(phonemic, " +")
	for i, word in ipairs(words) do
		-- IPA stress marks are syllable divisions if between characters; otherwise just remove.
		word = rsub(word, "(.)(.)", "%1.%2")
		word = rsub(word, "", "")
		words = word
	end
	-- There should be a syllable boundary between words.
	phonemic = table.concat(words, ".")
	return ulen(rsub(phonemic, "", "")) + 1
end

-- Take the last two syllables for the ryhme, unless the word has a single syllable
local function convert_phonemic_to_rhyme(phonemic)
	local syllables = rsplit(phonemic, "%.")
	local rh = ""
	if #syllables > 1 then
		rh = table.concat(syllables, ".", #syllables - 1, #syllables)
	else
		rh = phonemic
	end
	return rsub(rh, "^*", ""):gsub("%.", ""):gsub("͡", "")
end



local function split_syllabified_spelling(spelling)
	return rsplit(spelling, "%.")
end


-- "Align" syllabification to original spelling by matching character-by-character, allowing for extra syllable and
-- accent markers in the syllabification. If we encounter an extra syllable marker (.), we allow and keep it. If we
-- encounter an extra accent marker in the syllabification, we drop it. In any other case, we return nil indicating
-- the alignment failed.
local function align_syllabification_to_spelling(syllab, spelling)
	local result = {}
	local syll_chars = rsplit(decompose(syllab), "")
	local spelling_chars = rsplit(decompose(spelling), "")
	local i = 1
	local j = 1
	while i <= #syll_chars or j <= #spelling_chars do
		local ci = syll_chars
		local cj = spelling_chars
		if ci == cj then
			table.insert(result, ci)
			i = i + 1
			j = j + 1
		elseif ci == "." then
			table.insert(result, ci)
			i = i + 1
		elseif ci == AC or ci == GR or ci == CFLEX then
			-- skip character
			i = i + 1
		else
			-- non-matching character
			return nil
		end
	end
	if i <= #syll_chars or j <= #spelling_chars then
		-- left-over characters on one side or the other
		return nil
	end
	return unfc(table.concat(result))
end


local function generate_hyph_obj(term)
	return {syllabification = term, hyph = split_syllabified_spelling(term)}
end


-- Word should already be decomposed.
local function word_has_vowels(word)
	return rfind(word, V)
end


local function all_words_have_vowels(term)
	local words = rsplit(decompose(term), "")
	for i, word in ipairs(words) do
		-- Allow empty word; this occurs with prefixes and suffixes.
		if word ~= "" and not word_has_vowels(word) then
			return false
		end
	end
	return true
end


local function should_generate_rhyme_from_respelling(term)
	local words = rsplit(decompose(term), " +")
	return #words == 1 and -- no if multiple words
		not words:find(".%-.") and -- no if word is composed of hyphenated parts (e.g. ])
		not words:find("%-$") and -- no if word is a prefix
		not (words:find("^%-") and words:find(CFLEX)) and -- no if word is an unstressed suffix
		word_has_vowels(words) -- no if word has no vowels (e.g. a single letter)
end


local function should_generate_rhyme_from_ipa(ipa)
	return not ipa:find("%s") and word_has_vowels(decompose(ipa))
end


local function dodialect_specified_rhymes(rhymes, hyphs, parsed_respellings, rhyme_ret, dialect)
	rhyme_ret.pronun = {}
	for _, rhyme in ipairs(rhymes) do
		local num_syl = rhyme.num_syl
		local no_num_syl = false

		-- If user explicitly gave the rhyme but didn't explicitly specify the number of syllables, try to take it from
		-- the hyphenation.
		if not num_syl then
			num_syl = {}
			for _, hyph in ipairs(hyphs) do
				if should_generate_rhyme_from_respelling(hyph.syllabification) then
					local this_num_syl = 1 + ulen(rsub(hyph.syllabification, "", ""))
					m_table.insertIfNot(num_syl, this_num_syl)
				else
					no_num_syl = true
					break
				end
			end
			if no_num_syl or #num_syl == 0 then
				num_syl = nil
			end
		end

		-- If that fails and term is single-word, try to take it from the phonemic.
		if not no_num_syl and not num_syl then
			for _, parsed in ipairs(parsed_respellings) do
				for dialect, pronun in pairs(parsed.pronun.pronun) do
					-- Check that pronun.phonemic exists (it may not if raw phonetic-only pronun is given).
					if pronun.phonemic then
						if not should_generate_rhyme_from_ipa(pronun.phonemic) then
							no_num_syl = true
							break
						end
						-- Count number of syllables by looking at syllable boundaries (including stress marks).
						local this_num_syl = get_num_syl_from_phonemic(pronun.phonemic)
						m_table.insertIfNot(num_syl, this_num_syl)
					end
				end
				if no_num_syl then
					break
				end
			end
			if no_num_syl or #num_syl == 0 then
				num_syl = nil
			end
		end

		table.insert(rhyme_ret.pronun, {
			rhyme = rhyme.rhyme,
			num_syl = num_syl,
			qualifiers = rhyme.qualifiers,
			differences = construct_default_differences(dialect),
		})
	end
end


local function parse_pron_modifier(arg, put, parse_err, generate_obj, param_mods, no_split_on_comma)
	local retval = {}

	if arg:find("<") then
		if not put then
			put = require(put_module)
		end

		local function get_valid_prefixes()
			local valid_prefixes = {}
			for param_mod, _ in pairs(param_mods) do
				table.insert(valid_prefixes, param_mod)
			end
			table.insert(valid_prefixes, "q")
			table.insert(valid_prefixes, "qq")
			table.insert(valid_prefixes, "a")
			table.insert(valid_prefixes, "aa")
			table.sort(valid_prefixes)
			return valid_prefixes
		end

		local segments = put.parse_balanced_segment_run(arg, "<", ">")
		local comma_separated_groups =
			no_split_on_comma and {segments} or put.split_alternating_runs_on_comma(segments)
		for _, group in ipairs(comma_separated_groups) do
			local obj = generate_obj(group)
			for j = 2, #group - 1, 2 do
				if group ~= "" then
					parse_err("Extraneous text '" .. group .. "' after modifier")
				end
				local modtext = group:match("^<(.*)>$")
				if not modtext then
					parse_err("Internal error: Modifier '" .. group .. "' isn't surrounded by angle brackets")
				end
				local prefix, val = modtext:match("^(+):(.*)$")
				if not prefix then
					local valid_prefixes = get_valid_prefixes()
					for i, valid_prefix in ipairs(valid_prefixes) do
						valid_prefixes = "'" .. valid_prefix .. ":'"
					end
					parse_err("Modifier " .. group .. " lacks a prefix, should begin with one of " ..
						m_table.serialCommaJoin(valid_prefixes))
				end
				if prefix == "q" or prefix == "qq" or prefix == "a" or prefix == "aa" then
					if not obj then
						obj = {}
					end
					table.insert(obj, val)
				elseif param_mods then
					local key = param_mods.item_dest or prefix
					if obj then
						parse_err("Modifier '" .. prefix .. "' specified more than once")
					end
					local convert = param_mods.convert
					if convert then
						obj = convert(val)
					else
						obj = val
					end
				else
					local valid_prefixes = get_valid_prefixes()
					for i, valid_prefix in ipairs(valid_prefixes) do
						valid_prefixes = "'" .. valid_prefix .. "'"
					end
					parse_err("Unrecognized prefix '" .. prefix .. "' in modifier " .. group
						.. ", should be " .. m_table.serialCommaJoin(valid_prefixes))
				end
			end
			table.insert(retval, obj)
		end
	elseif no_split_on_comma then
		table.insert(retval, generate_obj(arg))
	else
		for _, term in ipairs(split_on_comma(arg)) do
			table.insert(retval, generate_obj(term))
		end
	end

	return retval
end


local function parse_rhyme(arg, put, parse_err)
	local function generate_obj(term)
		return {rhyme = term}
	end
	local param_mods = {
		s = {
			item_dest = "num_syl",
			convert = function(arg)
				local nsyls = rsplit(arg, ",")
				for i, nsyl in ipairs(nsyls) do
					if not nsyl:find("^+$") then
						parse_err("Number of syllables '" .. nsyl .. "' should be numeric")
					end
					nsyls = tonumber(nsyl)
				end
				return nsyls
			end,
		},
	}

	return parse_pron_modifier(arg, put, parse_err, generate_obj, param_mods)
end


local function parse_hyph(arg, put, parse_err)
	-- None other than qualifiers
	local param_mods = {}

	return parse_pron_modifier(arg, put, parse_err, generate_hyph_obj, param_mods)
end


local function parse_homophone(arg, put, parse_err)
	local function generate_obj(term)
		return {term = term}
	end
	local param_mods = {
		t = {
			-- We need to store the <t:...> inline modifier into the "gloss" key of the parsed term,
			-- because that is what ] (called from ]) expects.
			item_dest = "gloss",
		},
		gloss = {},
		pos = {},
		alt = {},
		lit = {},
		id = {},
		g = {
			-- We need to store the <g:...> inline modifier into the "genders" key of the parsed term,
			-- because that is what ] (called from ]) expects.
			item_dest = "genders",
			convert = function(arg)
				return rsplit(arg, ",")
			end,
		},
	}

	return parse_pron_modifier(arg, put, parse_err, generate_obj, param_mods)
end


local function generate_audio_obj(arg)
	local file, gloss
	if arg:find("#") then
		file, gloss = arg:match("^(.-)%s*#%s*(.*)$")
	else
		file, gloss = arg:match("^(.-)%s*;%s*(.*)$")
	end
	file = file or arg
	return {file = file, gloss = gloss}
end


local function parse_audio(arg, put, parse_err)
	-- None other than qualifiers
	local param_mods = {}

	-- Don't split on comma because some filenames have embedded commas not followed by a space
	-- (typically followed by an underscore).
	return parse_pron_modifier(arg, put, parse_err, generate_audio_obj, param_mods, "no split on comma")
end


-- External entry point for {{es-pr}}.
function export.show_pr(frame)
	local params = {
		 = {list = true},
		 = {},
		 = {},
		 = {},
		 = {list = true},
		 = {},
	}
	local parargs = frame:getParent().args
	local args = require("Module:parameters").process(parargs, params)
	local pagename = args.pagename or mw.title.getCurrentTitle().subpageText

	-- Parse the arguments.
	local function remove_technical_chars(text)
		return rsub(text, "()", "")
	end
	local respellings = #args > 0 and args or {"+"}
	local parsed_respellings = {}
	local function overall_parse_err(msg, arg, val)
		error(msg .. ": " .. arg .. "= " .. val)
	end
	local overall_rhyme = args.rhyme and
		parse_rhyme(args.rhyme, nil, function(msg) overall_parse_err(msg, "rhyme", args.rhyme) end) or nil
	local overall_hyph = args.hyph and
		parse_hyph(args.hyph, nil, function(msg) overall_parse_err(msg, "hyph", args.hyph) end) or nil
	local overall_hmp = args.hmp and
		parse_homophone(args.hmp, nil, function(msg) overall_parse_err(msg, "hmp", args.hmp) end) or nil
	local overall_audio
	if args.audio then
		overall_audio = {}
		for _, audio in ipairs(args.audio) do
			local parsed_audio = parse_audio(audio, nil, function(msg) overall_parse_err(msg, "audio", audio) end)
			if #parsed_audio > 1 then
				error("Internal error: Saw more than one object returned from parse_audio")
			end
			table.insert(overall_audio, parsed_audio)
		end
	end
	local put

	for i, respelling in ipairs(respellings) do
		local function parse_err(msg)
			error(msg .. ": " .. i .. "= " .. respelling)
		end
		if respelling:find("<") then
			if not put then
				put = require(put_module)
			end

			local param_mods = {
				pre = {},
				post = {},
				style = {},
				bullets = {
					convert = function(arg)
						if not arg:find("^+$") then
							parse_err("Modifier 'bullets' should have a number as argument, but saw '" .. arg .. "'")
						end
						return tonumber(arg)
					end,
				},
				rhyme = {
					insert = true,
					flatten = true,
					convert = function(arg) return parse_rhyme(arg, put, parse_err) end,
				},
				hyph = {
					insert = true,
					flatten = true,
					convert = function(arg) return parse_hyph(arg, put, parse_err) end,
				},
				hmp = {
					insert = true,
					flatten = true,
					convert = function(arg) return parse_homophone(arg, put, parse_err) end,
				},
				audio = {
					insert = true,
					flatten = true,
					convert = function(arg) return parse_audio(arg, put, parse_err) end,
				},
			}

			local function get_valid_prefixes()
				local valid_prefixes = {}
				for param_mod, _ in pairs(param_mods) do
					table.insert(valid_prefixes, param_mod)
				end
				table.insert(valid_prefixes, "ref")
				table.insert(valid_prefixes, "q")
				table.insert(valid_prefixes, "qq")
				table.insert(valid_prefixes, "a")
				table.insert(valid_prefixes, "aa")
				table.sort(valid_prefixes)
				return valid_prefixes
			end

			local segments = put.parse_balanced_segment_run(respelling, "<", ">")
			local comma_separated_groups = put.split_alternating_runs_on_comma(segments, ",")
			local parsed = {terms = {}, audio = {}, rhyme = {}, hyph = {}, hmp = {}}
			for j, group in ipairs(comma_separated_groups) do
				local termobj = parse_respelling(group, pagename, parse_err)
				for k = 2, #group - 1, 2 do
					if group ~= "" then
						parse_err("Extraneous text '" .. group .. "' after modifier")
					end
					local modtext = group:match("^<(.*)>$")
					if not modtext then
						parse_err("Internal error: Modifier '" .. group .. "' isn't surrounded by angle brackets")
					end
					local prefix, arg = modtext:match("^(+):(.*)$")
					if not prefix then
						local valid_prefixes = get_valid_prefixes()
						for i, valid_prefix in ipairs(valid_prefixes) do
							valid_prefixes = "'" .. valid_prefix .. ":'"
						end
						parse_err("Modifier " .. group .. " lacks a prefix, should begin with one of " ..
							m_table.serialCommaJoin(valid_prefixes))
					end
					if prefix == "ref" or prefix == "q" or prefix == "qq" or prefix == "a" or prefix == "aa" then
						if not termobj then
							termobj = {}
						end
						table.insert(termobj, arg)
					elseif param_mods then
						if j < #comma_separated_groups then
							parse_err("Modifier '" .. prefix .. "' should occur after the last comma-separated term")
						end
						if not param_mods.insert and parsed then
							parse_err("Modifier '" .. prefix .. "' occurs twice, second occurrence " .. group)
						end
						local converted
						if param_mods.convert then
							converted = param_mods.convert(arg)
						else
							converted = arg
						end
						if param_mods.insert then
							if param_mods.flatten then
								for _, obj in ipairs(converted) do
									table.insert(parsed, obj)
								end
							else
								table.insert(parsed, converted)
							end
						else
							parsed = converted
						end
					else
						local valid_prefixes = get_valid_prefixes()
						for i, valid_prefix in ipairs(valid_prefixes) do
							valid_prefixes = "'" .. valid_prefix .. "'"
						end
						parse_err("Unrecognized prefix '" .. prefix .. "' in modifier " .. group
							.. ", should be " .. m_table.serialCommaJoin(valid_prefixes))
					end
				end
				table.insert(parsed.terms, termobj)
			end
			if not parsed.bullets then
				parsed.bullets = 1
			end
			table.insert(parsed_respellings, parsed)
		else
			local termobjs = {}
			for _, term in ipairs(split_on_comma(respelling)) do
				table.insert(termobjs, parse_respelling(term, pagename, parse_err))
			end
			table.insert(parsed_respellings, {
				terms = termobjs,
				audio = {},
				rhyme = {},
				hyph = {},
				hmp = {},
				bullets = 1,
			})
		end
	end

	if overall_hyph then
		local hyphs = {}
		for _, hyph in ipairs(overall_hyph) do
			if hyph.syllabification == "+" then
				hyph.syllabification = syllabify_from_spelling(pagename)
				hyph.hyph = split_syllabified_spelling(hyph.syllabification)
			elseif hyph.syllabification == "-" then
				overall_hyph = {}
				break
			end
		end
	end

	-- Loop over individual respellings, processing each.
	for _, parsed in ipairs(parsed_respellings) do
		parsed.pronun = generate_pronun(parsed)
		local no_auto_rhyme = false
		for _, term in ipairs(parsed.terms) do
			if term.raw then
				if not should_generate_rhyme_from_ipa(term.raw_phonemic or term.raw_phonetic) then
					no_auto_rhyme = true
					break
				end
			elseif not should_generate_rhyme_from_respelling(term.term) then
				no_auto_rhyme = true
				break
			end
		end

		if #parsed.hyph == 0 then
			if not overall_hyph and all_words_have_vowels(pagename) then
				for _, term in ipairs(parsed.terms) do
					if not term.raw then
						local syllabification = syllabify_from_spelling(term.term)
						local aligned_syll = align_syllabification_to_spelling(syllabification, pagename)
						if aligned_syll then
							m_table.insertIfNot(parsed.hyph, generate_hyph_obj(aligned_syll))
						end
					end
				end
			end
		else
			for _, hyph in ipairs(parsed.hyph) do
				if hyph.syllabification == "+" then
					hyph.syllabification = syllabify_from_spelling(pagename)
					hyph.hyph = split_syllabified_spelling(hyph.syllabification)
				elseif hyph.syllabification == "-" then
					parsed.hyph = {}
					break
				end
			end
		end

		-- Generate the rhymes.
		local function dodialect_rhymes_from_pronun(rhyme_ret, dialect)
			rhyme_ret.pronun = {}
			-- It's possible the pronunciation for a passed-in dialect was never generated. This happens e.g. with
			-- {{es-pr|cebolla<style:seseo>}}. The initial call to generate_pronun() fails to generate a pronunciation
			-- for the dialect 'distinction-yeismo' because the pronunciation of 'cebolla' differs between distincion
			-- and seseo and so the seseo style restriction rules out generation of pronunciation for distincion
			-- dialects (other than 'gipuzkoan', which always gets generated so as to determine on which axes
			-- the dialects differ). However, when generating the rhyme, it is based only on -olla, whose pronunciation
			-- does not differ between distincion and seseo, but does differ between lleismo and yeismo, so it needs to
			-- generate a yeismo-specific rhyme, and 'distincion-yeismo' is the representative dialect for yeismo in the
			-- situation where distincion and seseo do not have distinct results (based on the following line in
			-- express_all_styles()):
			--   express_style(false, "most of Spain and Latin America", "distincion-yeismo", "distincion-seseo-yeismo")
			-- In this case we need to generate the missing overall pronunciation ourselves since we need it to generate
			-- the dialect-specific rhyme pronunciation.
			if not parsed.pronun.pronun then
				dodialect_pronun(parsed, parsed.pronun, dialect)
			end
			for _, pronun in ipairs(parsed.pronun.pronun) do
				-- We should have already excluded multiword terms and terms without vowels from rhyme generation (see
				-- `no_auto_rhyme` below). But make sure to check that pronun.phonemic exists (it may not if raw
				-- phonetic-only pronun is given).
				if pronun.phonemic then
					-- Count number of syllables by looking at syllable boundaries (including stress marks).
					local num_syl = get_num_syl_from_phonemic(pronun.phonemic)
					-- Get the rhyme by truncating everything up through the last stress mark + any following
					-- consonants, and remove syllable boundary markers.
					local rhyme = convert_phonemic_to_rhyme(pronun.phonemic)
					local saw_already = false
					for _, existing in ipairs(rhyme_ret.pronun) do
						if existing.rhyme == rhyme then
							saw_already = true
							-- We already saw this rhyme but possibly with a different number of syllables,
							-- e.g. if the user specified two pronunciations 'biología' (4 syllables) and
							-- 'bi.ología' (5 syllables), both of which have the same rhyme /ia/.
							m_table.insertIfNot(existing.num_syl, num_syl)
							break
						end
					end
					if not saw_already then
						local rh2 = rsub(rhyme, "ts̻", "")
						local rhyme_diffs = nil
						if dialect == "gipuzkoan" then
							rhyme_diffs = {}
							if rfind(rh2,"s̻") then
								rhyme_diffs.need_bisc = true
							end
							if rfind(rhyme,"ts̺") then
								rhyme_diffs.need_bisc = true
							end
							if rfind(rhyme,"J") then
								rhyme_diffs.j_different = true
							end
							if rfind(rhyme,"N") then
								rhyme_diffs.northern_different = true
							end
						end
						table.insert(rhyme_ret.pronun, {
							rhyme = remove_technical_chars(rhyme),
							num_syl = {num_syl},
							differences = rhyme_diffs,
						})
					end
				end
			end
		end

		if #parsed.rhyme == 0 then
			if overall_rhyme or no_auto_rhyme then
				parsed.rhyme = nil
			else
				parsed.rhyme = express_all_styles(parsed.style, dodialect_rhymes_from_pronun)
			end
		else
			local no_rhyme = false
			for _, rhyme in ipairs(parsed.rhyme) do
				if rhyme.rhyme == "-" then
					no_rhyme = true
					break
				end
			end
			if no_rhyme then
				parsed.rhyme = nil
			else
				local function this_dodialect(rhyme_ret, dialect)
					return dodialect_specified_rhymes(parsed.rhyme, parsed.hyph, {parsed}, rhyme_ret, dialect)
				end
				parsed.rhyme = express_all_styles(parsed.style, this_dodialect)
			end
		end
	end

	if overall_rhyme then
		local no_overall_rhyme = false
		for _, orhyme in ipairs(overall_rhyme) do
			if orhyme.rhyme == "-" then
				no_overall_rhyme = true
				break
			end
		end
		if no_overall_rhyme then
			overall_rhyme = nil
		else
			local all_hyphs
			if overall_hyph then
				all_hyphs = overall_hyph
			else
				all_hyphs = {}
				for _, parsed in ipairs(parsed_respellings) do
					for _, hyph in ipairs(parsed.hyph) do
						m_table.insertIfNot(all_hyphs, hyph)
					end
				end
			end
			local function dodialect_overall_rhyme(rhyme_ret, dialect)
				return dodialect_specified_rhymes(overall_rhyme, all_hyphs, parsed_respellings, rhyme_ret, dialect)
			end
			overall_rhyme = express_all_styles(parsed.style, dodialect_overall_rhyme)
		end
	end

	-- If all sets of pronunciations have the same rhymes, display them only once at the bottom.
	-- Otherwise, display rhymes beneath each set, indented.
	local first_rhyme_ret
	local all_rhyme_sets_eq = true
	for j, parsed in ipairs(parsed_respellings) do
		if j == 1 then
			first_rhyme_ret = parsed.rhyme
		elseif not m_table.deepEquals(first_rhyme_ret, parsed.rhyme) then
			all_rhyme_sets_eq = false
			break
		end
	end

	local function format_rhyme(rhyme_ret, num_bullets)
		local function format_rhyme_style(tag, expressed_style, is_first)
			local pronunciations = {}
			local rhymes = {}
			for _, pronun in ipairs(expressed_style.pronun) do
				table.insert(rhymes, pronun)
			end
			local data = {
				lang = lang,
				rhymes = rhymes,
				qualifiers = tag and {tag} or nil,
				force_cat = force_cat,
			}
			local bullet = string.rep("*", num_bullets) .. " "
			local formatted = bullet .. require("Module:rhymes").format_rhymes(data)
			local formatted_for_len_parts = {}
			table.insert(formatted_for_len_parts, bullet .. "Rhymes: " .. (tag and "(" .. tag .. ") " or ""))
			for j, pronun in ipairs(expressed_style.pronun) do
				if j > 1 then
					table.insert(formatted_for_len_parts, ", ")
				end
				if pronun.qualifiers then
					table.insert(formatted_for_len_parts, "(" .. table.concat(pronun.qualifiers, ", ") .. ") ")
				end
				table.insert(formatted_for_len_parts, "-" .. pronun.rhyme)
			end
			return formatted, textual_len(table.concat(formatted_for_len_parts))
		end

		return format_all_styles(rhyme_ret.expressed_styles, format_rhyme_style)
	end

	-- If all sets of pronunciations have the same hyphenations, display them only once at the bottom.
	-- Otherwise, display hyphenations beneath each set, indented.
	local first_hyphs
	local all_hyph_sets_eq = true
	for j, parsed in ipairs(parsed_respellings) do
		if j == 1 then
			first_hyphs = parsed.hyph
		elseif not m_table.deepEquals(first_hyphs, parsed.hyph) then
			all_hyph_sets_eq = false
			break
		end
	end

	local function format_hyphenations(hyphs, num_bullets)
		local hyphtext = require("Module:hyphenation").format_hyphenations { lang = lang, hyphs = hyphs}
		return string.rep("*", num_bullets) .. " " .. hyphtext
	end

	-- If all sets of pronunciations have the same homophones, display them only once at the bottom.
	-- Otherwise, display homophones beneath each set, indented.
	local first_hmps
	local all_hmp_sets_eq = true
	for j, parsed in ipairs(parsed_respellings) do
		if j == 1 then
			first_hmps = parsed.hmp
		elseif not m_table.deepEquals(first_hmps, parsed.hmp) then
			all_hmp_sets_eq = false
			break
		end
	end

	local function format_homophones(hmps, num_bullets)
		local hmptext = require("Module:homophones").format_homophones { lang = lang, homophones = hmps }
		return string.rep("*", num_bullets) .. " " .. hmptext
	end

	local function format_audio(audios, num_bullets)
		local ret = {}
		for i, audio in ipairs(audios) do
			local text = require("Module:audio").format_audio {
				lang = lang,
				file = audio.file,
				caption = audio.gloss,
				q = audio.q,
				qq = audio.qq,
				a = audio.a,
				aa = audio.aa,
			}
			table.insert(ret, string.rep("*", num_bullets) .. " " .. text)
		end
		return table.concat(ret, "\n")
	end

	local textparts = {}
	local min_num_bullets = 9999
	for j, parsed in ipairs(parsed_respellings) do
		if parsed.bullets < min_num_bullets then
			min_num_bullets = parsed.bullets
		end
		if j > 1 then
			table.insert(textparts, "\n")
		end
		table.insert(textparts, parsed.pronun.text)
		if #parsed.audio > 0 then
			table.insert(textparts, "\n")
			-- If only one pronunciation set, add the audio with the same number of bullets, otherwise
			-- indent audio by one more bullet.
			table.insert(textparts, format_audio(parsed.audio,
				#parsed_respellings == 1 and parsed.bullets or parsed.bullets + 1))
		end
		if not all_rhyme_sets_eq and parsed.rhyme then
			table.insert(textparts, "\n")
			table.insert(textparts, format_rhyme(parsed.rhyme, parsed.bullets + 1))
		end
		if not all_hyph_sets_eq and #parsed.hyph > 0 then
			table.insert(textparts, "\n")
			table.insert(textparts, format_hyphenations(parsed.hyph, parsed.bullets + 1))
		end
		if not all_hmp_sets_eq and #parsed.hmp > 0 then
			table.insert(textparts, "\n")
			table.insert(textparts, format_homophones(parsed.hmp, parsed.bullets + 1))
		end
	end
	if overall_audio and #overall_audio > 0 then
		table.insert(textparts, "\n")
		table.insert(textparts, format_audio(overall_audio, min_num_bullets))
	end
	if all_rhyme_sets_eq and first_rhyme_ret then
		table.insert(textparts, "\n")
		table.insert(textparts, format_rhyme(first_rhyme_ret, min_num_bullets))
	end
	if overall_rhyme then
		table.insert(textparts, "\n")
		table.insert(textparts, format_rhyme(overall_rhyme, min_num_bullets))
	end
	if all_hyph_sets_eq and #first_hyphs > 0 then
		table.insert(textparts, "\n")
		table.insert(textparts, format_hyphenations(first_hyphs, min_num_bullets))
	end
	if overall_hyph and #overall_hyph > 0 then
		table.insert(textparts, "\n")
		table.insert(textparts, format_hyphenations(overall_hyph, min_num_bullets))
	end
	if all_hmp_sets_eq and #first_hmps > 0 then
		table.insert(textparts, "\n")
		table.insert(textparts, format_homophones(first_hmps, min_num_bullets))
	end
	if overall_hmp and #overall_hmp > 0 then
		table.insert(textparts, "\n")
		table.insert(textparts, format_homophones(overall_hmp, min_num_bullets))
	end

	return table.concat(textparts)
end

return export