Module:IPAc2-mh

-- This module is primarily maintained at: -- https://en.wiktionary.org/wiki/Module:mh-pronunc

local export = {}

local math_max = math.max local mw_text_gsplit = mw.text.gsplit local mw_text_split = mw.text.split local mw_text_trim = mw.text.trim local mw_ustring_gsub = mw.ustring.gsub local mw_ustring_lower = mw.ustring.lower local string_byte = string.byte local string_find = string.find local string_gmatch = string.gmatch local string_gsub = string.gsub local string_lower = string.lower local string_sub = string.sub local table_concat = table.concat local table_remove = table.remove

local ASYLL = "̯" local BREVE = "̆" local CEDILLA = "̧" local MACRON = "̄" local TIE = "͡" local TIE2 = "͜"

local C1_ = "pbtdcJszkgmnNrlyYhH_" local C1 = "["..C1_.."]" local C = ".[jGw]" local V_ = "aEeiAV7MQOou" local V = "["..V_.."]" local VI_ = V_.."I" local VI = "["..VI_.."]" local S = "[%s%-]+"

local UTF8_CHAR = "[%z\1-\127\194-\244][\128-\191]*"

local function addUnique(seq, value) for _, value2 in pairs(seq) do		if value == value2 then return end end seq[#seq + 1] = value end

local function assign(target, ...) local args = { ... }	for _, source in pairs(args) do		if type(source) == "table" then for key, value in pairs(source) do				target[key] = value end end end return target end

local function fastTrim(text) if text == "" or text == " " then return "" else local atLeft = string_byte(text) == 32 local atRight = string_byte(text, -1) == 32 if atLeft then if atRight then return string_sub(text, 2, -2) else return string_sub(text, 2) end elseif atRight then return string_sub(text, 1, -2) else return text end end end

local function parseBoolean(text) if type(text) == "string" then text = string_gsub(text, "[^0-9A-Za-z]", "") if text ~= "" and text ~= "0" and string_lower(text) ~= "false" then return true end end return false end

local function splitChars(text, pattern, chars, shorten) chars = chars or {} local index = 1 for ch in string_gmatch(text, pattern or UTF8_CHAR) do		chars[index] = ch		index = index + 1 end if index <= #chars then if shorten then table_remove(chars, index) else repeat chars[index] = nil index = index + 1 until index > #chars end end return chars end

local function string_gsub2(text, pattern, subst) return string_gsub(string_gsub(text, pattern, subst), pattern, subst) end

local function tableGet(value, key1, key2, key3) if type(value) ~= "table" or key1 == nil then return value end value = value[key1] if key2 == nil then return value end if type(value) ~= "table" then return nil end value = value[key2] if key3 == nil then return value end if type(value) ~= "table" then return nil end return value[key3] end

local function ZTBL(text, sep) local tbl = {} for key in mw_text_gsplit(text, sep or " ") do		tbl[key] = true end return tbl end

local PARSE_PSEUDO_GLIDE local PARSE_C_CH_CW local PARSE_REMAINING

local function parse(code) local outSeq = {} code = mw_ustring_gsub(code, "%s+", " ") code = string_lower(code) for text in mw_text_gsplit(code, " ?,[ ,]*") do		text = fastTrim(text) if text ~= "" then local temp = string_gsub(text, "[abdeghijklmnprtwy_&'%- ]", "") if temp ~= "" then error("'"..code.."' contains unsupported characters: "..temp) end -- Recognize "y_", "h_", "w_", "_y", "_h", "_w" as pseudo-glides. PARSE_PSEUDO_GLIDE = PARSE_PSEUDO_GLIDE or { ["y"] = "0", ["h"] = "0h", ["w"] = "0w" }			text = string_gsub(text, "_*([hwy])_+", PARSE_PSEUDO_GLIDE) text = string_gsub(text, "_+([hwy])", PARSE_PSEUDO_GLIDE) if string_find(text, "_") then error("contains misplaced underscores: "..code) end -- a plain {i} protected from dialect-specific reflexes text = string_gsub(text, "'i", "I") -- "yi'y" and "'yiy" sequences text = string_gsub(text, "('?)yi('*)y", function(aposA, aposB)				if aposA ~= "" then					-- "dwelling upon" i					return "Z"				elseif aposB ~= "" then					-- "passing over lightly" i					return "z"				end			end) -- Convert multigraphs to pseudo-X-SAMPA format. PARSE_C_CH_CW = PARSE_C_CH_CW or { ["k"]  = "kG", ["kh"] = "kGh", -- N\A ["kw"] = "kW", ["l"]  = "lJ", ["lh"] = "lG", ["lw"] = "lW", ["m"]  = "mJ", ["mh"] = "mG", ["mw"] = "mJw", -- N\A ["n"]  = "nJ", ["nh"] = "nG", ["nw"] = "nW", ["ng"] = "NG", ["ngh"] = "NGh", -- N\A ["ngw"] = "NW", ["r"]  = "rG", ["rh"] = "rGh", -- N\A ["rw"] = "rW", ["0"]  = "_J", ["0h"] = "_G", ["0w"] = "_W" }			text = string_gsub(text, "[klmnr0]g?[hw]?", PARSE_C_CH_CW) if string_find(text, "g") then error("contains g that is not part of ng: "..code) end -- Convert remaining sequences to pseudo-X-SAMPA format. PARSE_REMAINING = PARSE_REMAINING or { ["b"] = "pG", ["d"] = "rj", ["e"] = "E", ["&"] = "e", ["h"] = "hG", ["j"] = "tj", ["J"] = "j", ["p"] = "pj", ["t"] = "tG", ["w"] = "hw", ["W"] = "w", ["y"] = "hj", ["z"] = "yj", ["Z"] = "Yj", ["'"] = ""			}			text = string_gsub(text, ".", PARSE_REMAINING) -- Enforce CVC, CVCVC, CVCCVC, etc. phonotactics, -- but allow VC, CV at affix boundaries -- where a vowel may link to another morpheme's consonant. temp = string_gsub(text, "[%s%-]+", "") if	string_find(temp, "_..[jGw]") or				string_find(temp, ".[jGw]_.") then error("pseudo-glides may not neighbor a consonant") end if string_find(temp, VI.."_."..VI) then error("pseudo-glides may only be at the beginning or end"..code) end if string_find(temp, VI..VI) then error("vowels must be separated by a consonant: "..code) end if string_find(temp, ".[jGw].[jGw].[jGw]") then error("each consonant cluster is limited to two: "..code) end if string_find(temp, ".[jGw].[jGw]$") then error("may not end with a consonant cluster: "..code) end string_gsub(temp, "^(.[jGw])(.[jGw])", function(consonX, consonY)				if consonX ~= consonY then					error("may only begin with single or geminated consonant: " ..code)				end			end) if text ~= "" then addUnique(outSeq, text) end end end return outSeq end

local BENDER_1968 = { ["pj"] = "p", ["pG"] = "b", ["tj"] = "j", ["tG"] = "t", ["kG"] = "k", ["kw"] = "q", ["mj"] = "m", ["mG"] = "ṁ", ["nj"] = "n", ["nG"] = "ṅ", ["nw"] = "n̈", ["NG"] = "g", ["Nw"] = "g̈", ["rj"] = "d", ["rG"] = "r", ["rw"] = "r̈", ["lj"] = "l", ["lG"] = "ł", ["lw"] = "l̈", ["yj"] = "yi'y", ["Yj"] = "'yiy", ["hj"] = "y", ["hG"] = "h", ["hw"] = "w", ["_j"] = "", ["_G"] = "",  ["_w"] = "", ["a"] = "a", ["E"] = "e", ["e"] = "&", ["i"] = "i", ["I"] = "i" } local BENDER_MED = assign({}, BENDER_1968, {	["mG"] = "m̧",	["nG"] = "ņ",	["nw"] = "ņ°",	["Nw"] = "g°",	["rw"] = "r°",	["lG"] = "ļ",	["lw"] = "ļ°",	["e"] = "ȩ" }) local BENDER_MOD = assign({}, BENDER_MED, {	["kw"] = "kʷ",	["mG"] = "ṃ",	["nG"] = "ṇ",	["nw"] = "ṇʷ",	["Nw"] = "gʷ",	["rw"] = "rʷ",	["lG"] = "ḷ",	["lw"] = "ḷʷ",	["e"] = "ẹ" }) local BENDER_DEFAULT = assign({}, BENDER_MOD, {	["mG"] = "m̧",	["nG"] = "ņ",	["nw"] = "ņʷ",	["lG"] = "ļ",	["lw"] = "ļʷ",	["e"] = "ȩ" }) local BENDER_MAPS = { ["1968"] = BENDER_1968, ["med"] = BENDER_MED, ["mod"] = BENDER_MOD }

local function toBender(inSeq, args) -- "1968" is from "Marshallese Phonology" (1968 by Byron W. Bender). -- "med" is from the Marshallese-English Dictionary (1976). -- "mod" is from the Marshallese-English Online Dictionary. -- "default" is the same as "mod" but with cedillas. local version = args and args.version local map = BENDER_MAPS[ type(version) == "string" and string_lower(version) or "" ] or BENDER_DEFAULT local outSeq = {} for _, text in pairs(inSeq) do		text = string_gsub(text, ".[jGw]?", map) addUnique(outSeq, text) end return outSeq end

local PHONEMIC_MAP = { ["pj"] = "pʲ", ["pG"] = "pˠ", ["tj"] = "tʲ", ["tG"] = "tˠ", ["kG"] = "k", ["kw"] = "kʷ", ["mj"] = "mʲ", ["mG"] = "mˠ", ["nj"] = "nʲ", ["nG"] = "nˠ", ["nw"] = "nʷ", ["NG"] = "ŋ", ["Nw"] = "ŋʷ", ["rj"] = "rʲ", ["rG"] = "rˠ", ["rw"] = "rʷ", ["lj"] = "lʲ", ["lG"] = "lˠ", ["lw"] = "lʷ", ["hj"] = "j", ["hG"] = "ɰ",  ["hw"] = "w", ["_j"] = "",  ["_G"] = "",   ["_w"] = "", ["a"] = "æ", ["E"] = "ɛ", ["e"] = "e", ["i"] = "i", ["I"] = "i" } if false then assign(PHONEMIC_MAP, {		["a"] = "ɐ",		["E"] = "ə",		["e"] = "ɘ",		["i"] = "ɨ",		["I"] = "ɨ"	}) end assign(PHONEMIC_MAP, {	["yj"] = "j"..PHONEMIC_MAP["i"]..ASYLL.."j",	["Yj"] = "j"..PHONEMIC_MAP["i"].."ːj" })

local function toPhonemic(inSeq) local outSeq = {} for _, text in pairs(inSeq) do		text = string_gsub(text, ".[jGw]?", PHONEMIC_MAP) addUnique(outSeq, text) end return outSeq end

local VOWEL = { -- VOWELS[f1][f2] { "a", "A", "Q" }, { "E", "V", "O" }, { "e", "7", "o" }, { "i", "M", "u" } }

local F1 = {} local F2_FRONT = 1 local F2_BACK = 2 local F2_ROUND = 3 local F2 = { ["j"] = F2_FRONT, ["G"] = F2_BACK, ["w"] = F2_ROUND }

local FRONT_VOWEL = {} local BACK_VOWEL = {} local ROUND_VOWEL = {}

for f1, row in pairs(VOWEL) do	local front = row[F2_FRONT] local back = row[F2_BACK] local round = row[F2_ROUND] for f2, vowel in pairs(row) do		F1[vowel] = f1		F2[vowel] = f2		FRONT_VOWEL[vowel] = front BACK_VOWEL[vowel] = back ROUND_VOWEL[vowel] = round end end

local function maxF1(a, b)	if b then return VOWEL[math_max(2, F1[a], F1[b])][F2_FRONT] else return VOWEL[math_max(2, F1[a])][F2_FRONT] end end

local function toPhoneticDialect(text, config, isRalik) -- Morphemes can begin with geminated consonants, but spoken words cannot. text = string_gsub(text, "^(.[jGw])%1("..VI..")", function(conson, vowel)		if conson == "hG" then			if isRalik then				return "hG"..vowel.."hG"..vowel			else				return "hG"..vowel			end		else			if isRalik then				return "hj"..maxF1(vowel)..conson..conson..vowel			else				return conson..maxF1(vowel)..conson..vowel			end		end	end) -- Initial {yiyV-, yiwV-, wiwV-} sequences have special behavior. -- To block this in the template argument, use "'i" instead of "i". text = " "..text text = string_gsub(text, "([ jGw])(h[jw])i(h[jw])("..VI..")",		function(nonVowel, consonX, consonY, vowel)			if consonY == "hw" then				-- {yiwV-, wiwV-} sequences				if isRalik then					-- Rālik {wiwV-} becomes {yiwV-}.					consonX = "hj"				end				-- {[yw]iwV-} becomes {[yw]iwwV-} in both dialects.				return nonVowel..consonX.."I"..consonY..consonY..vowel			elseif consonX == "hj" then				-- {yiyV-} sequences				if isRalik then					-- "dwelling upon" i					return nonVowel.."Yj"..vowel				else					-- "passing over lightly" i					return nonVowel.."yj"..vowel				end			end		end	) text = string_sub(text, 2) -- Restore protected {i}, we won't be checking for it anymore. text = string_gsub(text, "I", "i") return text end

local EMPTY = {}

local IS_VOWEL = FRONT_VOWEL

local VOICED_PRIMARY = { ["p"] = "b", ["t"] = "d", ["c"] = "J", ["s"] = "z", ["k"] = "g" } local VOICELESS_PRIMARY = { ["b"] = "p", ["d"] = "t", ["J"] = "c", ["z"] = "s", ["g"] = "k" }

local function substDiphthong(	primaryL, secondaryL, vowel, epenth, primaryR, secondaryR ) local f1 = F1[vowel] return (		primaryL..secondaryL..		VOWEL[f1][F2[secondaryL]]..epenth.."="..		VOWEL[f1][F2[secondaryR]]..epenth..		primaryR..secondaryR	) end

local JVJ_VOWEL local JVK_VOWEL local KVJ_VOWEL local KVK_VOWEL local QVJ_VOWEL local QVK_VOWEL local CVQ_VOWEL local CVC_VOWEL local GVC_VOWEL local PHONETIC_GLIDE local PHONETIC_IPA

local function toPhoneticRemainder(code, config) local text = code local chars, subst -- If the phrase begins or ends with a bare vowel -- and no pseudo-glide, display phrase three times -- with each of the different pseudo-glides. if IS_VOWEL[string_sub(text, 1, 1)] then toPhoneticRemainder("_j"..text, config) toPhoneticRemainder("_G"..text, config) toPhoneticRemainder("_w"..text, config) return end if IS_VOWEL[string_sub(text, -1)] then toPhoneticRemainder(text.."_j", config) toPhoneticRemainder(text.."_G", config) toPhoneticRemainder(text.."_w", config) return end local careful = config.careful if	careful ~= false and careful ~= true then local superSeq = config.outSeq local subSeq = {} config.outSeq = subSeq config.careful = true toPhoneticRemainder(text, config) config.careful = false toPhoneticRemainder(text, config) config.careful = careful config.outSeq = superSeq addUnique(superSeq, table_concat(subSeq, " ~ ")) return end local diphthongs = config.diphthongs local initialJ  = config.initialJ local medialJ   = config.medialJ local finalJ    = config.finalJ local geminateJ = config.geminateJ local noHints   = config.noHints local voice     = config.voice -- Glides always trigger epenthesis, even neighboring other glides. if not diphthongs then -- {ww} always causes the second glide to surface. text = string_gsub(text, "([aEei])hw".."hw", "%1hw".."%1@".."Hw") end text = string_gsub(text, "([aEei])hG".."(.[jGw])", "%1hG".."%1@".."%2") text = string_gsub(text, "(.[jGw])".."hG([aEei])", "%1".."%2@".."hG%2") text = string_gsub(text, "([aEei])h(.)".."(.[jGw])", "%1h%2".."%1@".."%3") text = string_gsub(text, "(.[jGw])".."h(.)([aEei])", "%1".."%3@".."h%2%3") text = string_gsub(text, "(.[jGw])".."([yY].)", "%1".."i@".."%2") -- Preserve these exceptionally stable clusters. text = string_gsub(text, "l([jG])".."tG", "l%1|tG") -- Unstable consonant clusters trigger epenthesis. -- Liquids before coronal obstruents. text = string_gsub(text, "([rl].)".."t", "%1vt") -- Nasals and liquids after coronal obstruents. text = string_gsub(text, "t(.)".."([nrl])", "t%1v%2") -- Heterorganic clusters. -- Labial consonants neighboring coronal or dorsal consonants. text = string_gsub(text, "([pm].)".."([tnrlkN])", "%1v%2") -- Coronal consonants neighboring labial or dorsal consonants. text = string_gsub(text, "([tnrl].)".."([pmkN])", "%1v%2") -- Dorsal consonants neighboring labial or coronal consonants. text = string_gsub(text, "([kN].)".."([pmtnrl])", "%1v%2") if careful then -- In careful speech, clusters trigger epenthesis if they have the -- same primary articulation but different secondary articulations. text = string_gsub(text, "(.)([jGw])%1([jGw])",			function(primary, secondaryL, secondaryR)				if secondaryL ~= secondaryR then					return primary..secondaryL.."v"..primary..secondaryR				end			end		) else -- Organic speech involves certain consonant cluster assimilations. -- Forward assimilation of rounded consonants. -- There is no rounded coronal obstruent. text = string_gsub(text, "(w.)[jG]", "%1w") -- There is no rounded coronal obstruent. text = string_gsub(text, "tw", "tG") -- Backward assimilation of remaining secondary articulations. text = string_gsub(text, "[jGw](.)([jGw])", "%2%1%2") -- Backward nasal assimilation of primary articulations. text = string_gsub(text, "[pkrl](.)([mnN])", "%2%1%2") end -- No longer need to protect exceptionally stable consonant clusters. text = string_gsub(text, "|", "") -- Give a vowel height to all epenthetic vowels that still lack one. text = string_gsub2(text, "(.)(..)v(..)(.)",		function(vowelL, consonL, consonR, vowelR)			return vowelL..consonL..maxF1(vowelL, vowelR).."@"..consonR..vowelR		end	) -- Tag all vowels for next set of operations. text = string_gsub(text, "([aEei])", "/%1") -- There is no variation in the surface realizations of vowels -- between two identical secondary articulations. text = string_gsub2(text, "([jGw])/([aEei])(@?.)%1",		function(secondary, vowel, infix)			return secondary..VOWEL[F1[vowel]][F2[secondary]]..infix..secondary		end	) if diphthongs then text = string_gsub2(text,			"(.)([jGw])/([aEei])(@?)(.)([jGw])", substDiphthong) else -- Represent vowels neighboring pseudo-glides are diphthongs regardless. text = string_gsub(text,			"(_)([jGw])/([aEei])(@?)(.)([jGw])", substDiphthong) text = string_gsub(text,			"(.)([jGw])/([aEei])(@?)(_)([jGw])", substDiphthong) -- Vowels between two non-glides have the most predictable reflexes. if not CVC_VOWEL then JVJ_VOWEL = { "a", "E", "e", "i" } JVK_VOWEL = { "A", "E", "e", "i" } KVJ_VOWEL = { "A", "V", "7", "i" } KVK_VOWEL = { "A", "V", "7", "M" } QVJ_VOWEL = { "A", "V", "7", "u" } QVK_VOWEL = { "A", "O", "o", "u" } CVQ_VOWEL = { "Q", "O", "o", "u" } CVC_VOWEL = CVC_VOWEL or { { JVJ_VOWEL, JVK_VOWEL, CVQ_VOWEL }, { KVJ_VOWEL, KVK_VOWEL, CVQ_VOWEL }, { QVJ_VOWEL, QVK_VOWEL, CVQ_VOWEL } }		end text = string_gsub2(text, "([ptkmnNrl])(.)/([aEei])(@?)([ptkmnNrl])(.)",			function(primaryL, secondaryL, vowel, epenth, primaryR, secondaryR)				if	secondaryL == "j" and					secondaryR == "G" and ( primaryL == "n" or						primaryL == "l" )				then					vowel = KVJ_VOWEL[F1[vowel]]				else					vowel = CVC_VOWEL[F2[secondaryL]][F2[secondaryR]][F1[vowel]]				end				return primaryL..secondaryL..vowel..epenth..primaryR..secondaryR			end		) -- Vowels always claim the secondary articulation -- of a neighboring back unrounded glide. text = string_gsub(text, "hG/([aEei])", function(vowel)			return "hG"..BACK_VOWEL[vowel]		end) text = string_gsub(text, "/([aEei])(@?)hG", function(vowel, epenth)			return BACK_VOWEL[vowel]..epenth.."hG"		end) -- Unless already claimed, epenthetic vowels after a glide -- always claim the secondary articulation to the left. text = string_gsub(text, "([hH])(.)/([aEei])@",			function(primaryL, secondaryL, vowel)				return ( primaryL..secondaryL.. VOWEL[F1[vowel]][F2[secondaryL]].."@" )			end		) -- Unless already claimed, vowels before a glide -- always claim the secondary articulation to the right. text = string_gsub(text, "/([aEei])(@?)([hHyY])(.)",			function(vowel, epenth, primaryR, secondaryR)				return ( VOWEL[F1[vowel]][F2[secondaryR]]..epenth.. primaryR..secondaryR )			end		) -- For now, unless already claimed, vowels before a rounded consonant -- claim the secondary articulation to the right. text = string_gsub(text, "/([aEei])(@?.w)", function(vowel, suffix)			return ROUND_VOWEL[vowel]..suffix		end) -- For now, unless already claimed, remaining vowels -- claim the secondary articulation to the left. text = string_gsub(text, "([jGw])/([aEei])", function(secondaryL, vowel)			return secondaryL..VOWEL[F1[vowel]][F2[secondaryL]]		end) -- Vowels after {yi'y} and {'yiy} -- claim the secondary articulation to the right. -- In organic speech, this may happen only word-initially. subst = function(consonL, vowel, infix, secondaryR) return consonL..VOWEL[F1[vowel]][F2[secondaryR]]..infix..secondaryR end if careful then text = string_gsub(text, "([yY].)([aEei])(@?.)([jGw])", subst) else text = string_gsub(text, "^([yY].)([aEei])(@?.)([jGw])", subst) end -- Change certain vowels in a special environment from round to front. text = string_gsub(text, "([hyY]j)([Oou])(.w)("..V..")",			function(prefix, vowelL, conson, vowelR)				if conson ~= "hw" or F1[vowelL] ~= F1[vowelR] then					return prefix..FRONT_VOWEL[vowelL]..conson..vowelR				end			end		) text = string_gsub(text, "([hyY]j)([Oou])(.w.w)",			function(prefix, vowel, suffix)				return prefix..FRONT_VOWEL[vowel]..suffix			end		) text = string_gsub(text, "(a@?hj)Q(.w"..V..")", "%1a%2") text = string_gsub(text, "(a@?hj)Q(.w.w)", "%1a%2") -- Tag certain glide-vowel-non-glide sequences for special reflexes. text = string_gsub(text, "(H[jw])("..V.."[ptkmnNrl])", "%1/%2") text = string_gsub(text, "^(h[jw])("..V.."[ptkmnNrl])", "%1/%2") if careful then text = string_gsub(text, "(@h[jw])("..V.."[ptkmnNrl])", "%1/%2") end text = string_gsub(text,			"([EeiAV7MOou]@?h[jw])([aAQ][ptkmnNrl])", "%1/%2") text = string_gsub(text, "(hj[aEei]@?hw)("..V.."[ptkmnNrl])", "%1/%2") -- Untag certain sequences, exempting them from special reflexes. text = string_gsub(text, "hj/([aEei][knNrl]w)", "hj%1") text = string_gsub(text, "hj/a([tr]G)", "hja%1") -- Special reflexes. text = string_gsub(text, "w/("..V..")tG", function(vowel)			return "w"..QVJ_VOWEL[F1[vowel]].."tG"		end) GVC_VOWEL = GVC_VOWEL or { { JVJ_VOWEL, JVK_VOWEL, CVQ_VOWEL }, { KVK_VOWEL, KVK_VOWEL, KVK_VOWEL }, { KVK_VOWEL, QVK_VOWEL, CVQ_VOWEL } }		text = string_gsub(text, "([jw])/("..V..")(.)([jGw])",			function(secondaryL, vowel, primaryR, secondaryR)				return secondaryL..GVC_VOWEL[					F2[secondaryL]				][					F2[secondaryR]				][F1[vowel]]..primaryR..secondaryR			end		) end -- {yi'y} neighboring epenthetic {i} cancels the epenthetic vowel. text = string_gsub(text, "i@yj", "yj") -- {yi'y} neighboring non-epenthetic {i} may now be demoted to {y}. text = string_gsub(text, "([iMu])yj", "%1hj") text = string_gsub(text, "yj([iMu])", "hj%1") -- {'yiy} may now be demoted everywhere. text = string_gsub(text, "i@Yj", "i@hjihj") text = string_gsub(text, "Yj", "hjihji@hj") -- For the purposes of this template, -- surface all glides pronounced in isolation. text = string_gsub(text, "^h(.)$", "H%1") if diphthongs then -- Surface realization of {yi'y}. text = string_gsub(text, "yj", "i^") -- Delete all remaining glides. text = string_gsub(text, "[hH].", "") else -- Surface this glide. text = string_gsub(text, "([ptkmnNrl].)[aEei]@hj([AV7MQOou])", "%1Hj%2") -- Opportunistically front this vowel. if careful then text = string_gsub(text,				"([yh]j)A([kN]G[kN]?G?"..V.."[^@])", "%1a%2") text = string_gsub2(text,				"([yh]j)A([kN]G[kN]?G?"..V.."@h)", "%1a%2") else text = string_gsub(text, "([yh]j)A([kN]G[kN]?G?"..V..")", "%1a%2") end -- Surface certain other glides. text = string_gsub(text, "hw([aEeiAV7M])", "Hw%1") text = string_gsub(text, "^hj([AV7MQOou])", "Hj%1") text = string_gsub(text, "([AV7MQOou]@?)hj([AV7MQOou])", "%1Hj%2") text = string_gsub(text, "([aEeiAV7M])(@?)(hw)([QOou])",			function(vowelL, epenthL, conson, vowelR)				if F1[vowelL] > F1[vowelR] then					return vowelL..epenthL.."Hw"..vowelR				end			end		) text = string_gsub(text, "([AV7MQOou])(@?)(hj)([aEei])",			function(vowelL, epenthL, conson, vowelR)				if F1[vowelL] > F1[vowelR] then					return vowelL..epenthL.."Hj"..vowelR				end			end		) text = string_gsub(text, "([aEei])(@?)(hj)([AV7MQOou])",			function(vowelL, epenthL, conson, vowelR)				if F1[vowelL] < F1[vowelR] then					return vowelL..epenthL.."Hj"..vowelR				end			end		) text = string_gsub(text, "("..V..")(h)([jw])$",			function(vowel, primary, secondary)				if F2[vowel] ~= F2[secondary] then					return vowel.."H"..secondary				end			end		) -- De-epenthesize vowels if they still neighbor unsurfaced glides. text = string_gsub(text, "("..V..")@(h.)", "%1%2") text = string_gsub(text, "(h."..V..")@", "%1") -- Surface realization for remaining glides and {yi'y}, -- except surfaced {y}. PHONETIC_GLIDE = PHONETIC_GLIDE or { -- Delete remaining unsurfaced glides. ["hj"] = "", ["hG"] = "", ["hw"] = "", -- Simple realizations for surfaced {h}, {w} and {yi'y}. ["HG"] = "?", ["Hw"] = "W", ["yj"] = "i^" }		text = string_gsub(text, "[yhH].", PHONETIC_GLIDE) -- Realization for surfaced {y}. text = string_gsub2(text, "("..V.."?)(@?)Hj("..V.."?)",			function(vowelL, epenthL, vowelR)				if vowelL ~= "" then					if vowelR ~= "" then						return ( vowelL..epenthL.. maxF1(vowelL, vowelR).."^"..vowelR )					else						return vowelL..epenthL..maxF1(vowelL).."^"					end				else					if vowelR ~= "" then						return maxF1(vowelR).."^"..vowelR					else						return "i^"					end				end			end		) -- Adjust this surfaced glide. text = string_gsub(text, "([ptkmnNrl].[aEei])%^("..V..")", "%1@%2") end chars = splitChars(text, ".") if not diphthongs then -- Geminate long vowels. local index = #chars repeat local ch = chars[index] local index2 = index - 1 if IS_VOWEL[ch] then local ch2 = chars[index + 1] if	ch2 ~= "@" and ch2 ~= "^" and chars[index2] == ch				then chars[index] = ":" end end index = index2 until index == 1 text = table_concat(chars, "") end -- Tweak remaining consonants, using offsets as a guide. text = string_gsub(text, "(.)([jGw])([ptkmnNrl]?)([jGw]?)",		function(offsetL, primaryL, secondaryL, primaryR, secondaryR, offsetR)			local isInitial = offsetL == 1			local isFinal = offsetR == #chars + 1			if primaryL == "_" then				if noHints then					-- Delete pseudo-glide.					return ""				elseif isInitial then					-- Show secondary articulation to the left, not the right.					return secondaryL..primaryL				else					return primaryL..secondaryL				end			end			if	primaryR == "t" and				secondaryR == "j"			then				if	primaryL == "t" and					secondaryL == "j"				then					primaryL = geminateJ					primaryR = geminateJ				else					primaryR = medialJ				end			elseif				primaryL == "t" and				secondaryL == "j"			then				if isInitial then					primaryL = initialJ				elseif isFinal then					primaryL = finalJ				else					primaryL = medialJ				end			end			if primaryR ~= "" then				-- Consonant cluster. if	primaryL ~= primaryR and primaryL ~= "l" then primaryL = VOICED_PRIMARY[primaryL] or primaryL primaryR = VOICED_PRIMARY[primaryR] or primaryR end if secondaryL == secondaryR then if primaryL == primaryR then primaryR = ":" secondaryR = "" else secondaryL = "" end end elseif not isInitial and not isFinal then -- Medial single consonant. primaryL = VOICED_PRIMARY[primaryL] or primaryL end if voice == false then primaryL = VOICELESS_PRIMARY[primaryL] or primaryL primaryR = VOICELESS_PRIMARY[primaryR] or primaryR elseif voice == true then primaryL = VOICED_PRIMARY[primaryL] or primaryL primaryR = VOICED_PRIMARY[primaryR] or primaryR end return primaryL..secondaryL..primaryR..secondaryR end )	-- Dorsal consonants are already velarized by default.	text = string_gsub(text, "([kgN])G", "%1")	if not PHONETIC_IPA then		local map = {			["p"] = "p",			["b"] = "b",			["t"] = "t",			["d"] = "d",			["s"] = "s",			["z"] = "z",			["k"] = "k",			["g"] = "ɡ",			["m"] = "m",			["n"] = "n",			["N"] = "ŋ",			["r"] = "r",			["l"] = "l",			["?"] = "ʔ",			["W"] = "w",			["_"] = "‿",			["j"] = "ʲ",			["G"] = "ˠ",			["w"] = "ʷ",			["a"] = "æ",			["E"] = "ɛ",			["e"] = "e",			["i"] = "i",			["A"] = "ɑ",			["V"] = "ʌ",			["7"] = "ɤ",			["M"] = "ɯ",			["Q"] = "ɒ",			["O"] = "ɔ",			["o"] = "o",			["u"] = "u",			["^"] = ASYLL,			["@"] = BREVE,			[":"] = "ː",			["="] = TIE2		}		map["c"] = map["c"] or (map["t"]..TIE..map["s"])		map["J"] = map["J"] or (map["d"]..TIE..map["z"])		PHONETIC_IPA = map	end	text = string_gsub(text, ".", PHONETIC_IPA)	addUnique(config.outSeq, text) end

