Module:wen-pron

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

This module implements {{dsb-pr}} and {{hsb-pr}}.


local export = {}
--Based on ]
local m_str_utils = require("Module:string utilities")
local m_table = require("Module:table")
local audio_module = "Module:audio"
local links_module = "Module:links"
local parse_utilities_module = "Module:parse utilities"

local u = m_str_utils.char
local rfind = m_str_utils.find
local rmatch = m_str_utils.match
local rsplit = m_str_utils.split
local rsubn = m_str_utils.gsub
local ulen = m_str_utils.len
local ulower = m_str_utils.lower
local uupper = mw.ustring.upper
local usub = mw.ustring.sub
local find = mw.ustring.find

local vowels = "aeiouěóyɔɛɪʊɨ"

-- FIXME: Implement optional assimilation across word boundaries.
local assimilate_across_word_boundaries = false

local function track(page)
    require("Module:debug/track")("wen-pron/" .. page)
    return true
end

-- 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

-- 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

-- Flat-map a function `fun` over `items`. This is like `map` over a sequence followed by `flatten`, i.e. the function
-- must itself return a sequence and all of the returned sequences are flattened into a single sequence.
local function flatmap(items, fun)
	local new = {}
	for _, item in ipairs(items) do
		local results = fun(item)
		for _, result in ipairs(results) do
			m_table.insertIfNot(new, result)
		end
	end
	return new
end

-- Combine two sets of qualifiers, either of which may be nil or a list of qualifiers. Remove duplicate qualifiers.
-- Return value is nil or a list of qualifiers.
local function combine_qualifiers(qual1, qual2)
	if not qual1 then
		return qual2
	end
	if not qual2 then
		return qual1
	end
	local qualifiers = m_table.deepcopy(qual1)
	for _, qual in ipairs(qual2) do
		m_table.insertIfNot(qualifiers, qual)
	end
	return qualifiers
end

local function split_on_comma(term)
	if not term then
		return nil
	end
	if term:find(",%s") or term:find("\\") then
		return require(parse_utilities_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(links_module).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 parse_respellings_with_modifiers(respelling, paramname)
	local function generate_obj(respelling, parse_err)
		return {respelling = respelling}
	end

	if respelling:find("") then
		local put = require(parse_utilities_module)
		-- Parse balanced segment runs involving either  (substitution notation) or <...> (inline modifiers).
		-- We do this because we don't want commas inside of square or angle brackets to count as respelling
		-- delimiters. However, we need to rejoin square-bracketed segments with nearby ones after splitting
		-- alternating runs on comma. For example, if we are given
		-- "aa<q:learned>,<q:nonstandard>", after calling
		-- parse_multi_delimiter_balanced_segment_run() we get the following output:
		--
		-- {"a", "", "a", "<q:learned>", ",", "", "", "<q:nonstandard>", ""}
		--
		-- After calling split_alternating_runs(), we get the following:
		--
		-- {{"a", "", "a", "<q:learned>", ""}, {"", "", "", "<q:nonstandard>", ""}}
		--
		-- We need to rejoin stuff on either side of the square-bracketed portions.
		local segments = put.parse_multi_delimiter_balanced_segment_run(respelling, {{"<", ">"}, {""}})

		local comma_separated_groups = put.split_alternating_runs_on_comma(segments)

		-- Process each value.
		local retval = {}
		for i, group in ipairs(comma_separated_groups) do
			-- Rejoin runs that don't involve <...>.
			local j = 2
			while j <= #group do
				if not group:find("^<.*>$") then
					group = group .. group .. group
					table.remove(group, j)
					table.remove(group, j)
				else
					j = j + 2
				end
			end

			local param_mods = {
				q = {type = "qualifier"},
				qq = {type = "qualifier"},
				a = {type = "labels"},
				aa = {type = "labels"},
				ref = {item_dest = "refs", type = "references"},
			}

			table.insert(retval, put.parse_inline_modifiers_from_segments {
				group = group,
				arg = respelling,
				props = {
					paramname = paramname,
					param_mods = param_mods,
					generate_obj = generate_obj,
				},
			})
		end
		return retval
	else
		local retval = {}
		for _, item in ipairs(split_on_comma(respelling)) do
			table.insert(retval, generate_obj(item))
		end
		return retval
	end
end

-- Parse a pronunciation modifier in `arg`, the argument portion in an inline modifier (after the prefix), which
-- specifies a pronunciation property such as rhyme, hyphenation/syllabification, homophones or audio. The argument
-- can itself have inline modifiers, e.g. <audio:Foo.ogg<a:Masovia>>. The allowed inline modifiers are specified
-- by `param_mods` (of the format expected by `parse_inline_modifiers()`); in addition to any modifiers specified
-- there, the modifiers <q:...>, <qq:...>, <a:...>, <aa:...> and <ref:...> are always accepted. `generate_obj` and
-- `parse_err` are like in `parse_inline_modifiers()` and specify respectively a function to generate the object into
-- which modifier properties are stored given the non-modifier part of the argument, and a function to generate an error
-- message (given the message). Normally, a comma-separated list of pronunciation properties is accepted and parsed,
-- where each element in the list can have its own inline modifiers and where no spaces are allowed next to the commas
-- in order for them to be recognized as separators. This can be overridden with `splitchar` (which can actually be a
-- Lua pattern). The return value is a list of property objects.
local function parse_pron_modifier(arg, paramname, generate_obj, param_mods, splitchar)
	splitchar = splitchar or ","
	if arg:find("<") then
		param_mods.q = {type = "qualifier"}
		param_mods.qq = {type = "qualifier"}
		param_mods.a = {type = "labels"}
		param_mods.aa = {type = "labels"}
		param_mods.ref = {item_dest = "refs", type = "references"}
		return require(parse_utilities_module).parse_inline_modifiers(arg, {
			param_mods = param_mods,
			generate_obj = generate_obj,
			paramname = paramname,
			splitchar = splitchar,
		})
	else
		local retval = {}
		local split_arg = splitchar == "," and split_on_comma(arg) or rsplit(arg, splitchar)
		for _, term in ipairs(split_arg) do
			table.insert(retval, generate_obj(term))
		end
		return retval
	end
end


local function parse_audio(lang, arg, pagename, paramname)
	local param_mods = {
		IPA = {
			sublist = true,
		},
		text = {},
		t = {
			item_dest = "gloss",
		},
		-- No tr=, ts=, or sc=; doesn't make sense for Polish.
		gloss = {},
		pos = {},
		-- No alt=; text= already goes in alt=.
		lit = {},
		-- No id=; text= already goes in alt= and isn't normally linked.
		g = {
			item_dest = "genders",
			sublist = true,
		},
		bad = {},
		cap = {
			item_dest = "caption",
		},
	}

	local function process_special_chars(val)
		if not val then
			return val
		end
		return (val:gsub("", {
			 = pagename,
		}))
	end

	local function generate_audio_obj(arg)
		return {file = process_special_chars(arg)}
	end

	-- Split on semicolon instead of comma because some filenames have embedded commas not followed by a space
	-- (typically followed by an underscore).
	local retvals = parse_pron_modifier(arg, paramname, generate_audio_obj, param_mods, "%s*;%s*")
	for i, retval in ipairs(retvals) do
		retval.lang = lang
		retval.text = process_special_chars(retval.text)
		retval.caption = process_special_chars(retval.caption)
		local textobj = require(audio_module).construct_audio_textobj(retval)
		retval.text = textobj
		-- Set to nil the fields that were moved into `retval.text`.
		retval.gloss = nil
		retval.pos = nil
		retval.lit = nil
		retval.genders = nil
	end
	return retvals
end

local function parse_homophones(arg, paramname)
	local function generate_obj(term)
		return {term = term}
	end
	local param_mods = {
		t = {
			-- ] expects the gloss in "gloss".
			item_dest = "gloss",
		},
		gloss = {},
		-- No tr=, ts=, or sc=; doesn't make sense for Polish.
		pos = {},
		alt = {},
		lit = {},
		id = {},
		g = {
			-- ] expects the genders in "genders".
			item_dest = "genders",
			sublist = true,
		},
	}

	return parse_pron_modifier(arg, paramname, generate_obj, param_mods)
