Module:mh-pronunc

-- This module is primarily maintained at: -- https://en.wiktionary.org/wiki/Module:mh-pronunc -- Please direct all technical queries and contributions there. -- The version of this script on Wikipedia is only a mirror.

local export = {}

local MERGED_MID_VOWELS = false local PARENTHETICAL_EPENTHESIS = true local PHONETIC_DETAILS = false local W_OFF_GLIDES = false

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

local C1_ = "pbtdSZszkgmnNrlyYhH_" local C1 = "["..C1_.."]" local C2_ = "jGw" local C = ".["..C2_.."]" local V_ = "aEei63@1AV7Mq<8>QOou" local V = "["..V_.."]" local S = "[%s%-]+"

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

local F2J = 1 local F2G = 3 local F2W = 5

-- Forward-declare functions and lookups. local addUnique local assign local BACK_VOWEL local BENDER_MAPS local CONSON_REFLEX local F1 local F2 local F2_SECOND local fastTrim local FRONT_VOWEL local IS_VOWEL local needCONSON_REFLEX local needPHONETIC_IPA local needVOWEL local needVOWEL_REFLEX local parse local PARSE_C_CH_CW local PARSE_PSEUDO_GLIDE local PARSE_REMAINING local parseBoolean local PHONEMIC_MAP local PHONETIC_ARG_J local PHONETIC_IPA local ROUND_VOWEL local splitChars local string_gsub2 local string_gsubx local tableGet local TO_MOD local toBender local toMOD local toPhonemic local toPhonetic local toPhoneticDialect local toPhoneticRemainder local VOICED_PRIMARY local VOICELESS_PRIMARY local VOWEL local VOWEL_REFLEX local ZTBL

-- Adds elements to a sequence as if it's a set (retains unique elements only). addUnique = function(seq, value) for _, value2 in pairs(seq) do		if value == value2 then return end end seq[#seq + 1] = value end

-- Intended to work the same as JavaScript's Object.assign function. assign = function(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

fastTrim = function(text) return string.match(text, "^%s*(.-)%s*$") end

maxF1 = function(a, b, c)	needVOWEL if c then return VOWEL[math.max(F1[a], F1[b], F1[c])][F2J] elseif b then return VOWEL[math.max(F1[a], F1[b])][F2J] else return FRONT_VOWEL[a] end end

needCONSON_REFLEX = function if CONSON_REFLEX then return end local map = { ["t"] = { ["j"] = "T" }, ["n"] = { ["j"] = "J" }, ["r"] = { ["j"] = "R" }, ["l"] = { ["j"] = "L" } }	for primary in mw.text.gsplit("ptkmnNrl", "") do		local map2 = map[primary] if not map2 then map2 = {} map[primary] = map2 end map2["j"] = map2["j"] or primary map2["G"] = map2["G"] or primary map2["w"] = map2["w"] or primary end map["T"] = map["t"] map["J"] = map["n"] map["R"] = map["r"] map["L"] = map["l"] CONSON_REFLEX = map VOICED_PRIMARY = { ["p"]="b", ["t"]="d", ["T"]="D", ["S"]="Z", ["s"]="z", ["k"]="g" }	VOICELESS_PRIMARY = { ["b"]="p", ["d"]="t", ["D"]="T", ["Z"]="S", ["z"]="s", ["g"]="k" } end

