Content deleted Content added
No edit summary |
[w] off-glide tweak |
||
(11 intermediate revisions by 2 users not shown) | |||
Line 1:
-- 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_VOWELS = false
local PARENTHETICAL_EPENTHESIS = true
local PHONETIC_DETAILS = false
local W_OFF_GLIDES = true
local ASYLL = "̯"
Line 34 ⟶ 20:
local C1_ = "pbtdSZszkgmnNrlyYhH_"
local C1 = "["..C1_.."]"
local
local C = ".["..C2_.."]"
local V_ = "aEeiAV7MQOou"
local V = "["..V_.."]"
Line 45 ⟶ 32:
local EMPTY = {}
-- Adds elements to a sequence as if it's a set (retains unique elements only).
local function addUnique(seq, value)
for _, value2 in pairs(seq) do
Line 54 ⟶ 42:
end
-- Intended to work the same as JavaScript's Object.assign() function.
local function assign(target, ...)
local args = { ... }
Line 67 ⟶ 56:
local function fastTrim(text)
return string.match(text, "^%s*(.-)%s*$")
end
local function parseBoolean(text)
if type(text) == "string" then
text =
if text ~= "" and text ~= "0" and
return true
end
Line 99 ⟶ 72:
chars = chars or {}
local index = 1
for ch in
chars[index] = ch
index = index + 1
Line 105 ⟶ 78:
if index <= #chars then
if shorten then
else
repeat
Line 117 ⟶ 90:
local function string_gsub2(text, pattern, subst)
return
end
Line 143 ⟶ 116:
local function ZTBL(text, sep)
local tbl = {}
for key in
tbl[key] = true
end
Line 151 ⟶ 124:
local PARSE_PSEUDO_GLIDE = {
["y"] = "0",
["h"] = "0h",
["w"] = "0w"
}
local PARSE_C_CH_CW = {
["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"
}
local PARSE_REMAINING = {
["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",
["'"] = ""
}
local function parse(code)
local outSeq = {}
code =
code =
for text in
text = fastTrim(text)
if text ~= "" then
local temp =
if temp ~= "" then
error("'"..code.."' contains unsupported characters: "..temp)
Line 171 ⟶ 188:
-- Recognize "y_", "h_", "w_", "_y", "_h", "_w" as pseudo-glides.
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 =
-- "yi'y" and "'yiy" sequences
text =
if aposA ~= "" then
-- "dwelling upon" i
Line 195 ⟶ 209:
-- Convert multigraphs to pseudo-X-SAMPA format.
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.
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 =
if
then
error("pseudo-glides may not neighbor a consonant")
end
if
error("pseudo-glides may only be at the beginning or end"..code)
end
if
error("vowels must be separated by a consonant: "..code)
end
if
error("each consonant cluster is limited to two: "..code)
end
if
error("may not end with a consonant cluster: "..code)
end
if consonX ~= consonY then
error("may only begin with single or geminated consonant: "
Line 298 ⟶ 272:
["hj"] = "y", ["hG"] = "h", ["hw"] = "w",
["_j"] = "", ["_G"] = "", ["_w"] = "",
["a"] = "a",
["E"] = "e",
["e"] = "&",
["i"] = "i",
["I"] = "i"
}
local BENDER_MED = assign({}, BENDER_1968, {
Line 312 ⟶ 286:
["lG"] = "ļ",
["lw"] = "ļ°",
["e"] = "ȩ"
})
local BENDER_MOD = assign({}, BENDER_MED, {
Line 323 ⟶ 297:
["lG"] = "ḷ",
["lw"] = "ḷʷ",
["e"] = "ẹ"
})
local BENDER_DEFAULT = assign({}, BENDER_MOD, {
Line 331 ⟶ 305:
["lG"] = "ļ",
["lw"] = "ļʷ",
["e"] = "ȩ"
})
local BENDER_MAPS = {
["1968"] = BENDER_1968,
["med"] = BENDER_MED,
["mod"] = BENDER_MOD
}
Line 346 ⟶ 320:
local version = args and args.version
local map = BENDER_MAPS[
type(version) == "string" and
] or BENDER_DEFAULT
local outSeq = {}
for _, text in pairs(inSeq) do
text =
addUnique(outSeq, text)
end
Line 368 ⟶ 342:
local function toMOD(text)
text =
return text
end
Line 385 ⟶ 359:
["hj"] = "j", ["hG"] = "ɰ", ["hw"] = "w",
["_j"] = "", ["_G"] = "", ["_w"] = "",
["a"] = "æ",
["E"] = "ɛ",
["e"] = "e",
["i"] = "i",
["I"] = "i"
}
if false then
Line 408 ⟶ 382:
local outSeq = {}
for _, text in pairs(inSeq) do
text =
addUnique(outSeq, text)
end
Line 427 ⟶ 401:
local F2_BACK = 2
local F2_ROUND = 3
local F2 = {
["j"] = F2_FRONT, ["G"] = F2_BACK, ["w"] = F2_ROUND } local FRONT_VOWEL = {}
Line 446 ⟶ 424:
end
local function maxF1(a, b, c)
if
return VOWEL[
elseif b then
return VOWEL[math.max(F1[a], F1[b])][F2_FRONT]
else
return
end
end
Line 457 ⟶ 437:
-- Morphemes can begin with geminated consonants, but spoken words cannot.
text =
function(conson, _, __, vowel)
if conson == "hG" then
Line 467 ⟶ 447:
else
if isRalik then
return "hj"..maxF1(vowel, "E")..conson.._..conson..__..vowel
else
return conson..maxF1(vowel, "E").._..conson..__..vowel
end
end
Line 478 ⟶ 458:
-- To block this in the template argument, use "'i" instead of "i".
text = " "..text
text =
"([ jGw])( *)(h[jw])( *)i( *)(h[jw])( *)("..VI..")",
function(nonVowel, _, consonX, __, ___, consonY, ____, vowel)
Line 502 ⟶ 482:
end
)
text =
-- Restore protected {i}, we won't be checking for it anymore.
text =
return text
Line 529 ⟶ 509:
local njv_X = { aEei, AV7i, QOou }
local hjvtX = { aEei, aEei, QOou }
local hjvkX = { AV7i, AV7i, QOou }
local _Gv_X = { AV7i, AV7M, QOou }
local rGv_X = { AEei, AV7M, QOou } -- not currently used
Line 535 ⟶ 516:
local rwv_X = { AOou, AOou, QOou }
local hwv_X = { AV7M, AOou, QOou }
local hwvtX = { AV7M,
-- [F2[secondaryL]][F2[secondaryR]][f1]
local _Xv__ = { _jv_X, _Gv_X, _wv_X }
Line 542 ⟶ 523:
local hXv__ = { _jv_X, hGv_X, hwv_X }
local hXvt_ = { hjvtX, hGv_X, hwvtX }
local hXvk_ = { hjvkX, hGv_X, _wv_X }
local hXvr_ = { hjvtX, hGv_X, hwv_X }
-- [primaryR][F2[secondaryL]][F2[secondaryR]][f1]
Line 560 ⟶ 542:
}
local h_vX_ = {
["p"] = hXv__, ["t"] = hXvt_, ["k"] =
["m"] = hXv__, ["n"] = hXv__, ["N"] =
["r"] = hXvr_, ["l"] = hXv__
}
Line 567 ⟶ 549:
VOWEL_REFLEX = {
["p"] = __vX_, ["t"] = __vX_, ["k"] = __vX_,
["m"] = __vX_, ["n"] = n_vX_, ["N"] =
["r"] = r_vX_, ["l"] = n_vX_, ["h"] = h_vX_
}
Line 580 ⟶ 562:
["l"] = { ["j"] = "L" }
}
for primary in
local map2 = map[primary]
if not map2 then
Line 619 ⟶ 601:
["r"] = "r",
["l"] = "l",
["Hj"] = "j",
["HG"] = "ʔ",
["Hw"] = "w",
Line 683 ⟶ 666:
map["Hj"] = map["Hj"] or map["i"]..map["^"]
local key
for primary in
for secondary in
key = primary..secondary
map[key] = map[key] or (map[primary]..map[secondary])
end
end
for vowel in
key = vowel.."@"
map[key] = map[key] or (map[vowel]..map["@"])
Line 708 ⟶ 691:
-- and no pseudo-glide, display phrase up to five times
-- with each of the different pseudo-glides and possible vowel reflexes.
if IS_VOWEL[
text = "_j"..code
toPhoneticRemainder(text, config, false, rightFlag)
Line 726 ⟶ 709:
return
end
if IS_VOWEL[
text = code.."_j"
toPhoneticRemainder(text, config, leftFlag, false)
Line 778 ⟶ 761:
end
toPhoneticRemainder(code, config)
addUnique(outSeq,
config.outSeq = outSeq
config.initialJ = initialJ
Line 787 ⟶ 770:
-- 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 =
-- Unstable consonant clusters trigger epenthesis.
-- Liquids before coronal obstruents.
text =
-- Nasals and liquids after coronal obstruents.
text =
-- Heterorganic clusters.
-- Labial consonants neighboring coronal or dorsal consonants.
text =
-- Coronal consonants neighboring labial or dorsal consonants.
text =
-- Dorsal consonants neighboring labial or coronal consonants.
text =
-- Organic speech involves certain consonant cluster assimilations.
Line 823 ⟶ 814:
-- Forward assimilation of rounded consonants.
-- There is no rounded coronal obstruent.
text =
-- Backward assimilation of remaining secondary articulations.
text =
-- Backward nasal assimilation of primary articulations.
text =
-- No longer need to protect exceptionally stable consonant clusters.
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
)
-- Tag all vowels for next set of operations.
text =
-- There is no variation in the surface realizations of vowels
Line 848 ⟶ 841:
text = string_gsub2(text, "([jGw])( *)/([aEei])(@? *.)%1",
function(secondary, _, vowel, infix)
return (
secondary.._..VOWEL[F1[vowel]][F2[secondary]].. infix..secondary
)
end
)
Line 855 ⟶ 850:
if diphthongs then
text = string_gsub2(text, "(.)([jGw])( *)/([aEei])(@?)( *)(.)([jGw])",
function(
primaryL, secondaryL, _, vowel, epenth, __, primaryR, secondaryR
Line 881 ⟶ 875:
local f2
if flag then
f2 =
else
f2 =
end
return (
Line 891 ⟶ 885:
)
end
text =
function(a, b, c, d, e, f, g, h)
return subst(a, b, c, d, e, f, g, h, leftFlag)
end
)
text =
function(a, b, c, d, e, f, g, h)
return subst(a, b, c, d, e, f, g, h, rightFlag)
Line 916 ⟶ 910:
-- Exceptionally for the single word "rej".
text =
function(prefix, vowel, suffix)
return prefix..FRONT_VOWEL[vowel]..suffix
Line 924 ⟶ 918:
-- Vowels always claim the secondary articulation
-- of a neighboring back unrounded glide.
text =
return prefix..BACK_VOWEL[vowel]
end)
text =
return BACK_VOWEL[vowel]..suffix
end)
Line 933 ⟶ 927:
-- Unless already claimed, epenthetic vowels after a glide
-- always claim the secondary articulation to the left.
text =
function(primaryL, secondaryL, _, vowel)
return (
Line 944 ⟶ 938:
-- Unless already claimed, vowels before a glide
-- always claim the secondary articulation to the right.
text =
function(vowel, epenth, primaryR, secondaryR)
return (
Line 955 ⟶ 949:
-- For now, unless already claimed, vowels before a rounded consonant
-- claim the secondary articulation to the right.
text =
return ROUND_VOWEL[vowel]..suffix
end)
Line 961 ⟶ 955:
-- For now, unless already claimed, remaining vowels
-- claim the secondary articulation to the left.
text =
function(secondaryL, _, vowel)
return secondaryL.._..VOWEL[F1[vowel]][F2[secondaryL]]
end
)
-- Change certain vowels in a special environment from round to front.
text =
function(prefix,
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
)
text =
function(prefix, vowel, suffix)
return prefix..FRONT_VOWEL[vowel]..suffix
end
)
text =
text =
-- Tag certain glide-vowel-non-glide sequences for special reflexes.
text =
text =
text =
text =
"([EeiAV7MOou]
text =
text = string.gsub(text,
"(hj *[aEei]@? *hw *)("..V.." *[ptkmnNrl])", "%1/%2")
-- Untag certain sequences, exempting them from special reflexes.
text =
-- Special reflexes.
text =
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
-- Temporarily cancel epenthetic {i} neighboring {yi'y}.
text =
-- {yi'y} neighboring {i} may now be demoted to {y}.
text =
text =
-- {'yiy} may now be demoted everywhere.
text =
text =
-- For the purposes of this template,
-- surface all glides pronounced in isolation.
text =
if not diphthongs then
-- Opportunistically front
text =
function(prefix, vowel, suffix)
return prefix..FRONT_VOWEL[vowel]..suffix
end
)
-- Surface certain glides.
text =
text =
text =
text =
text =
text =
function(vowelL, infix, _, vowelR)
if F1[vowelL] > F1[vowelR] then
Line 1,048 ⟶ 1,053:
end
)
text =
function(vowelL, infix, _, vowelR)
if F1[vowelL] > F1[vowelR] then
Line 1,055 ⟶ 1,060:
end
)
text =
function(vowelL, infix, _, vowelR)
if F1[vowelL] < F1[vowelR] then
Line 1,062 ⟶ 1,067:
end
)
text =
function(vowel, _, secondary)
if F2[vowel] ~= F2[secondary] then
Line 1,071 ⟶ 1,076:
-- Protect word-final epenthetic vowels after non-glides
-- from the next operation.
text =
-- De-epenthesize vowels if they still neighbor unsurfaced glides.
text =
text =
-- Adjust F1 of currently remaining epenthetic vowels.
Line 1,082 ⟶ 1,087:
"("..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 =
end
-- Delete all remaining unsurfaced glides.
text =
-- Surface realization for {yi'y}.
text =
if not diphthongs then
Line 1,105 ⟶ 1,112:
if vowelL ~= "" then
if vowelR ~= "" then
F2[vowelL
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^"..__
Line 1,122 ⟶ 1,135:
)
--
text =
-- Collapse this epenthetic vowel and surfaced glide into a semi-vowel.
text = string.gsub(text, "([aEei])@( *)%1%^", "%2%1^")
end
if MERGED_VOWELS then
text =
return VOWEL[F1[vowel] + 1][F2[vowel]]
end)
Line 1,152 ⟶ 1,168:
index = index2
until index == 1
text =
end
-- Tweak remaining consonants, using offsets as a guide.
text =
function(
offsetL, primaryL, secondaryL, _, primaryR, secondaryR, offsetR
Line 1,255 ⟶ 1,271:
-- Elegantly connect long and epenthetic vowels across word gaps.
text =
text =
text =
text =
text =
if W_OFF_GLIDES then
-- Add [
subst = function(primary, _, epenth)
if epenth == "" then
Line 1,269 ⟶ 1,285:
end
if false and PHONETIC_DETAILS then
text =
function(primary, _, epenth)
if epenth == "" then
Line 1,277 ⟶ 1,293:
)
else
text =
end
text =
-- Remove [w] off-glides after certain consonants
-- when they occur after rounded vowels.
text = string.gsub(text, "([QOou] *[nrl]? *[nrl])Hw", "%1w")
text =
end
end
if
if not diphthongs then
text =
end
text = string.gsub(text, "(.)@", "(%1)")
text = string.gsub(text, "%)(=?)%(", "%1")
if not diphthongs and W_OFF_GLIDES then
if false and PHONETIC_DETAILS then
text = string.gsub(text, "([pbm]G%()([aEei])", "%1BG%2")
else
text = string.gsub(text, "([pbm]G%()([aEei])", "%1Hw%2")
end
text = string.gsub(text, "([kgnNrl]w%()([aEeiAV7M])", "%1Hw%2")
text = string.gsub(text, "([QOou] *[nrl]w%()Hw", "%1")
text = string.gsub(text, "([QOou] *Nw%()HwM", "%1M")
end
end
-- Convert remaining word gaps to liaison.
text =
text =
text =
addUnique(outSeq, text)
Line 1,314 ⟶ 1,342:
-- For other values, list both possible dialect reflexes where applicable.
local dialect = args and args.dialect and
if dialect == "rālik" then
dialect = "ralik"
Line 1,327 ⟶ 1,355:
-- Real-world pronunciation said to vary by sociological factors,
-- but all realizations may occur in free variation.
local modeJ = splitChars(args and args.J and
local initialJ = PHONETIC_ARG_J[modeJ[1] or ""] or "t"
local medialJ = PHONETIC_ARG_J[modeJ[2] or ""] or "s"
Line 1,356 ⟶ 1,384:
for _, str in pairs(inSeq) do
str =
str =
str =
local isRalik = dialect == "ralik"
if isRalik or dialect == "ratak" then
Line 1,387 ⟶ 1,415:
function export.bender(frame)
return
end
Line 1,395 ⟶ 1,423:
function export.parse(frame)
return
end
function export.phonemic(frame)
return
end
function export.phonetic(frame)
return
end
|