end


--[=[
Given a single word in `txt`, compute its "lightly phonetic" IPA representation. If there are multiple possible outputs,
we signal this through special symbols such as capital letters (on input, capital letters have been lowercased 
so we can use capital letters as special symbols). The actual generation of multiple outputs happens in multiword() after the full term's IPA
has been generated. Return two values: the IPA representation and the hyphenation.
]=]
local function single_word(data)
	local txt, lang = data.txt, data.lang
	local is_prefix = not not txt:find("%-$")
	local is_suffix = not not txt:find("^%-")
	local unstressed = is_prefix or is_suffix or data.is_prep

	function tsub(s, r)
		local c
		txt, c = rsubn(txt, s, r)
		return c > 0
	end
	function tsub_repeatedly(s, r)
		txt = rsub_repeatedly(txt, s, r)
	end
	function lg(s) return s or s end
	function tfind(s) return rfind(txt, s) end

	-- Save indices of uppercase characters before setting everything lowercase.
	local uppercase_indices
	uppercase_indices = {}
	local capitals = (""):format(lg {
		hsb = "ČĆĚŁŃÓŘŠŽŹ",
		dsb = "ČĆĚŁŃÓŔŠŚŽŹ",
	})
	if tfind(capitals) then
		local i = 1
		local str = rsub(txt, "", "")
		while rfind(str, capitals, i) do
			local r, _ = rfind(str, capitals, i)
			table.insert(uppercase_indices, r)
			i = r + 1
		end
	end
	if #uppercase_indices == 0 then
		uppercase_indices = nil
	end

	txt = ulower(txt)

	-- Replace digraphs with single capital letters to simplify the code below."tř", "tč", "tš", "dš", "dč"
	tsub("", {
		ch = "H",
		dz = "Z",
		 = "Ź",
		 = "Ž",
		 = "Ć",
		ts = "C",
		 = "Č",
		 = "Š",
		 = "Ř",
		 = "D",
		 = "T",
		 = "K",
		 = "Q",
		 = "S",
		 = "P",
		 = "R",
	})

	local function undo_digraph_replacement(txt)
		return rsub(txt, "", {
			H = "ch",
			Z = "dz",
			 = "dź",
			 = "dž",
			 = "tś",
			C = "ts",
			 = "tč",
			 = "tš",
			 = "tř",
			D = "dš",
			T = "dč",
			K = "kš",
			Q = "kś",
			P = "pš",
			R = "pś",
			S = "př"
		})
	end

	-- Vowels, both spelled and IPA vowels. To make the distinction easier, we separate the IPA-only vowels and
	-- concatenate them at the end.
	local V = lg {
		hsb = "aeiouěóy" .. "ɔɛɪʊ",
		dsb = "aeiouěóy" .. "ɔɛɪʊɨ",
	}
	local C = (""):format(V)

	if txt:find("^%*") then
		-- The symbol <*> before a word indicates it is unstressed.
		unstressed = true
		txt = txt:sub(2)
	end

	-- Syllabify common prefixes as separate; note that since we replaced digraphs above with single
	-- chars, we need to use the replacement chars here. Longer prefixes must precede shorter subprefixes,
	-- e.g. pśede- must precede pśed- for the former to be recognized.
	local prefixes = lg {
		hsb = {
			"wote", "wot", "roze", "roz", "Sez", "Sed", "Se", "pode", "pod",
		},
		dsb = {
			"wóte", "wót", "roze", "roz", "Rez", "Rede", "Red", "Re", "póde", "pód", "nade", "nad"
		},
	}
	for _, prefix in ipairs(prefixes) do
		if tfind("^" .. prefix) then
			-- Make sure the suffix is followed by zero or more consonants (including - but not
			-- including a syllable divider) followed by a vowel. We do this so that we don't put
			-- a syllable divider when the user specified the divider in a different place.
			tsub(("^(%s)(%s*)"):format(prefix, C, V), "%1.%2")
			break
		end
	end

	-- syllabify common suffixes as separate
	local suffixes = lg {
		hsb = {
			"dla"
		},
		dsb = {
			"dla"
		},
	}

	for _, v in ipairs(suffixes) do
		-- Make sure there's no syllable divider elsewhere in the consonant cluster
		-- preceding the suffix. Same logic as above for prefixes.
		if tsub(("(%s*)(%s)$"):format(V, C, v), "%1.%2") then break end
	end

	-- Syllabify <-Ctka> as /Ct.ka/. Same for any case form.
	local C_before_t = "łrnfskp"
	if tfind(("+t"):format(V, C_before_t)) then
		local endings = lg {
			-- As with prefixes above, must use digraph replacement codes.
			dsb = { "k", "ce", "koma", "kowu", "kow", "kami", "ka", }, 
			hsb = { "k", "ce", "komaj", "kow", "kami", "ka", }, 
		}
		for _, ending in ipairs(endings) do
			-- Make sure there's no syllable divider elsewhere in the consonant cluster
			-- preceding the suffix. Same logic as above for prefixes.
			if tsub(("(+t)(%s)$"):format(V, C_before_t, ending), "%1.%2") then
				break
			end
		end
	end

	-- Syllabify by adding a period (.) between syllables. There may already be user-supplied syllable divisions
	-- (period or single quote), which we need to respect. This works by replacing each sequence of VC*V with
	-- V.V, V.CV, V.TRV (where T is an obstruent and R is a liquid) or otherwise VTR.C+V or VC.C+V, i.e. if there are
	-- multiple consonants, place the syllable boundary after the first TR sequence or otherwise the first consonant.
	-- We need to repeat since each VC*V sequence overlaps the next one. Digraphs have already been replaced by single
	-- capital letters.

	-- List of obstruents and liquids, including capital letters representing digraphs.
	local obstruent = "bdgkCptxfszśźšžđZŹŽĆCČŠŘDTKQPR"
	local liquid_no_w = "jlr"
	local liquid = liquid_no_w .. "łw"

	tsub_repeatedly(("()(%s*)()"):format(V, C, V), function(v1, cons, v2)
		local cons_no_hyphen = cons:gsub("%-", "")
		-- If there's only one consonant, or just an obstruent-liquid sequence, or any single consonant,
		-- put the syllable break before the consonant(s).
		if (ulen(cons_no_hyphen) < 2 or rfind(cons_no_hyphen, ("^j?$"):format(obstruent, liquid)) or
			rfind(cons_no_hyphen, ("^%sj$"):format(C))) then
			cons = "." .. cons
		else
			local nsubs
			-- Don't syllabify ] as niósł.by or ] as jabłczan.
			-- FIXME: Not sure if this is quite right.
			cons, nsubs = rsubn(cons, ("^(%%-?%%-?)"):format(obstruent, liquid_no_w), "%1.")
			if nsubs == 0 then
				cons = rsub(cons, "^(%-?.)", "%1.")
			end
		end
		return ("%s%s%s"):format(v1, cons, v2)
	end)

	-- Ignore certain symbols and diacritics for the hyphenation.
	local hyph = txt:gsub("", "."):gsub("^%.", ""):gsub("-", ""):gsub(",", "")

	hyph = undo_digraph_replacement(hyph)
	hyph = hyph:lower()
	-- Restore uppercase characters.
	if uppercase_indices then
		-- str_i loops through all the characters of the string
		-- list_i loops as above but doesn't count dots
		-- array_i loops through the indices at which the capital letters are
		local str_i, list_i, array_i = 1, 1, 1
		function h_sub(x, y) return usub(hyph, x, y) end
		while array_i <= #uppercase_indices do
			if h_sub(str_i, str_i) ~= "." then
				if list_i == uppercase_indices then
					hyph = ("%s%s%s"):format(h_sub(1,str_i-1), uupper(h_sub(str_i,str_i)), h_sub(str_i+1))
					array_i = array_i + 1
				end
				list_i = list_i + 1
			end
			str_i = str_i + 1
		end
	end

	tsub("'", "ˈ")
	tsub('"', "ˌ")

	txt = undo_digraph_replacement(txt)

	-- handle <x>; must precede ch -> ; use - to prevent palatalization by following i
	tsub("x", "ks-")
	-- move syllable boundary between  if preceded by a vowel
	tsub(("()()ks"):format(V), "%1k%2s")

	-- handle digraphs and related stuff
	tsub("ch", "x")
	tsub("tś", "ć")
	tsub("ts", "c")
	-- (marginal phonemes in Lower Sorbian)
	tsub("dz", "đ")
	tsub("dž", "џ")
	
	if lang == "hsb" then
		tsub("ć", "č") -- In Upper Sorbian <ć> and <č> represent the same phone. Internally, we will use ć for t͡sʲ (Upper) and t͡ɕ (Lower)
		tsub("t", "ć")
		tsub("d", "ć")
		tsub("ř", "š")
		tsub("dź", "џ")
	else
		tsub("dź", "ђ")
		tsub("tš", "č")
		tsub("r(%.џ)", "%1")
		--word initial/final <ł> is not pronounced if followed/preceded by an obstruent
		tsub("^ł()", "%1")
		tsub("()ł$", "%1")
	end
	
	--Voicing/devoicing
	local devoice = {
		 = "p",
		 = "t",
		 = "k", --non IPA <g>
		 = "s",
		 = "ś",
		 = "ć",
		 = "c",
		 = "č",
		 = "f",
		 = "š",
	}
	local voice = {}
	for k, v in pairs(devoice) do
		voice = k
	end

	-- final devoicing
	if not data.is_prep then
		for v, d in pairs(devoice) do
			tsub(v .. "$", d)
		end
	end
	
	--Regressive assimilation
	local vobs = ""
	local dobs = ""

	local prev_txt
	while txt ~= prev_txt do
		prev_txt = txt
		for v, d in pairs(devoice) do
			tsub(v .. "(*" .. dobs .. ")", d .. "%1")
		end
	end
	prev_txt = ""
	while txt ~= prev_txt do
		prev_txt = txt
		for d, v in pairs(voice) do
			tsub(d .. "(*" .. vobs .. ")", v .. "%1")
		end
	end

	-- not using lg() here for speed
	if lang == "hsb" then
		tsub("^xc", "c")
		tsub("hw", "f")
		tsub("h$", "")
		tsub("h" .. "(?" .. C .. ")", "%1")
	else
		-- <#pt/#pc> -> <#c>
		tsub("^p()", "%1")
		--<#rdž> -> <#dž>
		tsub("^rџ", "џ")
		--<šć> -> <ść> and <ždź> -> <źdź>
		tsub("š(*)ć", "ś%1ć")
		tsub("ž(*)ђ", "ź%1ђ")
		--mute <t>
		tsub("t" .. "(*)", "%1")
		--mute <k>
		tsub("^gdy", "dy")
		--values of <w>
		tsub("^woł", "Woł")
		tsub("^w()", "ʍ%1")
		tsub("%-w(*)", "ʍ%1") --we use the symbol ʍ to keep track of a mute <w>
	end
	--changes common to Upper and Lower
	tsub("^wj", "Wj")
	tsub("^w(" .. C .. ")", "%1")
	tsub("W", "w")

	-- palatalization
	local palat_intern = {{"p", "ṕ"}, {"b", "ḇ"}, {"m", "ḿ"}, {"w", "ẃ"}, {"f", "ḟ"}, {"t", "ť"}, {"d", "ď"}, {"n", "ñ"}, {"r", "ŕ"}, {"k", "ḱ"}, {"g", "ǵ"}}
	for _, pair in ipairs(palat_intern) do
		tsub(pair .. "()", pair .. "%1")
	end
	
	-- glide before soft consonants
	if lang == "hsb" then
		tsub("()(?)()", "%1j%2%3")
	end
	
	--other conversions
	if lang == "hsb" then
		tsub("ń", "jn")
	else
		tsub("ń", "ñ")
		tsub("ó(?)j", "e%1j")
	end

	-- Hyphen separator, e.g. to prevent palatalization of <kwazi->.
	tsub("-", "")
	
	--Final substitutions
	local final_replacements = {
		 = "pʲ",  = "bʲ",  = "mʲ",  = "wʲ",  = "fʲ", 
		 = "tʲ",  = "dʲ",  = "ɲ",  = "rʲ",
		 = "kʲ",  = "ɡʲ",
		 = "w",
		 = "ɛ",  = "ɔ",  = "ɪ",  = "ɨ",  = "ʊ",
		 = "ɡ",
		 = "t͡s",  = "t͡ʃ",  = "d͡z",  = "d͡ʒ",  = "ʒ",  = "ʃ",
		--lower Sorbian
		 = "ɕ",  = "ʑ",  = "d͡ʑ",
	}
	tsub(".", final_replacements)
	if lang == "hsb" then
		tsub("()ʲ", "%1") --Do we want this?
	end
	tsub("()j", "%1") -- we don't want <ʲj> in the transcriptions
	
	--stress on the first syllable, unless manually stressed or unstressed
	local should_stress = not (unstressed or txt:find("ˈ"))
	if should_stress then
		if rfind(txt, "^naj") and lang == "hsb" then
			txt = rsub(txt, "^naj", "najˈ")
			txt = rsub(txt, "ˈ%.", "ˈ")
		else
			txt = "ˈ" .. txt
		end
	end
	
	if data.is_prep then
		txt = txt .. "$"
	end
	
	if lang == "hsb" then
		tsub("ć", "t͡sʲ")
		tsub("r", "ʀ")
		--Unstressed <ó> and <e>.
		tsub("%.(*)ʊ", "%1O")
		tsub("%.(*)ɪ", "%1E")
	else
		tsub("ć", "t͡ɕ")
		-- silent <j>
		tsub("()ji", "%1i")
		tsub("ij", "i")
		tsub("h", "H") --stand-in for optional /h/
		tsub("HH", "h")
		tsub("wʲ", "vʲ")
	end
	
	tsub("ʍ", "")

	return txt, hyph