needPHONETIC_IPA = function if PHONETIC_IPA then return end needVOWEL local map = { ["p"] = "p", ["b"] = "b", ["B"] = "β̞", ["BG"] = "w", ["t"] = "t", ["d"] = "d", ["s"] = "s", ["z"] = "z", ["k"] = "k", ["g"] = "ɡ", ["m"] = "m", ["n"] = "n", ["N"] = "ŋ", ["r"] = "r", ["l"] = "l", -- ["Hj"] = "j", -- ["i^"] = "j", ["HG"] = "ʔ", ["Hw"] = "w", ["_"] = "‿",		["j"] = "ʲ", ["G"] = "ˠ", ["w"] = "ʷ", ["a"] = "æ", ["E"] = "ɛ", ["e"] = "e", ["i"] = "i", ["6"] = "ɐ", ["3"] = "ə", ["@"] = "ɘ", ["1"] = "ɨ", ["A"] = "ɑ", ["V"] = "ʌ", ["7"] = "ɤ", ["M"] = "ɯ", ["Q"] = "ɒ", ["O"] = "ɔ", ["o"] = "o", ["u"] = "u", ["^"] = ASYLL, ["#"] = ASYLL, ["("] = "(",		[")"] = ")",		[":"] = "ː", ["="] = TIE2, ["."] = ".",		-- ["&"] = " | "		["&"] = " "	}	if PHONETIC_DETAILS then assign(map, {			["BG"] = map["B"]..map["G"],			["t"] = "t̪",			["T"] = "t̠",			["d"] = "d̪",			["D"] = "d̠",			["s"] = "s̠",			["z"] = "z̠",			["k"] = "k̠",			["g"] = "ɡ̠",			["n"] = "n̠",			["J"] = "n̪",			["N"] = "ŋ̠",			["r"] = "r̠",			["R"] = "r̪",			["l"] = "l̠",			["L"] = "l̪"		}) end map["T"] = map["T"] or map["t"] map["D"] = map["D"] or map["d"] map["S"] = map["S"] or (map["T"]..map["s"]) map["Z"] = map["Z"] or (map["D"]..map["z"]) map["kG"] = map["kG"] or map["k"] map["gG"] = map["gG"] or map["g"] map["J"] = map["J"] or map["n"] map["NG"] = map["NG"] or map["N"] map["R"] = map["R"] or map["r"] map["L"] = map["L"] or map["l"] map["Hj"] = map["Hj"] or map["i"]..map["^"] local key for primary in mw.text.gsplit("pbBtdTDSZszkgmnJNrRlL_", "") do		for secondary in mw.text.gsplit("jGw", "") do			key = primary..secondary map[key] = map[key] or (map[primary]..map[secondary]) end end for vowel in mw.text.gsplit("q<8>", "") do		map[vowel] = map[vowel] or (map[BACK_VOWEL[vowel]]..LESSROUND) end for vowel in mw.text.gsplit(V_, "") do		key = vowel.."#" map[key] = map[key] or (map[vowel]..map["#"]) key = vowel.."^" map[key] = map[key] or (map[vowel]..map["^"]) end PHONETIC_IPA = map end

needVOWEL = function if VOWEL then return end VOWEL = { -- VOWELS[f1][f2] { "a", "6", "A", "q", "Q" }, { "E", "3", "V", "<", "O" }, { "e", "@", "7", "8", "o" }, { "i", "1", "M", ">", "u" } }	F1 = {} F2       = { ["j"] = F2J, ["G"] = F2G, ["w"] = F2W } F2_SECOND = { [F2J] = "j", [F2G] = "G", [F2W] = "w" } FRONT_VOWEL = {} BACK_VOWEL = {} ROUND_VOWEL = {} IS_VOWEL = FRONT_VOWEL for f1, row in pairs(VOWEL) do		local front = row[F2J] local back = row[F2G] local round = row[F2W] 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 end