local PHONETIC_ARG_J

local function toPhonetic(inSeq, args) -- Recognize "ralik" for Rālik Chain (western dialect). -- Recognize "ratak" for Ratak Chain (eastern dialect). -- For other values, list both possible dialect reflexes where applicable. local dialect = args and args.dialect and mw_ustring_lower(mw_text_trim(args.dialect)) or "" if dialect == "rālik" then dialect = "ralik" end -- "false" will opportunistically assimilate -- consonant clusters and epenthetic vowels -- per Bender (1968) and Willson (2003). -- "true" will pronounce words in a more morpheme-isolating manner. -- Empty string or absent by default will display both modes if they differ. -- If diphthongs are enabled, vowel display is overridden with diphthongs. local careful = args and args.careful and mw_text_trim(args.careful) or "" if careful ~= "" then careful = parseBoolean(careful) end -- If enabled, display full diphthong allophones for short vowels. local diphthongs = not not (args and parseBoolean(args.diphthongs)) -- Argument "J" has format like "tstt". -- Recognized letters are "t" = plosive, "c" = affricate, "s" = fricative. -- Letters for initial, medial, final and geminate respectively. -- Real-world pronunciation said to vary by sociological factors, -- but all realizations may occur in free variation. PHONETIC_ARG_J = PHONETIC_ARG_J or { ["t"] = "t", ["c"] = "c", ["s"] = "s" } local modeJ = splitChars(args and args.J and string_lower(args.J) or "tstt") local initialJ = PHONETIC_ARG_J[modeJ[1] or ""] or "t" local medialJ = PHONETIC_ARG_J[modeJ[2] or ""] or "s" local finalJ = PHONETIC_ARG_J[modeJ[3] or ""] or initialJ local geminateJ = PHONETIC_ARG_J[modeJ[4] or ""] or initialJ -- If enabled, do not display pseudo-glide hints at all. local noHints = not not (args and parseBoolean(args.nohints)) -- "false" will display all obstruent allophones as voiceless. -- "true" will display all obstruent allophones as voiced. -- Empty string or absent by default will display -- only medial obstruent allophones as semi-voiced. local voice = args and args.voice or "" if voice ~= "" then voice = parseBoolean(voice) end local outSeq = {} local config = { ["outSeq"] = outSeq, ["careful"] = careful, ["diphthongs"] = diphthongs, ["initialJ"] = initialJ, ["medialJ"] = medialJ, ["finalJ"] = finalJ, ["geminateJ"] = geminateJ, ["noHints"] = noHints, ["voice"] = voice }	for _, str in pairs(inSeq) do		str = string_gsub(str, "[%s%-]+", "") local isRalik = dialect == "ralik" if isRalik or dialect == "ratak" then str = toPhoneticDialect(str, config, isRalik) toPhoneticRemainder(str, config) else local ralik = toPhoneticDialect(str, config, true) local ratak = toPhoneticDialect(str, config, false) -- If both dialect reflexes are the same, display only one of them. toPhoneticRemainder(ralik, config) if ralik ~= ratak then toPhoneticRemainder(ratak, config) end end end return outSeq end

export._parse = parse export._toBender = toBender export._toPhonemic = toPhonemic export._toPhonetic = toPhonetic

function export.bender(frame) return table_concat(toBender(parse(frame.args[1], frame.args)), ", ") end

function export.parse(frame) return table_concat(parse(frame.args[1]), ", ") end

function export.phonemic(frame) return table_concat(toPhonemic(parse(frame.args[1])), ", ") end

function export.phonetic(frame) return table_concat(toPhonetic(parse(frame.args[1]), frame.args), ", ") end

function export.phoneticMED(frame) return "DEPRECATED" end

function export.phoneticChoi(frame) return "DEPRECATED" end

function export.phoneticWillson(frame) return "DEPRECATED" end

return export