end

-- Returns rhyme from a transcription.
local function do_rhyme(pron, lang)
	-- No rhymes for multiword terms.
	if find(pron, "") then
		return nil
	end
	local V = "aɛiɔɪuʊɨ"
	local rhyme = rsub(rsub(pron:gsub("^.*ˈ", ""), ("^-()"):format(V, V), "%1"), "", "")
	local num_syl = { select(2, rsubn(pron, (""):format(V), "")) }
	return {
		rhyme = rhyme,
		num_syl = num_syl
	}
end

--[[
Handles a single input, returning a table of transcriptions. Returns also a string of hyphenation and a table of rhymes
if it is a single-word term.
--]]
local function multiword(term, lang)
	if term:find("^raw:%$") then
		return {{ phonetic = term:gsub("^raw:", "") }}
	end
	local ipa, hyph
	term = rsub(term, "%s*,%s*", " | ")
	if term:find(" ") then
		-- TODO: repeated
		function lg(s)
			return s or s
		end

		local prepositions = lg {
			hsb = {
				"bjeze?", "dla", "do", "ke?", "mjeze?", "na", "nade?", "pod?", "pode", "podłu", "pola", "porno", "pře?", "přee",
				"přećiwo", "při", "spod", "w?", "wo?", "wote", "z?", "zeza",
				"blisko", "dale", "nimo", "niže", "njedaloko", "spody", "srjedź", "wyše", "zady", "zboka", "zespody",
			}, 
			dsb = {
				"blisko", "bźez", "dalej", "dla", "do", "hob?", "hu", "ku?", "mimo", "mjazy", "nad?", "pa", "pod?", "pód?", "pódla", "pśed", "pśez", "pśi", "sa", "srjejź", "we?", "wob?", "wokoło", "wote?",
				"wót?", "wu", "za?", "ze"
			},
		}

		local ipaparts, hyphparts = {}, {}
		local contains_preps = false

		local words = rsplit(term, " +")
		for i, word in ipairs(words) do
			if word == "|" then
				-- prosodic boundary, from a comma
				table.insert(ipaparts, word)
				if hyphparts then
					hyphparts = hyphparts .. ","
				else
					hyphparts = ","
				end
			else
				local is_prep = false
				for _, prep in ipairs(prepositions) do
					if (rfind(word, ("^%s$"):format(prep))) then
						is_prep = true
						break
					end
				end
				contains_preps = contains_preps or is_prep
				local wordipa, wordhyph = single_word {
					txt = word,
					lang = lang,
					is_prep = is_prep,
				}
				table.insert(ipaparts, wordipa)
				table.insert(hyphparts, wordhyph)
				if i < #words then
					local separator = is_prep and "‿" or " "
					table.insert(ipaparts, separator)
					table.insert(hyphparts, separator)
				end
			end
		end

		ipa = table.concat(ipaparts)
		hyph = table.concat(hyphparts)

		local function assimilate_preps(str)
			local function assim(from, to, before)
				str = rsub(str, ("%s(%%$‿?)"):format(from, before), to .. "%1")
			end
			local T = "fptsɕkx"
			assim("d", "t", T)
			assim("v", "f", T)
			assim("z", "s", T)
			return rsub(str, "%$", "")
		end

		if contains_preps then
			ipa = assimilate_preps(ipa)
			-- Move stress before clitics (only non-syllabic ones in dsb) attached with tie bar. FIXME: We should be using # or similar
			-- at string boundaries to avoid the need to substitute twice.
			if lang == "dsb" then
				local C = ""
				ipa = rsub(ipa, ("()(%s+)‿()"):format(C), "%1%3%2‿")
				ipa = rsub(ipa, ("^(%s+)‿()"):format(C), "%2%1‿")
			else
				ipa = rsub(ipa, ("()(.+)‿()"), "%1%3%2‿")
				ipa = rsub(ipa, ("^(.+)‿()"), "%2%1‿")
			end
		end
	else
		ipa, hyph = single_word {
			txt = term,
			lang = lang,
			is_prep = false,
		}
	end

	local result = {{
		pron = ipa,
		norhyme = false,
	}}

	-- Map over each element in `result`. If `from` is found in the element, replace the element with up to three
	-- elements, respectively replacing `from` with `to1` (with accent qualifiers `a1`), `to2` (with accent qualifiers
	-- `a2`) and `to3` (with accent qualifiers `a3`). If `to2` or `to3` are nil, no replacement is done for them.
	-- If `nr1` is true, this variant should not have rhymes generated; likewise for `nr2` and `nr3`.
	local function flatmap_and_sub_post(from, to1, a1, nr1, to2, a2, nr2, to3, a3, nr3)
		local any_change = false
		result = flatmap(result, function(item)
			if rfind(item.pron, from) then
				any_change = true
				local retval = {
					{
						pron = rsub(item.pron, from, to1),
						a = combine_qualifiers(item.a, a1),
						norhyme = item.norhyme or nr1,
					}
				}
				if to2 then
					table.insert(retval,
						{
							pron = rsub(item.pron, from, to2),
							a = combine_qualifiers(item.a, a2),
							norhyme = item.norhyme or nr2,
						}
					)
				end
				if to3 then
					table.insert(retval,
						{
							pron = rsub(item.pron, from, to3),
							a = combine_qualifiers(item.a, a3),
							norhyme = item.norhyme or nr3,
						}
					)
				end
				return retval
			else
				return {item}
			end
		end)
		
		return any_change
	end

	--In Lower Sorbian, add variant pronunciations of <ó>, <h> (unless we're using the respelling <hh>)
	--In Upper Sorbian, add variant pronunciations of unstressed <ě> and <ó>
	if lang == "dsb" then
		flatmap_and_sub_post("ʊ", "ɛ", {}, false, "ɨ", {}, false, "ʊ", {"dated"}, false)
		flatmap_and_sub_post("H", "", {}, false, "h", {"less common"}, false)
	else
		flatmap_and_sub_post("E", "ɪ", {}, false, "ɛ", {"less common"}, false)
		flatmap_and_sub_post("O", "ʊ", {}, false, "ɔ", {"less common"}, false)
	end

	return result, hyph
end

-- Given a single substitution spec, `to`, figure out the corresponding value of `from` used in a complete
-- substitution spec. `pagename` is the name of the page, either the actual one or taken from the `pagename` param.
-- `anchor_begin`, if set, indicates that the match must be to the beginning of a word (it was preceded by ~).
-- `anchor_end`, if set, indicates that the match must be to the end of a word (it was followed by ~). If there is a
-- second return value, it indicates that the `from` is a Lua pattern and doesn't need to be pattern-escaped.
local function convert_single_substitution_to_original(to, pagename, anchor_begin, anchor_end)
	-- Replace specially-handled characters with a class matching the character and possible replacements.
	local escaped_from = to
	-- If the entire single substitution spec is one of the special stress-controlling symbols (*),
	-- place it at the very beginning of the pagename. We do this by returning a Lua pattern that matches the
	-- beginning of the string, using the %f notation. (%f means a transition from %z, or the NUL character, to
	-- not-%z; strings are treated for this purpose as if they begin and end with a NUL character.)
	if escaped_from:find("^+$") then
		return "%f", true
	end
	escaped_from = escaped_from:gsub("", "")
	-- Call the equivalent of pattern_escape() in ] but leave out +, *, ^ and -, which we match below.
	local chars = {
		 = "%z",  = "%$",  = "%%",  = "%(",  = "%)",
		 = "%.",  = "%?",  = "%"] = "%]",
	}
	escaped_from = escaped_from:gsub("]", chars)
	-- A special stress-controlling symbol (&, &&, +, etc.) that occurs in a single substitution expression but
	-- is not the entire expression matches the beginning of a word. (Contrast the case above when the symbol stands
	-- alone and matches the beginning of the entire term.) This makes it possible e.g. with an expression like
	-- ] to write the substitution spec  and have it work; it matches
	-- R at the beginning of a word and converts it to &R, which makes ] have optional antepenultimate
	-- stress. This must precede other replacements esp. k -> +, otherwise the + will wrongly get matched.
	escaped_from = escaped_from:gsub("+", "%%f")
	-- A hyphen can match against a hyphen or nothing in the original. We have to take into account escaping the hyphen
	-- in the from side, adding a % sign in the replacement to escape the hyphen in the later rmatch, and escaping the
	-- percent sign in the replacement.
	escaped_from = escaped_from:gsub("%-", "%%-?")
	-- A space can match against space or hyphen in the original, so e.g. ] can use  to respell it as
	-- <agar agar>.
	escaped_from = escaped_from:gsub(" ", "")
	escaped_from = "(" .. escaped_from .. ")"
	if anchor_begin then
		escaped_from = "%f" .. escaped_from
	end
	if anchor_end then
		escaped_from = escaped_from .. "%f"
	end
	local match = rmatch(pagename, escaped_from)
	if match then
		if match == to then
			error(("Single substitution spec '%s' found in pagename '%s', replacement would have no effect"):
				format(to, pagename))
		end
		return match
	end
	error(("Single substitution spec '%s' couldn't be matched to pagename '%s' (escaped_from: %s"):format(
		to, pagename, mw.dumpObject(escaped_from)))