needVOWEL_REFLEX = function if VOWEL_REFLEX then return end -- [f1] local aEei = { "a", "E", "e", "i" } local AEei = { "A", "E", "e", "i" } local AV7i = { "A", "V", "7", "i" } local AV7M = { "A", "V", "7", "M" } local AV7u = { "A", "V", "7", "u" } local AOou = { "A", "O", "o", "u" } local QOou = { "Q", "O", "o", "u" } -- [F2[secondaryR]][f1] local _jv_X = { [F2J]=aEei, [F2G]=AEei, [F2W]=QOou } local njv_X = { [F2J]=aEei, [F2G]=AV7i, [F2W]=QOou } local hjvtX = { [F2J]=aEei, [F2G]=aEei, [F2W]=QOou } local hjvkX = { [F2J]=AV7i, [F2G]=AV7i, [F2W]=QOou } local _Gv_X = { [F2J]=AV7i, [F2G]=AV7M, [F2W]=QOou } local rGv_X = { [F2J]=AEei, [F2G]=AV7M, [F2W]=QOou } -- not currently used local hGv_X = { [F2J]=AV7M, [F2G]=AV7M, [F2W]=AV7M } local _wv_X = { [F2J]=AV7u, [F2G]=AOou, [F2W]=QOou } local rwv_X = { [F2J]=AOou, [F2G]=AOou, [F2W]=QOou } local hwv_X = { [F2J]=AV7M, [F2G]=AOou, [F2W]=QOou } local hwvtX = { [F2J]=AV7M, [F2G]=AV7M, [F2W]=QOou } -- [F2[secondaryL]][F2[secondaryR]][f1] local _Xv__ = { [F2J]=_jv_X, [F2G]=_Gv_X, [F2W]=_wv_X } local nXv__ = { [F2J]=njv_X, [F2G]=_Gv_X, [F2W]=hwv_X } local rXv__ = { [F2J]=_jv_X, [F2G]=_Gv_X, [F2W]=rwv_X } local hXv__ = { [F2J]=_jv_X, [F2G]=hGv_X, [F2W]=hwv_X } local hXvt_ = { [F2J]=hjvtX, [F2G]=hGv_X, [F2W]=hwvtX } local hXvk_ = { [F2J]=hjvkX, [F2G]=hGv_X, [F2W]=_wv_X } local hXvr_ = { [F2J]=hjvtX, [F2G]=hGv_X, [F2W]=hwv_X } -- [primaryR][F2[secondaryL]][F2[secondaryR]][f1] local __vX_ = { ["p"] = _Xv__, ["t"] = _Xv__, ["k"] = _Xv__, ["m"] = _Xv__, ["n"] = _Xv__, ["N"] = _Xv__, ["r"] = _Xv__, ["l"] = _Xv__ }	local n_vX_ = { ["p"] = nXv__, ["t"] = nXv__, ["k"] = nXv__, ["m"] = nXv__, ["n"] = nXv__, ["N"] = nXv__, ["r"] = nXv__, ["l"] = nXv__ }	local r_vX_ = { ["p"] = rXv__, ["t"] = rXv__, ["k"] = rXv__, ["m"] = rXv__, ["n"] = rXv__, ["N"] = rXv__, ["r"] = rXv__, ["l"] = _Xv__ }	local h_vX_ = { ["p"] = hXv__, ["t"] = hXvt_, ["k"] = hXvk_, ["m"] = hXv__, ["n"] = hXv__, ["N"] = hXvk_, ["r"] = hXvr_, ["l"] = hXv__ }	-- [primaryL][primaryR][F2[secondaryL]][F2[secondaryR]][f1] VOWEL_REFLEX = { ["p"] = __vX_, ["t"] = __vX_, ["k"] = __vX_, ["m"] = __vX_, ["n"] = n_vX_, ["N"] = n_vX_, ["r"] = r_vX_, ["l"] = n_vX_, ["h"] = h_vX_ } end

parse = function(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, "[aEeIi]_.[aEeIi]") then error("pseudo-glides may only be at the beginning or end"..code) end if string.find(temp, "[aEeIi][aEeIi]") then error("vowels must be separated by a consonant: "..code) end if string.find(temp, ".[jGw].[jGw]$") then error("may not end with a consonant cluster: "..code) end string.gsub(" "..temp, "[ jGw](.[jGw])(.[jGw][ptkmnNrlhyYjGw]*)",				function(consonX, consonY)					if consonX ~= consonY then						error( "may not begin with a consonant cluster ".. "unless it is a geminate: "..code )					end				end			) if text ~= "" then addUnique(outSeq, text) end end end return outSeq end

