-- To do: add stress in words with >2 syllables (primary and secondary)
--- Reference: 'Research report on phonetic and phonological analysis of Khmer'
--- http://www.panl10n.net/english/Outputs%20Phase%202/CCs/Cambodia/ITC/Papers/2007/0701/phonetic-and-phonological-analysis.pdf
--- Algorithm is simple, though may be inaccurate when automatically applied to multisyllabic words, as some can be 'romanised' in dictionaries as if they are one word but have stress patterns indicating otherwise
--- e.g. ]
local export = {}
local gsub = mw.ustring.gsub
local find = mw.ustring.find
local match = mw.ustring.match
local len = mw.ustring.len
local pagename = mw.title.getCurrentTitle().text
local j = "្"
local c = "កខគឃងចឆជឈញដឋឌឍណតថទធនបផពភមយរលវឝឞសហឡអ"
local cMod = "៉៊"
local vIndep = "ឣឤឥឦឧឨឩឪឫឬឭឮឯឰឱឲឳ"
local vDiac = "ាិីឹឺុូួើឿៀេែៃោៅំះៈ័"
local vPost = "់"
local apos = "'"
local kmChar = "ក-៹'"
local kmString = "+"
local recessive = ""
local cCapt, cUncapt = "(?)", "?"
local cOptCapt = "(??)"
local cCaptClus = {
"(?)",
"(?" .. j .. cUncapt .. ")",
"(?" .. j .. cUncapt .. j .. cUncapt .. ")",
"(?" .. j .. cUncapt .. j .. cUncapt .. j .. cUncapt .. ")"
}
local vCapt, vCaptB, vCaptM = "()", "()", "(*)"
local cvCapt = "()"
local vPostCapt = "(?)"
local postInit = vCaptM .. cOptCapt .. vPostCapt .. "(" .. apos .. "?)"
local consonants = {
= { class = 1, = { "k", "k" }, = { "k", "k" } },
= { class = 1, = { "kʰ", "k" }, = { "kh", "k" } },
= { class = 2, = { "k", "k" }, = { "k", "k" } },
= { class = 2, = { "kʰ", "k" }, = { "kh", "k" } },
= { class = 2, = { "ŋ", "ŋ" }, = { "ng", "ng" } },
= { class = 1, = { "ŋ", "ŋ" }, = { "ng", "ng" } },
= { class = 1, = { "c", "c" }, = { "c", "c" } },
= { class = 1, = { "cʰ", "c" }, = { "ch", "c" } },
= { class = 2, = { "c", "c" }, = { "c", "c" } },
= { class = 2, = { "cʰ", "c" }, = { "ch", "c" } },
= { class = 2, = { "ɲ", "ɲ" }, = { "ñ", "ñ" } },
= { class = 1, = { "ɲ", "ɲ" }, = { "ñ", "ñ" } },
= { class = 1, = { "ɗ", "t" }, = { "d", "t" } },
= { class = 1, = { "tʰ", "t" }, = { "th", "t" } },
= { class = 2, = { "ɗ", "t" }, = { "d", "t" } },
= { class = 2, = { "tʰ", "t" }, = { "th", "t" } },
= { class = 1, = { "n", "n" }, = { "n", "n" } },
= { class = 1, = { "t", "t" }, = { "t", "t" } },
= { class = 1, = { "tʰ", "t" }, = { "th", "t" } },
= { class = 2, = { "t", "t" }, = { "t", "t" } },
= { class = 2, = { "tʰ", "t" }, = { "th", "t" } },
= { class = 2, = { "n", "n" }, = { "n", "n" } },
= { class = 1, = { "n", "n" }, = { "n", "n" } },
= { class = 1, = { "ɓ", "p" }, = { "b", "p" } },
= { class = 1, = { "p", "p" }, = { "p", "p" } },
= { class = 2, = { "ɓ", "p" }, = { "b", "p" } },
= { class = 1, = { "pʰ", "p" }, = { "ph", "p" } },
= { class = 2, = { "p", "p" }, = { "p", "p" } },
= { class = 2, = { "pʰ", "p" }, = { "ph", "p" } },
= { class = 2, = { "m", "m" }, = { "m", "m" } },
= { class = 1, = { "m", "m" }, = { "m", "m" } },
= { class = 2, = { "j", "j" }, = { "y", "y" } },
= { class = 1, = { "j", "j" }, = { "y", "y" } },
= { class = 2, = { "r", "" }, = { "r", "" } },
= { class = 1, = { "r", "" }, = { "r", "" } },
= { class = 2, = { "l", "l" }, = { "l", "l" } },
= { class = 1, = { "l", "l" }, = { "l", "l" } },
= { class = 2, = { "ʋ", "w" }, = { "v", "w" } },
= { class = 1, = { "ʋ", "w" }, = { "v", "w" } },
= { class = 1, = { "s", "h" }, = { "s", "h" } },
= { class = 1, = { "s", "h" }, = { "s", "h" } },
= { class = 1, = { "s", "h" }, = { "s", "h" } },
= { class = 2, = { "s", "h" }, = { "s", "h" } },
= { class = 1, = { "h", "h" }, = { "h", "h" } },
= { class = 2, = { "h", "h" }, = { "h", "h" } },
= { class = 1, = { "l", "l" }, = { "l", "l" } },
= { class = 1, = { "ʔ", "" }, = { "ʾ", "ʾ" } },
= { class = 2, = { "ʔ", "" }, = { "ʾ", "ʾ" } },
= { class = 1, = { "ɡ", "k" }, = { "g", "k" } },
= { class = 2, = { "ɡ", "k" }, = { "g", "k" } },
= { class = 2, = { "ɡ", "k" }, = { "g", "k" } },
= { class = 1, = { "n", "" }, = { "n", "n" } },
= { class = 1, = { "m", "" }, = { "m", "m" } },
= { class = 1, = { "l", "" }, = { "l", "l" } },
= { class = 1, = { "f", "f" }, = { "f", "f" } },
= { class = 2, = { "f", "f" }, = { "f", "f" } },
= { class = 2, = { "f", "f" }, = { "f", "f" } },
= { class = 1, = { "z", "z" }, = { "z", "z" } },
= { class = 2, = { "z", "z" }, = { "z", "z" } },
= { class = 2, = { "z", "z" }, = { "z", "z" } },
= { class = 1, = { "", "" }, = { "", "" } },
}
local vowels = {
= { = { "ɑː", "ɔː" }, = { "ɑɑ", "ɔɔ" } },
= { = { "ɑ", "ŭə" }, = { "ɑ", "ŭə" } },
= { = { "ɑ", "u" }, = { "ɑ", "u" } }, --before labial finals
= { = { "a", "ŏə" }, = { "a", "ŏə" } },
= { = { "a", "ĕə" }, = { "a", "ĕə" } }, --before velar finals
= { = { "aj", "ɨj" }, = { "ay", "ɨy" } },
= { = { "", "ɔə" }, = { "", "ɔə" } },
= { = { "aː", "iə" }, = { "aa", "iə" } },
= { = { "a", "ŏə" }, = { "a", "ŏə" } },
= { = { "a", "ĕə" }, = { "a", "ĕə" } }, --before velar finals
= { = { "eʔ", "iʔ" }, = { "eʾ", "iʾ" } }, --glottal coda only in stressed syllables
= { = { "ə", "ɨ" }, = { "ə", "ɨ" } }, --with non-glottal coda
= { = { "əj", "iː" }, = { "əy", "ii" } },
= { = { "eh", "ih" }, = { "eh", "ih" } }, -- inferred
= { = { "əj", "iː" }, = { "əy", "ii" } },
= { = { "ə", "ɨ" }, = { "ə", "ɨ" } },
= { = { "əh", "ɨh" }, = { "əh", "ɨh" } },
= { = { "əɨ", "ɨː" }, = { "əɨ", "ɨɨ" } },
= { = { "oʔ", "uʔ" }, = { "oʾ", "uʾ" } }, --glottal coda only in stressed syllables
= { = { "o", "u" }, = { "o", "u" }}, --with non-glottal coda
= { = { "oh", "uh" }, = { "oh", "uh" } },
= { = { "ou", "uː" }, = { "ou", "uu" } },
= { = { "əw", "ɨw" }, = { "əw", "ɨw" } },
= { = { "uə", "uə" }, = { "uə", "uə" } },
= { = { "aə", "əː" }, = { "aə", "əə" } },
= { = { "əh", "" }, = { "əh", "" } },
= { = { "ɨə", "ɨə" }, = { "ɨə", "ɨə" } },
= { = { "iə", "iə" }, = { "iə", "iə" } },
= { = { "eː", "ei" }, = { "ee", "ei" } },
= { = { "ə", "ɨ" }, = { "ə", "ɨ" } }, --before palatals
= { = { "eh", "ih" }, = { "eh", "ih" } },
= { = { "ae", "ɛː" }, = { "ae", "ɛɛ" } },
= { = { "eh", "" }, = { "eh", "" } },
= { = { "aj", "ɨj" }, = { "ay", "ɨy" } },
= { = { "ao", "oː" }, = { "ao", "oo" } },
= { = { "ɑh", "ŭəh" }, = { "ɑh", "ŭəh" } },
= { = { "aw", "ɨw" }, = { "aw", "ɨw" } },
= { = { "om", "um" }, = { "om", "um" } },
= { = { "ɑm", "um" }, = { "ɑm", "um" } },
= { = { "am", "ŏəm" }, = { "am", "ŏəm" } },
= { = { "aŋ", "ĕəŋ" }, = { "ang", "ĕəng" } },
= { = { "ah", "ĕəh" }, = { "ah", "ĕəh" } },
= { = { "aʔ", "ĕəʔ" }, = { "aʾ", "ĕəʾ" } },
= { = { "ə", "ə" }, = { "ə", "ə" } },
}
local tl = {
= "k", = "kʰ", = "g", = "gʰ", = "ṅ",
= "c", = "cʰ", = "j", = "jʰ", = "ñ",
= "ṭ", = "ṭʰ", = "ḍ", = "ḍʰ", = "ṇ",
= "t", = "tʰ", = "d", = "dʰ", = "n",
= "p", = "pʰ", = "b", = "bʰ", = "m",
= "y", = "r", = "l", = "v",
= "ś", = "ṣ", = "s",
= "h", = "ḷ", = "ʾ",
= "a", = "ā", = "i", = "ī",
= "u", = "🤷", = "ū", = "ýu",
= "ṛ", = "ṝ", = "ḷ", = "ḹ",
= "ae", = "ai", = "o", = "o", = "au",
= "ā", = "i", = "ī", = "ẏ", = "ȳ",
= "u", = "ū", = "ua",
= "oe", = "ẏa", = "ia",
= "e", = "ae", = "ai", = "o", = "au",
= "ṃ", = "ḥ", = "`",
= "″", = "′", = "´", = "ŕ", = "̊",
= "⸗", = "ʿ", = "˘", = "̑", = "̥",
= "🤷", = "ǂ", = "ǁ", = "🤷", = "«",
= "🤷", = "§", = "»", = "",
= "🤷", = "🤷",
= "0", = "1", = "2", = "3", = "4",
= "5", = "6", = "7", = "8", = "9",
= "🤷", = "🤷", = "🤷", = "🤷", = "🤷",
= "🤷", = "🤷", = "🤷", = "🤷", = "🤷",
}
local glottify = {
= 1, = 1, = 1, = 1, = 1, = 1,
= 1, = 1, = 1, = 1, = 1
}
local err = {
= 1, = 1,
}
local ambig = {
= "kh", = "ch", = "th", = "ph",
= "ng",
}
function export.syllabify(text)
text = gsub(text, "()()", "%1-%2")
local seq1 = cvCapt .. cCapt .. vCaptB
while find(text, seq1) do text = gsub(text, seq1, "%1-%2%3") end
return text
end
function export.syl_analysis(syllable)
for ind = 4, 1, -1 do
if match(syllable, "^" .. cCaptClus .. postInit .. "$") then
return match(syllable, "^" .. cCaptClus .. postInit .. "$")
end
end
return nil
end
local function sylRedist(text, block)
for word in mw.ustring.gmatch(text, "+") do
local originalWord = word
local allSyl, syls, newWord = {}, mw.text.split(word, "%-"), {}
for sylId = 1, #syls do
if syls == "" then table.insert(allSyl, {})
else
local set = export.syl_analysis(syls)
if not set or set == "" then return nil end
table.insert(allSyl, { export.syl_analysis(syls) })
if sylId ~= 1 and allSyl == "" and find(allSyl, j) and not block then
allSyl, allSyl =
match(allSyl, "^(+)"),
match(allSyl, "^+" .. j .. "(.+)")
end
if #syls == 2 and sylId == 2 and allSyl .. allSyl == "" then
allSyl = vPost
end
end
end
for sylId = 1, #syls do
table.insert(newWord, table.concat(allSyl))
end
text = gsub(text, (gsub(originalWord, "%-", "%-")), table.concat(newWord, "%-"), 1)
end
return text
end
local function getCons(c1Set)
c1l, i, consSet = #c1Set, 1, {}
while i < c1l + 1 do
for j = 3, 1, -1 do
local conss = i + j - 1 > c1l and "a" or table.concat(c1Set, "", i, i + j - 1)
if consonants then
table.insert(consSet, conss)
i = i + j
break
end
if j == 1 then return nil end
end
end
return consSet
end
local function initClus(c1, mode)
local fittest, init, cData, pos = "", {}, {}, 1
c1 = gsub(c1, j, "")
if consonants then
local cData = consonants
c1, fittest = cData, cData.class
else
local consSet = getCons(mw.text.split(c1, ""))
if not consSet then return error("Error handling initial " .. c1 .. ".") end
for seq, ch in ipairs(consSet) do
local cData = consonants
fittest = (not find(cData, recessive) and not find(cData, "ng")
or (fittest == "" and seq == #consSet))
and cData.class or fittest
table.insert(init, cData)
end
c1 = table.concat(init)
end
c1 = gsub(c1, "(.)", "p%1")
c1 = gsub(c1, "(.)", "t%1")
if mode == "ipa" then
c1 = gsub(c1, "p()", "pʰ%1")
c1 = gsub(c1, "pʰ()", "p%1")
c1 = gsub(c1, "t()", "tʰ%1")
c1 = gsub(c1, "tʰ()", "t%1")
c1 = gsub(c1, "k()", "kʰ%1")
c1 = gsub(c1, "kʰ()", "k%1")
c1 = gsub(c1, "c()", "cʰ%1")
c1 = gsub(c1, "cʰ()", "c%1")
end
return c1, fittest
end
local function rime(v1, c2, fittest, red, mode)
if red == apos then v1 = red end
if vowels then return vowels end
c2 = consonants or c2
if ((v1 == "័" or v1 == "ា់") and (find(c2, "") or c2 == "ng")) or
(v1 == "េ" and (find(c2, "") or c2 == "ñ")) or
(v1 == "់" and find(c2, "")) or
((v1 == "ិ" or v1 == "ុ") and c2 ~= "") then
v1 = v1 .. "2"
end
v1 = vowels and vowels or v1
if (glottify and mode == "ipa") and c2 == "k" then c2 = "ʡ" end --proxy
return v1 .. c2
end
function export.convert(text, mode, source)
block = find(text, "%-")
text = sylRedist(export.syllabify(text), block)
if not text then return nil end
for syllable in mw.ustring.gmatch(text, kmString) do
local unchanged, sylStruc = syllable, {}
local c1, v1, c2, bantak, red = export.syl_analysis(syllable)
if not c1 then return nil end
c1, fittest = initClus(c1, mode)
if source == "temp" and (err or err) then
--require("Module:debug").track("km-pron/error-prone finals")
end
v1c2 = rime(v1 .. bantak, c2, fittest, red, mode)
if not v1c2 then return nil end
text = gsub(text, unchanged, c1 .. v1c2, 1)
end
text = gsub(text, "(.%%%-.)", ambig)
text = gsub(text, "%%", "")
text = gsub(text, "%-", ".")
text = gsub(text, "", "-")
text = gsub(text, "ʔ()", "%1")
text = gsub(text, "ŭə%.", "ɔ.")
text = gsub(text, "()%.", "%1.")
text = gsub(text, "ʡ%.s", "k.s")
text = gsub(text, "ʡ", "ʔ")
if mode == "tc" then
text = gsub(text, "%.%.%.", "…")
text = gsub(text, "%.", "")
else
text = gsub(text, "%-", ".")
local readings = {}
for reading in mw.text.gsplit(text, ", ") do
table.insert(readings, (gsub(reading, "^(+)%.(+)$", "%1.ˈ%2")))
end
text = table.concat(readings, ", ")
text = gsub(text, "^(+) (+)$", "%1 ˈ%2")
end
return text
end
local function return_error()
return error("The entry title or respelling contains zero-space width character. Please remove it.")
end
function export.make(frame)
local params = {
= { list = true },
= {},
= { alias_of = "a" },
= { default = pagename },
}
local args = require("Module:parameters").process(frame:getParent().args, params)
local output_text, respellings, transcriptions, ipas = {}, {}, {}, {}
if find(pagename, "") then return_error() end
if #args == 0 then args = { args.word } end
for _, param in ipairs(args) do
if find(param, "") then return_error() end
table.insert(respellings, export.syllabify(param))
table.insert(transcriptions, export.convert(param, "tc", "temp"))
table.insert(ipas, export.convert(param, "ipa"))
end
separate = (gsub(table.concat(respellings), "", "")) ~= args.word
respelling = table.concat(respellings, " / ")
local function row(a, b, class, lang, size)
return "\n<tr>" ..
tostring( mw.html.create( "td" )
:css( "padding-right", "0.8em" )
:css( "padding-left", "0.7em" )
:css( "font-size", "10.5pt" )
:css( "font-family", "DejaVu Sans, sans-serif" )
:css( "color", "#555" )
:css( "font-weight", "bold" )
:css( "background-color", "#F8F9F8" )
:wikitext(a)) .. "\n" ..
tostring( mw.html.create( "td" )
:css( "padding-left", "0.8em" )
:css( "padding-right", "0.8em" )
:css( "padding-top", ".4em" )
:css( "padding-bottom", ".4em" )
:wikitext(b)) ..
"</tr>"
end
local function textFormat(text, class, size, lang)
return tostring( mw.html.create( "span" )
:attr( "class", class or "Khmr" )
:css( "font-size", size or (class == "IPA" and "95%" or "130%") )
:attr( "lang", lang or (class == "IPA" and nil or "km") )
:wikitext(text))
end
table.insert(output_text,
[=[{| style="margin: 0 .4em .4em .4em"
|
<table cellpadding=1 style="border: 1px solid #DFDFDF; text-align: center; line-height: 25pt; padding: .1em .3em .1em .3em">]=] ..
row(separate
and "'']''"
or "'']''",
textFormat(args.word) .. "<br>" .. textFormat(gsub(gsub(args.word, ".", tl), "ʰ̥", "̥ʰ"), "IPA")
) ..
(separate
and row("'']''",
textFormat(respelling) .. "<br>" ..
textFormat(gsub(gsub(respelling, ".", tl), "ʰ̥", "̥ʰ"), "IPA"))
or "") ..
row("'']''",
textFormat(table.concat(transcriptions, ", "), "IPA", "100%")
) ..
row(
"('']'') ]" ..
"<sup>(])</sup>",
textFormat("/" .. table.concat(ipas, "/ ~ /") .. "/", "IPA", "110%")
) ..
(args.a
and row("Audio", mw.getCurrentFrame():expandTemplate{
title = "Template:audio",
args = { args.a == "y" and "Km-" .. args.word .. ".ogg" or args.a, lang = "km" }} )
or ""
) ..
"</table>\n|}" .. "]")
return table.concat(output_text)
end
return export