end


local function apply_substitution_spec(respelling, pagename, parse_err)
	local subs = split_on_comma(rmatch(respelling, "^%$"))
	respelling = pagename
	for _, sub in ipairs(subs) do
		if sub:find("%^") or sub:find("%$") then
			-- Changed to use ~ at beginning or end to avoid clashing with other use of ^; catch old uses
			track("old-anchor-symbols")
		end
		local from, escaped_from, to, escaped_to, anchor_begin, anchor_end
		if sub:find("^~") then
			-- anchor at beginning
			sub = rmatch(sub, "^~(.*)$")
			anchor_begin = true
		end
		local already_escaped
		if sub:find(":") then
			from, to = rmatch(sub, "^(.-):(.*)$")
			if from:find("~$") then
				-- anchor at end
				from = rmatch(from, "^(.*)~$")
				anchor_end = true
			end
		else
			if sub:find("~$") then
				-- anchor at end
				sub = rmatch(sub, "^(.*)~$")
				anchor_end = true
			end
			to = sub
			from, already_escaped = convert_single_substitution_to_original(to, pagename, anchor_begin, anchor_end)
		end
		if from then
			escaped_from = already_escaped and from or m_str_utils.pattern_escape(from)
			if anchor_begin then
				escaped_from = "%f" .. escaped_from
			end
			if anchor_end then
				escaped_from = escaped_from .. "%f"
			end
			escaped_to = m_str_utils.replacement_escape(to)
			local subbed_respelling, nsubs = rsubn(respelling, escaped_from, escaped_to)
			if nsubs == 0 then
				parse_err(("Substitution spec %s -> %s didn't match processed pagename '%s'"):format(
					from, to, respelling))
			elseif nsubs > 1 then
				parse_err(("Substitution spec %s -> %s matched multiple substrings in processed pagename '%s', add " ..
					"more context"):format(from, to, respelling))
			else
				respelling = subbed_respelling
			end
		end
	end

	return respelling