parseBoolean = function(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

splitChars = function(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

string_gsub2 = function(text, pattern, subst) local result = text result = string.gsub(result, pattern, subst) -- If it didn't change the first time, it won't change the second time. if result ~= text then result = string.gsub(result, pattern, subst) end return result end

string_gsubx = function(text, pattern, subst) repeat local oldText = text text = string.gsub(text, pattern, subst) until oldText == text return text end

tableGet = function(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

toBender = function(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 if not BENDER_MAPS then 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"] = "ȩ"		}) BENDER_MAPS = { ["1968"]   = BENDER_1968, ["med"]    = BENDER_MED, ["mod"]    = BENDER_MOD, ["default"] = BENDER_DEFAULT }	end local map = BENDER_MAPS[ type(version) == "string" and string.lower(version) or "" ] or BENDER_MAPS["default"] local outSeq = {} for _, text in pairs(inSeq) do		text = string.gsub(text, ".[jGw]?", map) addUnique(outSeq, text) end return outSeq end

toMOD = function(text) TO_MOD = TO_MOD or { ["Ȩ"] = "Ẹ", ["ȩ"] = "ẹ", ["Ļ"] = "Ḷ", ["ļ"] = "ḷ", ["M̧"] = "Ṃ", ["m̧"] = "ṃ", ["Ņ"] = "Ṇ", ["ņ"] = "ṇ", ["N̄"] = "Ñ", ["n̄"] = "ñ", ["O̧"] = "Ọ", ["o̧"] = "ọ" }	text = mw.ustring.gsub(text, ".["..CEDILLA..MACRON.."]?", TO_MOD) return text end

toPhonemic = function(inSeq) local outSeq = {} if not PHONETIC_MAP then local 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(map, {				["a"] = "ɐ",				["E"] = "ə",				["e"] = "ɘ",				["i"] = "ɨ",				["I"] = "ɨ"			}) end map["yj"] = map.hj..map.i..ASYLL..map.hj		map["Yj"] = map.hj..map.i..map.hj..map.hj		PHONEMIC_MAP = map end for _, text in pairs(inSeq) do		text = string.gsub(text, ".[jGw]?", PHONEMIC_MAP) addUnique(outSeq, text) end return outSeq end

toPhonetic = function(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 -- If enabled, display full diphthong allophones for short vowels. local diphthongs = not not (args and parseBoolean(args.diphthongs)) -- If enabled, break words at consonant cluster boundaries -- and enunciate the word fragments individually. -- This mode does not assimilate clusters or produce epenthetic vowels. local enunciate = not not (args and parseBoolean(args.enunciate)) -- Argument "J" has format like "cst". -- Recognized letters are "t" = plosive, "c" = affricate, "s" = fricative. -- Letters for initial, medial and final 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"] = "S", ["s"] = "s", ["x"] = "x" }	local modeJ = splitChars(args and args.J and string.lower(args.J) or "tst") 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 -- If enabled, display liaison joiners to mark -- spaces or hyphens in the input code that are not consonant clusters. local liaison = not not (args and parseBoolean(args.liaison)) -- 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, ["diphthongs"] = diphthongs, ["enunciate"] = enunciate, ["initialJ"] = initialJ, ["medialJ"] = medialJ, ["finalJ"] = finalJ, ["liaison"] = liaison, ["noHints"] = noHints, ["voice"] = voice }	for _, text in pairs(inSeq) do		text = string.gsub(text, S, " ") text = fastTrim(text) local isRalik = dialect == "ralik" if isRalik or dialect == "ratak" then text = toPhoneticDialect(text, config, isRalik) toPhoneticRemainder(text, config) else local ralik = toPhoneticDialect(text, config, true) local ratak = toPhoneticDialect(text, 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

toPhoneticDialect = function(text, config, isRalik) -- To streamline morpheme-initial regular expressions. text = "&"..text -- Morphemes can begin with geminated consonants, but spoken words cannot. -- Morphemes with {hhV-}. if isRalik then -- Rālik {hhVCC-} becomes {hVhVCC-}? -- Experimental. Examples may not actually exist. -- But prevents {hhVCC-} becoming {hVhCC-} next. text = string_gsub2(text,			"([&jGw] *hG)( *hG *)([aEeIi])( *.[jGw] *.[jGw])", "%1%3%2%3%4"		) -- Remaining Rālik {hhVC-} becomes {hVhC-}. text = string.gsub(text, "([&jGw] *hG)( *hG *)([aEeIi])", "%1%3%2") else -- Ratak {hhV-} becomes {hV-}. text = string.gsub(text, "([&jGw] *hG *)hG( *[aEeIi])", "%1%2") end -- Morphemes with remaining {CCV-}. if isRalik then -- Rālik {CCa-} becomes {yeCCa-}. text = string.gsub(			text, "([&jGw] *)(.[jGw])( *)%2( *a)", "%1hjE%2%3%2%4"		) -- Rālik remaining {CCV-} becomes {yVCCV-}. text = string.gsub(			text, "([&jGw] *)(.[jGw])( *)%2( *)([EeIi])", "%1hj%5%2%3%2%4%5"		) else -- Ratak {CCa-} becomes {CeCa-}. text = string.gsub(text, "([&jGw] *)(.[jGw])( *)%2( *a)", "%1%2E%3%2%4") -- Ratak remaining {CCV-} becomes {CVCV-}. text = string.gsub(			text, "([&jGw] *)(.[jGw])( *)%2( *)([EeIi])", "%1%2%5%3%2%4%5"		) end -- Initial {yiyV-, yiwV-, wiwV-} sequences have special behavior. -- To block this in the template argument, use "'i" instead of "i". if isRalik then -- Rālik {wiwV-} becomes {yiwV-}. text = string.gsub(text, "([&jGw] *h)w( *i *hw *[aEeIi])", "%1j%2") end -- {[yw]iwV-} becomes {[yw]iwwV-} in both dialects. text = string.gsub(text, "([&jGw] *h[jw] *i *hw)( *[aEeIi])", "%1hw%2") -- {yiyV-} sequences text = string.gsub(text,		"([&jGw] *)hj( *)i( *)hj( *[aEeIi])",		isRalik and "%1Yj%2%3%4" or "%1yj%2%3%4"	) -- No longer need initial "&". text = text.sub(text, 2) -- Don't need to protect {i} anymore. text = string.gsub(text, "I", "i") return text end

toPhoneticRemainder = function(code, config, leftFlag, rightFlag) local text = code local chars, fn	needVOWEL local diphthongs = config.diphthongs -- If the phrase begins or ends with a bare vowel -- and no pseudo-glide, display phrase up to five times -- with each of the different pseudo-glides and possible vowel reflexes. if IS_VOWEL[string.sub(text, 1, 1)] then text = "_j"..code toPhoneticRemainder(text, config, false, rightFlag) if not diphthongs then toPhoneticRemainder(text, config, true, rightFlag) end text = "_G"..code toPhoneticRemainder(text, config, false, rightFlag) if not diphthongs then toPhoneticRemainder(text, config, true, rightFlag) end text = "_w"..code toPhoneticRemainder(text, config, false, rightFlag) if not diphthongs then toPhoneticRemainder(text, config, true, rightFlag) end return end if IS_VOWEL[string.sub(text, -1)] then text = code.."_j" toPhoneticRemainder(text, config, leftFlag, false) if not diphthongs then toPhoneticRemainder(text, config, leftFlag, true) end text = code.."_G" toPhoneticRemainder(text, config, leftFlag, false) if not diphthongs then toPhoneticRemainder(text, config, leftFlag, true) end text = code.."_w" toPhoneticRemainder(text, config, leftFlag, false) if not diphthongs then toPhoneticRemainder(text, config, leftFlag, true) end return end local enunciate = config.enunciate local initialJ = config.initialJ local medialJ  = config.medialJ local finalJ   = config.finalJ local liaison  = config.liaison local noHints  = config.noHints local outSeq   = config.outSeq local voice    = config.voice -- Turn on liaison if we're enunciating. liaison = liaison or enunciate if	initialJ == "x" or		medialJ == "x" or		finalJ == "x" then local subSeq = {} config.outSeq = subSeq if initialJ == "x" then config.initialJ = "t" end if medialJ == "x" then config.medialJ = "t" end if finalJ == "x" then config.finalJ = "t" end toPhoneticRemainder(code, config) if initialJ == "x" then config.initialJ = "s" end if medialJ == "x" then config.medialJ = "s" end if finalJ == "x" then config.finalJ = "s" end toPhoneticRemainder(code, config) addUnique(outSeq, table.concat(subSeq, " ~ ")) config.outSeq = outSeq config.initialJ = initialJ config.medialJ = medialJ config.finalJ = finalJ return end text = "&"..text.."&" if enunciate then -- Create a prosodic break at consonant clusters. text = string.gsub(text, "([jGw]) *(.[jGw])", "%1&&%2") else -- Glides always trigger epenthesis, even neighboring other glides. text = string_gsub2(text, "([aEei])( *h)(.)( *)(h)%3( *)([aEei])",			function(vowelL, _, secondary, __, primaryR, ___, vowelR)				if secondary == "w" then					primaryR = "H"				end				return ( vowelL.._..secondary.. maxF1(vowelL, vowelR).."#".. __..primaryR..secondary..___..vowelR )			end		) text = string.gsub(text, "([aEei])( *)hG( *.[jGw])", "%1%2hG%1#%3") text = string.gsub(text, "(.[jGw])( *)hG( *)([aEei])", "%1%4#%2hG%3%4") text = string.gsub(text, "([aEei])( *)h(.)( *.[jGw])", "%1%2h%3%1#%4") text = string.gsub(text, "(.[jGw])( *)h(. *)([aEei])", "%1%4#%2h%3%4") text = string.gsub(text, "(.[jGw])( *[yY].)", "%1i#%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", "%1v%2t") -- 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") -- Organic speech involves certain consonant cluster assimilations. -- Forward assimilation of rounded consonants. -- There is no rounded coronal obstruent. text = string.gsub(text, "(w *[^t])[jG]", "%1w") -- 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") -- 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, "E").."#"..					consonR..vowelR			end		) 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])",			function( 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		) else -- Vowels neighboring pseudo-glides. fn = function(			primaryL, secondaryL, _, vowel, epenth,			__, primaryR, secondaryR, flag		) local f2L = F2[secondaryL] local f2R = F2[secondaryR] local f2			if flag then f2 = math.max(f2L, f2R) else f2 = math.min(f2L, f2R) end return (				primaryL..secondaryL.._..				VOWEL[F1[vowel]][f2]..epenth..__..				primaryR..secondaryR			) end text = string.gsub(text, "(_)([jGw])( *)/("..V..")(#?)( *)(.)([jGw])",			function(a, b, c, d, e, f, g, h)				return fn(a, b, c, d, e, f, g, h, leftFlag)			end		) text = string.gsub(text, "(.)([jGw])( *)/("..V..")(#?)( *)(_)([jGw])",			function(a, b, c, d, e, f, g, h)				return fn(a, b, c, d, e, f, g, h, rightFlag)			end		) needVOWEL_REFLEX -- Vowels between two non-glides have the most predictable reflexes. text = string_gsub2(text,			"([ptkmnNrl])(.)( *)/([aEei])(#? *)([ptkmnNrl])(.)",			function( primaryL, secondaryL, _, vowel, infix, primaryR, secondaryR )				return primaryL..secondaryL.._..					VOWEL_REFLEX[primaryL][primaryR]						[F2[secondaryL]][F2[secondaryR]][F1[vowel]]..					infix..primaryR..secondaryR			end		) -- Exceptionally for the single word "rej". text = string.gsub(text, "& *(rG *)([V7])( *tj) *&",			function(prefix, vowel, suffix)				return "&"..prefix..FRONT_VOWEL[vowel]..suffix.."&"			end		) -- Vowels always claim the secondary articulation -- of a neighboring back unrounded glide. text = string.gsub(text, "(hG *)/([aEei])", function(prefix, vowel)			return prefix..BACK_VOWEL[vowel]		end) text = string.gsub(text, "/([aEei])(#? *hG)", function(vowel, suffix)			return BACK_VOWEL[vowel]..suffix		end) if not enunciate then -- 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			) 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		) -- Change certain vowels in a special environment from round to front. text = string_gsub2(text, "(hj *)([Oou])( *.w *"..V.." *h[jh])",			function(prefix, vowel, suffix)				return prefix..FRONT_VOWEL[vowel]..suffix			end		) text = string.gsub(text, "(hj *)([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		) if not enunciate then text = string.gsub(text, "(hj *)([Oou])( *.w *.w)",				function(prefix, vowel, suffix)					return prefix..FRONT_VOWEL[vowel]..suffix				end			) end text = string.gsub(text, "(a#? *hj *)Q( *.w *"..V..")", "%1a%2") if not enunciate then text = string.gsub(text, "(a#? *hj *)Q( *.w *.w)", "%1a%2") end -- Tag certain glide-vowel-non-glide sequences for special reflexes. if false and enunciate then text = string.gsub(				text, "([hHyY][jw] *)("..V.." *[ptkmnNrl])", "%1/%2"			) else text = string.gsub(				text, "([HyY][jw] *)("..V.." *[ptkmnNrl])", "%1/%2"			) text = string.gsub(				text, "([&#] *h[jw] *)("..V.." *[ptkmnNrl])", "%1/%2"			) text = string.gsub(				text, "([EeiAV7MOou] *h[jw] *)([aAQ] *[ptkmnNrl])", "%1/%2"			) text = string.gsub(text, "([ei7M])( *hj *)([EeV7])( *[kN]G)",				function(vowelL, infix, vowelR, suffix)					if F1[vowelL] > F1[vowelR] then						return vowelL..infix.."/"..vowelR..suffix					end				end			) text = string.gsub(				text, "(hj *[aEei]#? *hw *)("..V.." *[ptkmnNrl])", "%1/%2"			) end -- Untag certain sequences, exempting them from special reflexes. text = string.gsub(text, "(hj *)/([aEei] *[knNrl]w)", "%1%2") -- Special reflexes. text = string.gsub(text, "([jw])( *)/("..V..")( *)(.)([jGw])",			function(secondaryL, _, vowel, __, primaryR, secondaryR)				return ( secondaryL.._.. VOWEL_REFLEX["h"][primaryR] [F2[secondaryL]][F2[secondaryR]][F1[vowel]].. __..primaryR..secondaryR )			end		) -- Exceptional phrase-initial reflex. text = string.gsub(text, "& *([Hh]j *)([V7])( *[kN]G)",			function(prefix, vowel, suffix)				return "&"..prefix..FRONT_VOWEL[vowel]..suffix			end		) text = string.gsub(text, "& *([Hh]w *)M( *tG)", "&%1u%2") end if not enunciate then -- Temporarily cancel epenthetic {i} neighboring {yi'y}. text = string.gsub(text, "i#( *yj)", "%1") -- {yi'y} neighboring {i} may now be demoted to {y}. text = string.gsub(text, "([iMu]#? *)yj", "%1hj") text = string.gsub(text, "yj( *[iMu])", "hj%1") end -- {'yiy} may now be demoted everywhere. if not enunciate then text = string.gsub(text, "(i# *)Yj", "%1hjihj") text = string.gsub(text, "Yj", "hjihji#hj") else text = string.gsub(text, "Yj", "hjihj&&hj") end -- For the purposes of this template, -- surface all glides pronounced in isolation. text = string.gsub(text, "& *h(.) *&", "&H%1&") if not diphthongs then -- Opportunistically front these vowels. text = string.gsub(text, "([hy]j *)([A7M])( *[kN]?G? *[kN]G *"..V..")",			function(prefix, vowel, suffix)				return prefix..FRONT_VOWEL[vowel]..suffix			end		) -- Surface certain glides. text = string.gsub(text, "& *h(w *[Oou])", "&H%1") text = string.gsub(text, "h(w *[aEeiAV7M])", "H%1") text = string.gsub(text, "& *h(j *[AV7MQOou])", "&H%1") if not enunciate then text = string.gsub(text, "([ptkmnNrl]..# *)h(w *[Oou])", "%1H%2") text = string.gsub(text, "([ptkmnNrl]..# *)h(j *"..V..")", "%1H%2") end text = string.gsub(text, "([AV7MQOou]#? *)h(j *[AV7MQOou])", "%1H%2") text = string.gsub(text, "([aEeiAV7M])(#? *)hw( *)([QOou])",			function(vowelL, infix, _, vowelR)				if F1[vowelL] > F1[vowelR] then					return vowelL..infix.."Hw".._..vowelR				end			end		) text = string.gsub(text, "([AV7MQOou])(#? *)hj( *)([aEei])",			function(vowelL, infix, _, vowelR)				if F1[vowelL] > F1[vowelR] then					return vowelL..infix.."Hj".._..vowelR				end			end		) text = string.gsub(text, "([aEei])(#? *)hj( *)([AV7MQOou])",			function(vowelL, infix, _, vowelR)				if F1[vowelL] < F1[vowelR] then					return vowelL..infix.."Hj".._..vowelR				end			end		) text = string.gsub(text, "("..V..")( *)h([jw]) *&",			function(vowel, _, secondary)				if F2[vowel] ~= F2[secondary] then					return vowel.._.."H"..secondary.."&"				end			end		) if not enunciate then -- Protect word-final epenthetic vowels after non-glides -- from the next operation. text = string.gsub(text, "([ptkmnNrl]."..V..")(# )", "%1/%2") -- De-epenthesize vowels if they still neighbor unsurfaced glides. text = string.gsub(text, "("..V..")#( *h.)", "%1%2") text = string.gsub(text, "(h. *"..V..")#", "%1") -- Adjust F1 of currently remaining epenthetic vowels. text = string_gsub2(text,				"("..V..")( *.[jGw])(.)#( *.[jGw] *)("..V..")",				function(vowelL, infixL, vowel, infixR, vowelR)					return ( vowelL..infixL.. VOWEL[F1[maxF1(vowelL, vowelR, "E")]][F2[vowel]].."/#".. infixR..vowelR )				end			) text = string.gsub(text, "/", "") end end -- Delete all remaining unsurfaced glides. text = string.gsub(text, "h.", "") -- Surface realization for {yi'y}. text = string.gsub(text, "yj", "i^") if not diphthongs then -- Realization for surfaced {y}. text = string_gsub2(text, "("..V.."?)(#?)( *)Hj( *)("..V.."?)",			function(vowelL, epenthL, _, __, vowelR)				if vowelL ~= "" then					if vowelR ~= "" then						if	vowelL == vowelR and							F2[vowelL] == F2J						then							return vowelL.._..__..vowelR						else							return ( vowelL..epenthL.._.. maxF1(vowelL, vowelR, "E").."^"..__..vowelR )						end					else						return vowelL.._..epenthL..maxF1(vowelL, "E").."^"..__					end				else					if vowelR ~= "" then						return _..maxF1(vowelR, "E").."^"..__..vowelR					else						return _.."i^"..__					end				end			end		) if not enunciate then -- Flatten this epenthetic vowel and surfaced glide. text = string_gsub2(				text, "([aAQ] *"..C..")E#( *)E%^( *)a", "%1a%2%3a"			) -- Collapse this epenthetic vowel and -- surfaced glide into a semi-vowel. text = string.gsub(text, "([aEei])#( *)%1%^", "%2%1^") end end if MERGED_MID_VOWELS then text = string.gsub(text, "E", "e") text = string.gsub(text, "O", "o") end chars = splitChars(text, ".") if not diphthongs and not enunciate 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 -- Just in case we've accumulated some whitespace at the edges. text = string.gsub(text, " *(&+) *", "%1") needCONSON_REFLEX -- Tweak remaining consonants, using offsets as a guide. text = string.gsub(text, "(.)([jGw])( *)([ptkmnNrl]?)([jGw]?)",		function( offsetL, primaryL, secondaryL, _, primaryR, secondaryR, offsetR )			local isInitial = chars[offsetL - 1] == "&"			local isFinal = chars[offsetR] == "&"			if	primaryL == "H" or				primaryL == "y"			then				return primaryL..secondaryL.._			end			if primaryL == "_" then				if noHints then					-- Delete pseudo-glide.					return _				end				if isInitial then					-- Show secondary articulation to the left, not the right.					return secondaryL..primaryL.._				end				return primaryL..secondaryL.._			end			local geminated = primaryL == primaryR			if primaryL ~= "t" and primaryR == "t" then				-- /tʲ/        is  palatalized postalveolar.				-- /tˠ/         is  velarized   dental.				-- /nʲ, rʲ, lʲ/ are palatalized dental.				-- /nˠ, rˠ, lˠ/ are velarized   postalveolar.				-- Regressively assimilate primary dental or postalveolar.				-- None of this will be visible unless PHONETIC_DETAILS == true.				primaryL = CONSON_REFLEX[primaryL]					[secondaryL == "j" and "G" or "j"] primaryR = CONSON_REFLEX[primaryR][secondaryR] else primaryL = CONSON_REFLEX[primaryL][secondaryL] if primaryR ~= "" then primaryR = CONSON_REFLEX[primaryR][secondaryR] end end if primaryR == "T" then if primaryL == "T" then primaryL = finalJ primaryR = initialJ if	primaryL == "S" and primaryR ~= "s" then primaryL = "T" elseif primaryL == "T" and primaryR == "s" and medialJ == "S" then primaryL = "S" end else primaryR = medialJ end elseif primaryL == "T" then if isInitial then primaryL = initialJ elseif isFinal then primaryL = finalJ else primaryL = medialJ end end if primaryR ~= "" then -- Consonant cluster. -- For some reason, the {t} in {lt} and {ļt} is voiceless. if	not geminated and primaryL ~= "l" and primaryL ~= "L" then primaryL = VOICED_PRIMARY[primaryL] or primaryL primaryR = VOICED_PRIMARY[primaryR] or primaryR end -- Display secondary articulation only once for the cluster. secondaryL = "" 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 )	if not diphthongs then		if not enunciate then			-- Elegantly connect long and epenthetic vowels across word gaps.			text = string.gsub(text, "(["..V_..":]): +", "%1 : ")			text = string.gsub(text, "("..V..") +%1([^%^])", "%1 :%2")			text = string.gsub(text, "("..V..") +%1 *&", "%1 :&")			text = string.gsub(text, "("..V..")# +%1", " %1 :")			text = string.gsub(text, "("..V.."#) +", " %1 ")		end		if W_OFF_GLIDES then			-- Add [w] off-glides after certain consonants.			text = string.gsub(text, "([pbm]G *)([aEei])", "%1BG%2")			text = string.gsub(text, "([kgnNrl]w *)([aEeiAV7M])", "%1Hw%2")			-- Remove [w] off-glides after certain consonants			-- when they occur after rounded vowels.			text = string.gsub(text, "([QOou] *[nrl]? *[nrl]w *)Hw", "%1")			text = string.gsub(text, "([QOou] *[kgN]? *Nw *)HwM", "%1M")		end	end	if not enunciation and PARENTHETICAL_EPENTHESIS then		text = string.gsub(text, "(.)#", "(%1)") if W_OFF_GLIDES then text = string.gsub(text, "([HB].)%(("..V..")", "(%1%2") text = string.gsub(text, "([HB][Gw])( *[Eei]%^)", "(%1)%2") end text = string.gsub(text, "%)(=?)%(", "%1") end -- Clean up prosodic unit margins. text = string.gsub(text, " *&[ &]*", "&") if false and enunciate then text = string.gsub(text, "("..V..")", "%1.") text = string.gsub(text, "("..V..")%.%^", "%1^") text = string.gsub(text, "%.(.[jGw]&)", "%1") text = string.gsub(text, "%.("..V.."%^&)", "%1") text = string.gsub(text, "%.&", "&") end text = string.gsub(text, "^&* *", "") text = string.gsub(text, " *&*$", "") -- Convert remaining word gaps to liaison. text = string.gsub(text, " +", liaison and "_" or "") needPHONETIC_IPA text = string.gsub(text, ".[jGw#%^]?", PHONETIC_IPA) addUnique(outSeq, text) end

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

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

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

function export.MOD(frame) return toMOD(frame.args[1]) 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

return export