end


-- This handles all the magic characters <*>, <^>, <&>, <+>, <.>, <#>.
local function normalise_input(term, pagename, paramname)
	local function check_af(str, af, reg, repl, err_msg)
		reg = reg:format(af)
		if not rfind(str, reg) then
			error(("The word %s does not %s with %s"):format(str, err_msg, af))
		end
		return str:gsub(reg, repl)
	end

	local function check_pref(str, pref) return check_af(str, pref, "^(%s)", "%1.", "start") end
	local function check_suf(str, suf) return check_af(str, suf, "(%s)$", ".%1", "end") end

	if term:find("^%$") then
		local function parse_err(msg)
			-- Don't call make_parse_err() until we actually need to throw an error, to avoid unnecessarily loading
			-- ].
			require(parse_utilities_module).make_parse_err(paramname)(msg)
		end
		return apply_substitution_spec(term, pagename, parse_err)
	end
	if term == "#" then
		-- The pound sign stands simply for {{PAGENAME}}.
		return pagename
	elseif (term == "+") or term:find("^%^+$") or term:find("^&+$") or (term == "*") then
		-- Inputs that are just '+', '*', '^', '^^', '&', '&&', etc. are treated as
		-- if they contained the pagename with those symbols preceding it.
		return term .. pagename
	-- Handle syntax like <po.>, <.ka> and <po..ka>. This allows to not respell
	-- the entire word when all is needed is to specify syllabification of a prefix
	-- and/or a suffix.
	elseif term:find(".+%.$") then
		return check_pref(pagename, term:sub(1, -2))
	elseif term:find("^%..+") then
		return check_suf(pagename, term:sub(2))
	elseif term:find(".+%.%..+") then
		return check_suf(check_pref(pagename, term:gsub("%.%..+", "")), term:gsub(".+%.%.", ""))
	end

	return term

end

-- This converts the raw information, the arguments and pagename, into tables to be handed over to the IPA module.
function export.get_lect_pron_info(terms, pagename, paramname, lang)
	local pron_list = {}
	local rhyme_list = {}
	local hyph_list = {}

	local brackets = "/%s/"

	-- Loops over the terms given as arguments.
	for _, term in ipairs(terms) do
		local respelling = term.respelling
		-- Handles magic symbols in the input.
		respelling = normalise_input(respelling, pagename, paramname)
		-- Obtains the transcription and hyphenation for the current index.
		local prons, hyph = multiword(respelling, lang)

		for i, pron in ipairs(prons) do
			if pron.phonetic then
				table.insert(pron_list, {
					pron = pron.phonetic,
					pron_with_syldivs = pron.phonetic,
					q = term.q,
					qq = term.qq,
					a = term.a,
					aa = term.aa,
					refs = i == 1 and term.refs or nil,
				})
			else
				local bracketed_pron = brackets:format(pron.pron)
				table.insert(pron_list, {
					pron = bracketed_pron,
					pron_with_syldivs = bracketed_pron,
					q = term.q,
					qq = term.qq,
					a = combine_qualifiers(pron.a, term.a),
					aa = term.aa,
					refs = i == 1 and term.refs or nil,
				})
				if not pron.norhyme then
					table.insert(rhyme_list, do_rhyme(pron.pron, lang))
				end
			end
		end

		-- If a hyphenation value had been returned by multiword(), make sure it matches the pagename; otherwise
		-- don't add. FIXME: This should be smarter in the presence of hyphens in the lemma.
		if hyph and hyph:gsub("%.", ""):gsub("‿", " ") == pagename then
			m_table.insertIfNot(hyph_list, hyph)
		end
	end

	return {
		pron_list = pron_list,
		hyph_list = hyph_list,
		rhyme_list = rhyme_list,
	}
end

function export.show(frame)
	local ilang = frame.args.lang

	local process_args = {
		 = {},
		 = {},  = { alias_of = "hyphs" },
		 = {},  = { alias_of = "rhymes" },
		 = {},  = { alias_of = "audios" },
		 = {},  = { alias_of = "homophones" },
		 = {}, -- for debugging or demonstration only
		 = {},
	}

	local args = require("Module:parameters").process(frame:getParent().args, process_args)
	local lang = require("Module:languages").getByCode(ilang, true, "allow etym")

	local termspec = args or "#"
	local terms = parse_respellings_with_modifiers(termspec, 1)
	local pagename = args.pagename or mw.loadData("Module:headword/data").pagename
	local indent = args.indent or "*"

	local pronobj = export.get_lect_pron_info(terms, pagename, 1, ilang)
	local hyph_list, rhyme_list = pronobj.hyph_list, pronobj.rhyme_list
	local hyph_automatic = true
	local do_hyph

	if args.hyphs then
		hyph_automatic = false
		if args.hyphs == "-" then
			do_hyph = false
		else
			hyph_list = split_on_comma(args.hyphs)
			do_hyph = true
		end
	else
		do_hyph = true
	end

	if args.rhymes then
		if args.rhymes == "-" then
			rhyme_list = {}
		elseif args.rhymes ~= "+" then
			rhyme_list = {}
			for _, rhyme in ipairs(split_on_comma(args.rhymes)) do
				if rfind(rhyme, ".+/.+") then
					table.insert(rhyme_list, {
						rhyme = rsub(rhyme, "/.+", ""),
						num_syl = { tonumber(rsub(rhyme, ".+/", "")) },
					})
				else
					error(("The manual rhyme %s did not specify syllable number as RHYME/NUM_SYL"):format(rhyme))
				end
			end
		end
	end

	-- This deals with duplicate values in rhymes.
	if #rhyme_list > 0 then
		local temp_rhyme_list = {}
		local indices = {}
		for _, rhymeobj in ipairs(rhyme_list) do
			local index = indices
			if index == nil then
				table.insert(temp_rhyme_list, rhymeobj)
				indices = #temp_rhyme_list
			else
				local different_num_syl = true
				for _, ns in ipairs(temp_rhyme_list.num_syl) do
					if ns == rhymeobj.num_syl then
						different_num_syl = false
						break
					end
				end
				if different_num_syl then
					table.insert(temp_rhyme_list.num_syl, rhymeobj.num_syl)
				end
			end
		end
		rhyme_list = temp_rhyme_list
	end

	local m_IPA_format = require("Module:IPA").format_IPA_full
	local parts = {}
	local function ins(text)
		table.insert(parts, text)
	end

	local do_collapse = false

	if pronobj.pron_list then
		ins(indent .. m_IPA_format { lang = lang, items = pronobj.pron_list })
	end

	local em_length

	if args.audios then
		local format_audio = require("Module:audio").format_audio
		local audio_objs = parse_audio(lang, args.audios, pagename, "audios")
		local num_audios = #audio_objs
		for i, audio_obj in ipairs(audio_objs) do
			if num_audios > 1 and not audio_obj.caption then
				audio_obj.caption = "Audio " .. i
			end
			ins("\n" .. indent .. " " .. format_audio(audio_obj))
		end
	end

	if #rhyme_list > 0 then
		ins("\n" .. indent .. " " .. require("Module:rhymes").format_rhymes { lang = lang, rhymes = rhyme_list })
	end

	if do_hyph then
		ins("\n" .. indent .. " ")
		if #hyph_list > 0 then
			local hyphs = {}
			local seen_num_pron_syls
			if pronobj.pron_list then
				-- Compute the number of syllables for each pronunciation, for use below.
				seen_num_pron_syls = {}
				for _, pronobj in ipairs(pronobj.pron_list) do
					local pron = pronobj.pron_with_syldivs
					-- Convert prosodic boundaries to regular spaces; ignore stress markers at the beginning
					-- of a word; and then count occurrences of spaces, syllable dividers and stress marks.
					local num_pron_syls = 1 + ulen(rsub(rsub(rsub(pron:gsub(" | ", " "), " ", " "), "^?", ""),
						"", ""))
					m_table.insertIfNot(seen_num_pron_syls, {pron = pron, nsyl = num_pron_syls})
				end
			end
			for i, hyph in ipairs(hyph_list) do
				hyphs = { hyph = {} }
				for syl in hyph:gmatch("+") do
					table.insert(hyphs.hyph, syl)
				end
				if pronobj.pron_list then
					-- Match each syllabification against the syllable counts of all pronunciations, since we don't have
					-- any alignment between the two. Only complain if the syllable count of the syllabification doesn't
					-- match the syllable count of any pronunciation.
					local num_hyph_syls = 1 + #(hyph:gsub("", ""))
					local matched = false
					for _, num_pron_syls in ipairs(seen_num_pron_syls) do
						if num_pron_syls.nsyl == num_hyph_syls then
							matched = true
							break
						end
					end
					if not matched then
						local prons_and_sylcounts = {}
						for _, num_pron_syls in ipairs(seen_num_pron_syls) do
							table.insert(prons_and_sylcounts, ("%s:%s"):format(num_pron_syls.pron, num_pron_syls.nsyl))
						end
						mw.log(("Syllable count %s for syllabification '%s' doesn't match pronunciation syllable count(s) %s for '%s'"):format(
							num_hyph_syls, hyph, table.concat(prons_and_sylcounts, ","), pagename))
						if mw.title.getCurrentTitle().nsText == "" then
							ins(("]"):format(
								lang:getFullName(), ilang, hyph_automatic and "automatic" or "manual"))
						end
					end
				end		
			end
			ins(require("Module:hyphenation").format_hyphenations {
				lang = lang, hyphs = hyphs, caption = "Syllabification"
			})
		else
			ins("Syllabification: <small></small>")
			if mw.title.getCurrentTitle().nsText == "" then
				ins(("]"):format(
					lang:getFullName(), ilang))
			end
		end
	end

	if args.homophones then
		local homophone_list = parse_homophones(args.homophones, "homophones")
		ins("\n" .. indent .. " " .. require("Module:homophones").format_homophones {
			lang = lang,
			homophones = homophone_list,
		})
	end

	local ret = table.concat(parts)
	if em_length then
		ret = m_str_utils.gsub(ret, "{width}", em_length)
	end

	return ret
end

return export