Module:Th-rm: Difference between revisions
Appearance
ReisuDesign (talk | contribs) No edit summary |
ReisuDesign (talk | contribs) No edit summary |
||
| Line 1: | Line 1: | ||
local p = {} | local p = {} | ||
local | local arguments = require('Module:Arguments') | ||
local | local tt = require('Module:Tt') | ||
local | local AUTO_TOOLTIP = 'This romanization was generated automatically. Please add a user-curated romanization as soon as possible.' | ||
local THAI = '[ก-๛]' | |||
local consonants = { | |||
['ก'] = 'k', | ['ก'] = 'k', | ||
['ข'] = 'kh', ['ฃ'] = 'kh', ['ค'] = 'kh', ['ฅ'] = 'kh', ['ฆ'] = 'kh', | ['ข'] = 'kh', | ||
['ฃ'] = 'kh', | |||
['ค'] = 'kh', | |||
['ฅ'] = 'kh', | |||
['ฆ'] = 'kh', | |||
['ง'] = 'ng', | ['ง'] = 'ng', | ||
['จ'] = 'ch', | ['จ'] = 'ch', | ||
['ฉ'] = 'ch', ['ช'] = 'ch', | ['ฉ'] = 'ch', | ||
['ช'] = 'ch', | |||
['ซ'] = 's', | ['ซ'] = 's', | ||
['ฌ'] = 'ch', | ['ฌ'] = 'ch', | ||
| Line 18: | Line 24: | ||
['ฎ'] = 'd', | ['ฎ'] = 'd', | ||
['ฏ'] = 't', | ['ฏ'] = 't', | ||
['ฐ'] = 'th', ['ฑ'] = 'th', ['ฒ'] = 'th', | ['ฐ'] = 'th', | ||
['ฑ'] = 'th', | |||
['ฒ'] = 'th', | |||
['ณ'] = 'n', | ['ณ'] = 'n', | ||
['ด'] = 'd', | ['ด'] = 'd', | ||
['ต'] = 't', | ['ต'] = 't', | ||
['ถ'] = 'th', ['ท'] = 'th', ['ธ'] = 'th', | ['ถ'] = 'th', | ||
['ท'] = 'th', | |||
['ธ'] = 'th', | |||
['น'] = 'n', | ['น'] = 'n', | ||
['บ'] = 'b', | ['บ'] = 'b', | ||
| Line 36: | Line 46: | ||
['ล'] = 'l', | ['ล'] = 'l', | ||
['ว'] = 'w', | ['ว'] = 'w', | ||
['ศ'] = 's', ['ษ'] = 's', ['ส'] = 's', | ['ศ'] = 's', | ||
['ษ'] = 's', | |||
['ส'] = 's', | |||
['ห'] = 'h', | ['ห'] = 'h', | ||
['ฬ'] = 'l', | ['ฬ'] = 'l', | ||
['ฮ'] = 'h' | ['ฮ'] = 'h', | ||
['อ'] = '' | |||
} | } | ||
local | local finals = { | ||
['ก'] = 'k', | ['ก'] = 'k', | ||
['ข'] = 'k', ['ฃ'] = 'k', ['ค'] = 'k', ['ฅ'] = 'k', ['ฆ'] = 'k', | ['ข'] = 'k', | ||
['ฃ'] = 'k', | |||
['ค'] = 'k', | |||
['ฅ'] = 'k', | |||
['ฆ'] = 'k', | |||
['จ'] = 't', | ['จ'] = 't', | ||
['ฉ'] = 't', ['ช'] = 't', ['ซ'] = 't', | ['ฉ'] = 't', | ||
['ฌ'] = '', | ['ช'] = 't', | ||
[' | ['ซ'] = 't', | ||
['ฌ'] = 't', | |||
['ฐ'] = 't', ['ฑ'] = 't', ['ฒ'] = 't | ['ฎ'] = 't', | ||
['ฏ'] = 't', | |||
['ฐ'] = 't', | |||
['ฑ'] = 't', | |||
['ฒ'] = 't', | |||
['ด'] = 't', | ['ด'] = 't', | ||
['ต'] = 't', | ['ต'] = 't', | ||
['ถ'] = 't', ['ท'] = 't', ['ธ'] = 't', | ['ถ'] = 't', | ||
[' | ['ท'] = 't', | ||
['ธ'] = 't', | |||
['ศ'] = 't', | |||
['ษ'] = 't', | |||
['ส'] = 't', | |||
['บ'] = 'p', | ['บ'] = 'p', | ||
['ป'] = 'p', | ['ป'] = 'p', | ||
['ผ'] = '', ['ฝ'] = '', | ['ผ'] = 'p', | ||
['พ'] = 'p', ['ฟ'] = 'p', ['ภ'] = 'p', | ['ฝ'] = 'p', | ||
[' | ['พ'] = 'p', | ||
[' | ['ฟ'] = 'p', | ||
['ภ'] = 'p', | |||
['ง'] = 'ng', | |||
['ญ'] = 'n', | |||
['ณ'] = 'n', | |||
['น'] = 'n', | |||
['ร'] = 'n', | ['ร'] = 'n', | ||
['ล'] = 'n', | ['ล'] = 'n', | ||
['ฬ'] = 'n', | ['ฬ'] = 'n', | ||
[' | |||
['ม'] = 'm' | |||
} | } | ||
local | local vowels = { | ||
-- Longest forms first. | |||
['เียะ'] = 'ia', | |||
['เีย'] = 'ia', | |||
['เือะ'] = 'uea', | |||
['เือ'] = 'uea', | |||
['เือย'] = 'ueai', | |||
['เียว'] = 'iao', | |||
['ัวะ'] = 'ua', | |||
['ัว'] = 'ua', | |||
['เิ็ย'] = 'oei', | |||
['เิย'] = 'oei', | |||
['เย'] = 'oei', | |||
['เอย'] = 'oei', | |||
['็อย'] = 'oi', | |||
['อย'] = 'oi', | |||
['โย'] = 'oi', | |||
['ุย'] = 'ui', | |||
['วย'] = 'uai', | |||
['ัย'] = 'ai', | |||
['ไย'] = 'ai', | |||
['าย'] = 'ai', | |||
['ใ'] = 'ai', | |||
['ไ'] = 'ai', | |||
['เ็ว'] = 'eo', | |||
['แ็ว'] = 'aeo', | |||
['เา'] = 'ao', | |||
['าว'] = 'ao', | |||
['เอว'] = 'oeu', | |||
['แว'] = 'aeo', | |||
['เว'] = 'eo', | |||
['โว'] = 'ou', | |||
['ิว'] = 'io', | |||
['ีว'] = 'io', | |||
['ำ'] = 'am', | |||
['ือ'] = 'ue', | |||
['ื'] = 'ue', | |||
['ึ'] = 'ue', | |||
['ิ'] = 'i', | |||
['ี'] = 'i', | |||
['ุ'] = 'u', | |||
['ู'] = 'u', | |||
['เ็'] = 'e', | |||
['เ'] = 'e', | |||
['แ็'] = 'ae', | |||
['แ'] = 'ae', | |||
['โะ'] = 'o', | |||
['โ'] = 'o', | |||
['เาะ'] = 'o', | |||
['อ'] = 'o', | |||
['เอะ'] = 'oe', | |||
['เอ'] = 'oe', | |||
['เะ'] = 'e', | |||
['ะ'] = 'a', | |||
['ั'] = 'a', | |||
['า'] = 'a' | |||
} | |||
local vowel_order = { | |||
'เือย', | |||
'เียว', | |||
'เือะ', | |||
'เียะ', | |||
'เีย', | |||
'เือ', | |||
'ัวะ', | |||
'ัว', | |||
'เิ็ย', | |||
'เิย', | |||
'็อย', | |||
'เอว', | |||
'แ็ว', | |||
'เ็ว', | |||
'เอย', | |||
'เา', | |||
'าว', | |||
'เว', | |||
'แว', | |||
'โว', | |||
'าย', | |||
'ไย', | |||
'ัย', | |||
'ุย', | |||
'วย', | |||
'อย', | |||
'โย', | |||
'เย', | |||
'ำ', | |||
'ิ', | |||
'ี', | |||
'ึ', | |||
'ื', | |||
'ุ', | |||
'ู', | |||
'เ็', | |||
'แ็', | |||
'เาะ', | |||
'เอะ', | |||
'เะ', | |||
'ะ', | |||
'ั', | |||
'า', | |||
'เ', | |||
'แ', | |||
'โ', | |||
'อ', | |||
'ใ', | |||
'ไ' | |||
} | |||
local vowels_with_front = { | |||
['เ'] = true, | |||
['แ'] = true, | |||
['โ'] = true, | |||
['ใ'] = true, | |||
['ไ'] = true | |||
} | |||
local consonant_set = {} | |||
for c in pairs(consonants) do | |||
consonant_set[c] = true | |||
end | |||
local valid_clusters = { | |||
['กร'] = true, | |||
['กล'] = true, | |||
['กว'] = true, | |||
['ขร'] = true, | |||
['ขล'] = true, | |||
['ขว'] = true, | |||
['คร'] = true, | |||
['คล'] = true, | |||
['คว'] = true, | |||
['ฆร'] = true, | |||
['ฆล'] = true, | |||
['จร'] = true, | |||
['ตร'] = true, | |||
['ปร'] = true, | |||
['ปล'] = true, | |||
['พร'] = true, | |||
['พล'] = true, | |||
['ผล'] = true, | |||
['ทร'] = true, | |||
['ธร'] = true, | |||
['ศร'] = true, | |||
['สร'] = true, | |||
['สล'] = true, | |||
['สว'] = true, | |||
['ซร'] = true, | |||
['ฝร'] = true, | |||
['ฟร'] = true, | |||
['หน'] = true, | |||
['หม'] = true, | |||
['หย'] = true, | |||
['หร'] = true, | |||
['หล'] = true, | |||
['หว'] = true, | |||
['หญ'] = true | |||
} | |||
local h_silent_clusters = { | |||
['หง'] = true, | |||
['หน'] = true, | |||
['หม'] = true, | |||
['หย'] = true, | |||
['หร'] = true, | |||
['หล'] = true, | |||
['หว'] = true, | |||
['หญ'] = true | |||
} | |||
local r_silent_clusters = { | |||
['จร'] = true, | |||
['ซร'] = true, | |||
['ศร'] = true, | |||
['สร'] = true | |||
} | |||
local function chars(text) | |||
local result = {} | |||
for c in mw.text.gsplit(text, '') do | |||
table.insert(result, c) | |||
end | |||
return result | |||
end | |||
local function isConsonant(c) | local function isConsonant(c) | ||
return c and | return c and consonant_set[c] == true | ||
end | end | ||
local function isThai(c) | local function isThai(c) | ||
return c and | return c and mw.ustring.find(c, THAI) ~= nil | ||
end | end | ||
local function | local function isTone(c) | ||
return c | return c == '่' or c == '้' or c == '๊' or c == '๋' or c == '์' | ||
end | end | ||
local function | local function isFrontVowel(c) | ||
return c and vowels_with_front[c] == true | |||
end | end | ||
local function | local function getVowel(chars_, pos) | ||
local remaining = table.concat(chars_, '', pos) | |||
for _, form in ipairs(vowel_order) do | |||
if mw.ustring.sub(remaining, 1, mw.ustring.len(form)) == form then | |||
return form, vowels[form] | |||
end | |||
end | |||
return | return nil, nil | ||
end | end | ||
local function | local function skipMarks(chars_, pos) | ||
while chars_[pos] and ( | |||
chars_[pos] == '็' or | |||
chars_[pos] == '่' or | |||
chars_[pos] == '้' or | |||
chars_[pos] == '๊' or | |||
chars_[pos] == '๋' or | |||
chars_[pos] == '์' or | |||
chars_[pos] == 'ํ' or | |||
chars_[pos] == '๎' | |||
) do | |||
pos = pos + 1 | |||
end | |||
return pos | |||
end | |||
local function findNextVowel(chars_, pos) | |||
local i = pos | |||
while chars_[i] do | |||
if isFrontVowel(chars_[i]) then | |||
return i | |||
end | |||
for _, form in ipairs(vowel_order) do | |||
if mw.ustring.sub(table.concat(chars_, '', i), 1, mw.ustring.len(form)) == form then | |||
return i | |||
end | |||
end | |||
if chars_[i] == '์' then | |||
i = i + 2 | |||
else | |||
i = i + 1 | |||
end | |||
end | |||
return nil | |||
end | |||
local function hasVowelAt(chars_, pos) | |||
local form = getVowel(chars_, pos) | |||
return form ~= nil | |||
end | |||
local function getInitial(chars_, pos) | |||
local c1 = chars_[pos] | |||
if not isConsonant(c1) then | |||
return '', pos | |||
end | |||
local c2 = chars_[pos + 1] | |||
if c1 == 'ห' and c2 and h_silent_clusters[c1 .. c2] then | |||
return consonants[c2], pos + 2, true, c1 .. c2 | |||
end | |||
if c2 and valid_clusters[c1 .. c2] then | |||
if c1 == 'ท' and c2 == 'ร' then | |||
return 's', pos + 2, false, c1 .. c2 | |||
end | |||
if r_silent_clusters[c1 .. c2] then | |||
return consonants[c1], pos + 2, false, c1 .. c2 | |||
end | |||
return consonants[c1] .. consonants[c2], pos + 2, false, c1 .. c2 | |||
end | |||
return consonants[c1], pos + 1, false, c1 | |||
end | |||
local function getCoda(chars_, pos) | |||
local c = chars_[pos] | |||
if not c or not isConsonant(c) then | |||
return '', pos | |||
end | |||
if c == 'อ' or c == 'ห' or c == 'ฮ' then | |||
return '', pos | |||
end | |||
if c == 'ย' or c == 'ว' then | |||
return '', pos | |||
end | |||
if finals[c] then | |||
return finals[c], pos + 1 | |||
end | |||
return '', pos + 1 | |||
end | |||
local function removeSilent(chars_) | |||
local result = {} | |||
local i = 1 | |||
while chars_[i] do | |||
if chars_[i] == '์' then | |||
i = i + 1 | |||
while chars_[i] and ( | |||
isConsonant(chars_[i]) or | |||
chars_[i] == '็' or | |||
chars_[i] == '่' or | |||
chars_[i] == '้' or | |||
chars_[i] == '๊' or | |||
chars_[i] == '๋' | |||
) do | |||
i = i + 1 | |||
end | |||
else | |||
table.insert(result, chars_[i]) | |||
i = i + 1 | |||
end | end | ||
end | end | ||
return result | |||
return | |||
end | end | ||
local function | local function cleanWord(word) | ||
word = mw.ustring.gsub(word, '์', '') | |||
return word | |||
end | |||
local function parseSyllable(chars_, start) | |||
local n = #chars_ | |||
local pos = start | local pos = start | ||
if not chars_[pos] then | |||
return nil, pos | |||
end | |||
local front = '' | |||
local | |||
if | if isFrontVowel(chars_[pos]) then | ||
front = chars_[pos] | |||
pos = pos + 1 | pos = pos + 1 | ||
end | end | ||
local | local initial, afterInitial, silentH, cluster = | ||
getInitial(chars_, pos) | |||
if not isConsonant( | if not chars_[pos] or not isConsonant(chars_[pos]) then | ||
return | return nil, start + 1 | ||
end | end | ||
pos = afterInitial | |||
local vowelForm | |||
local vowel | |||
if front ~= '' then | |||
local rest = front | |||
for _, form in ipairs(vowel_order) do | |||
local candidate = front .. mw.ustring.sub( | |||
table.concat(chars_, '', pos), | |||
1, | |||
mw.ustring.len(form) | |||
) | |||
if vowels[candidate] then | |||
vowelForm = candidate | |||
vowel = vowels[candidate] | |||
pos = pos + mw.ustring.len(form) | |||
break | |||
end | |||
end | |||
if not vowel then | |||
if front == 'เ' then | |||
if chars_[pos] == 'า' then | |||
pos = pos + | vowelForm = 'เา' | ||
vowel = 'ao' | |||
pos = pos + 1 | |||
else | |||
vowelForm = 'เ' | |||
vowel = 'e' | |||
end | |||
elseif front == 'แ' then | |||
vowelForm = 'แ' | |||
vowel = 'ae' | |||
elseif front == 'โ' then | |||
vowelForm = 'โ' | |||
vowel = 'o' | |||
elseif front == 'ใ' or front == 'ไ' then | |||
vowelForm = front | |||
vowel = 'ai' | |||
end | |||
end | |||
else | |||
vowelForm, vowel = getVowel(chars_, pos) | |||
if vowelForm then | |||
pos = pos + mw.ustring.len(vowelForm) | |||
end | |||
end | end | ||
-- | -- No written vowel: determine an implied vowel. | ||
if not vowel then | |||
local nextC = chars_[pos] | |||
if nextC and isConsonant(nextC) then | |||
local nextNext = chars_[pos + 1] | |||
if nextNext and ( | |||
isFrontVowel(nextNext) or | |||
hasVowelAt(chars_, pos + 1) | |||
) then | |||
-- Open syllable with an implied a. | |||
vowelForm = '' | |||
vowel = 'a' | |||
else | |||
-- Closed syllable with an implied o. | |||
vowelForm = '' | |||
vowel = 'o' | |||
end | |||
else | |||
vowelForm = '' | |||
vowel = 'a' | |||
end | |||
end | end | ||
pos = skipMarks(chars_, pos) | |||
local coda = '' | |||
local codaPos = pos | |||
if chars_[pos] and isConsonant(chars_[pos]) then | |||
local nextC = chars_[pos] | |||
local nextNext = chars_[pos + 1] | |||
-- If the following consonant is followed by a vowel, | |||
-- it normally starts the next syllable rather than being a coda. | |||
if nextNext and ( | |||
isFrontVowel(nextNext) or | |||
hasVowelAt(chars_, pos + 1) | |||
) then | |||
coda = '' | |||
else | |||
coda, codaPos = getCoda(chars_, pos) | |||
end | |||
end | end | ||
pos = codaPos | |||
pos = skipMarks(chars_, pos) | |||
return { | |||
initial = initial, | |||
vowel = vowel, | |||
vowelForm = vowelForm or '', | |||
coda = coda, | |||
next = pos, | |||
cluster = cluster, | |||
silentH = silentH | |||
}, pos | |||
end | |||
local function shouldHyphen(previous, current) | |||
if not previous or not current then | |||
return false | |||
end | |||
-- | -- A hyphen is used before a syllable beginning with a vowel. | ||
if | if current.initial == '' and current.vowel ~= '' then | ||
return true | |||
end | end | ||
-- | -- A syllable ending in a vowel followed by initial ng. | ||
if previous.coda == '' and current.initial == 'ng' then | |||
if | return true | ||
end | end | ||
return false | |||
local | end | ||
local | |||
local function romanizeWord(word) | |||
word = cleanWord(word) | |||
local c = chars(word) | |||
c = removeSilent(c) | |||
local syllables = {} | |||
local pos = 1 | |||
local safety = 0 | |||
while pos <= #c and safety < 1000 do | |||
safety = safety + 1 | |||
if not c[pos] then | |||
break | |||
end | |||
if c[pos] == 'ๆ' then | |||
table.insert(syllables, { | |||
if | raw = 'ๆ', | ||
initial = '', | |||
vowel = '', | |||
coda = '' | |||
}) | |||
pos = pos + 1 | pos = pos + 1 | ||
elseif isConsonant(c[pos]) or isFrontVowel(c[pos]) then | |||
local syllable, nextPos = parseSyllable(c, pos) | |||
-- | if syllable then | ||
table.insert(syllables, syllable) | |||
if nextPos <= pos then | |||
pos = pos + 1 | |||
else | |||
pos = nextPos | |||
end | |||
else | |||
-- Unknown Thai structure: consume one character so that | |||
-- the parser can never become stuck. | |||
table.insert(syllables, { | |||
raw = c[pos], | |||
initial = consonants[c[pos]] or '', | |||
vowel = 'a', | |||
coda = '' | |||
}) | |||
pos = pos + 1 | |||
end | end | ||
else | |||
pos = pos + 1 | |||
end | end | ||
end | end | ||
local output = {} | |||
for i, s in ipairs(syllables) do | |||
if s.raw == 'ๆ' then | |||
table.insert(output, 'ๆ') | |||
if | |||
else | else | ||
vowel = ' | local result = (s.initial or '') .. (s.vowel or '') .. (s.coda or '') | ||
if result == '' then | |||
result = 'a' | |||
end | |||
if shouldHyphen(syllables[i - 1], s) then | |||
table.insert(output, '-') | |||
end | |||
table.insert(output, result) | |||
end | end | ||
end | end | ||
return | return table.concat(output) | ||
end | end | ||
local function | local function romanizeThaiText(text) | ||
local result = {} | local result = {} | ||
local parts = mw.text.split( | |||
text, | |||
'(\1MANUALHYPHEN\1|\1MANUALSPACE\1)', | |||
true | |||
) | |||
for _, part in ipairs(parts) do | |||
if part == '\1MANUALHYPHEN\1' then | |||
table.insert(result, '-') | |||
elseif part == '\1MANUALSPACE\1' then | |||
table.insert(result, ' ') | |||
else | |||
local last = 1 | |||
while true do | |||
local startPos, endPos = | |||
mw.ustring.find(part, THAI .. '+', last) | |||
if not startPos then | |||
table.insert(result, mw.ustring.sub(part, last)) | |||
break | |||
end | |||
if | if startPos > last then | ||
table.insert(result, | table.insert( | ||
result, | |||
mw.ustring.sub(part, last, startPos - 1) | |||
) | |||
end | end | ||
local word = | |||
mw.ustring.sub(part, startPos, endPos) | |||
table.insert(result, | |||
table.insert(result, romanizeWord(word)) | |||
last = endPos + 1 | |||
end | end | ||
end | end | ||
end | end | ||
| Line 336: | Line 740: | ||
end | end | ||
local function | local function hasThai(text) | ||
return mw.ustring.find(text, THAI) ~= nil | |||
end | |||
local function containsThai(text) | |||
return mw.ustring.find(text, '[ก-๛]') ~= nil | |||
end | |||
local function hasOnlyThaiOrFormatting(text) | |||
return not mw.ustring.find(text, '[ก-๛]') | |||
end | |||
local function validateRomanization(text) | |||
-- The automatic result must never leak Thai characters. | |||
if containsThai(text) then | |||
return false | |||
end | |||
-- Thai marks should never survive either. | |||
if mw.ustring.find(text, '[ะ-๛]') then | |||
return false | |||
end | |||
return true | |||
end | |||
local function capitalizeWords(text) | |||
return mw.ustring.gsub( | |||
text, | |||
'(%a)([%a]*)', | |||
function(first, rest) | |||
return mw.ustring.upper(first) .. rest | |||
end | end | ||
) | |||
end | |||
local function capitalizeSentence(text) | |||
local first = mw.ustring.match(text, '^[%s%p]*([a-z])') | |||
if not first then | |||
return text | |||
end | |||
local upper = mw.ustring.upper(first) | |||
return mw.ustring.gsub( | |||
text, | |||
'^([%s%p]*)' .. first, | |||
'%1' .. upper, | |||
1 | |||
) | |||
end | |||
local function convertThaiDigits(text) | |||
local digits = { | |||
['๐'] = '0', | |||
['๑'] = '1', | |||
['๒'] = '2', | |||
['๓'] = '3', | |||
['๔'] = '4', | |||
['๕'] = '5', | |||
['๖'] = '6', | |||
['๗'] = '7', | |||
['๘'] = '8', | |||
['๙'] = '9' | |||
} | |||
for thai, latin in pairs(digits) do | |||
text = mw.ustring.gsub(text, thai, latin) | |||
end | end | ||
return | return text | ||
end | end | ||
function p.romanize(text) | |||
if not text or mw.text.trim(text) == '' then | |||
return '' | |||
end | |||
if not hasThai(text) then | |||
return '' | |||
end | |||
-- Manual spacing/hyphen controls. | |||
text = text | |||
:gsub('<->', '\1') | |||
:gsub('<>', '\2') | |||
text = convertThaiDigits(text) | |||
local result = romanizeThaiText(text) | |||
-- Restore manual separators. | |||
result = result | |||
:gsub('\1', '-') | |||
:gsub('\2', ' ') | |||
result = mw.ustring.gsub(result, '%s+', ' ') | |||
result = mw.text.trim(result) | |||
if not validateRomanization(result) then | |||
return '' | |||
end | end | ||
return | return result | ||
end | end | ||
function p.main(frame) | function p.main(frame) | ||
local args = | local args = arguments.getArgs(frame, { | ||
parentFirst = true, | parentFirst = true, | ||
wrappers = {'Template:Th-rm'} | wrappers = {'Template:Th-rm'} | ||
}) | }) | ||
local text = args[1] | |||
if not text or mw.text.trim(text) == '' then | |||
return '' | return '' | ||
end | end | ||
-- If there is no Thai at all, this module should not create | |||
-- an automatic romanization. | |||
-- | if not hasThai(text) then | ||
if not | |||
return '' | return '' | ||
end | end | ||
local result = p.romanize(text) | |||
if result == '' then | |||
return '' | |||
end | |||
if args.capi then | if args.capi then | ||
result = capitalizeWords(result) | result = capitalizeWords(result) | ||
end | end | ||
return result | if args.sent then | ||
result = capitalizeSentence(result) | |||
end | |||
-- Automatically generated romanizations are deliberately marked | |||
-- with the tooltip from Module:Tt. | |||
return tt._main({ | |||
[1] = result, | |||
[2] = AUTO_TOOLTIP, | |||
['class'] = args.class | |||
}) | |||
end | end | ||
return p | return p | ||
Revision as of 17:58, 2 September 2026
| Main | Documentation | Usage |
This module implements {{th-rm}}.
local p = {}
local arguments = require('Module:Arguments')
local tt = require('Module:Tt')
local AUTO_TOOLTIP = 'This romanization was generated automatically. Please add a user-curated romanization as soon as possible.'
local THAI = '[ก-๛]'
local consonants = {
['ก'] = 'k',
['ข'] = 'kh',
['ฃ'] = 'kh',
['ค'] = 'kh',
['ฅ'] = 'kh',
['ฆ'] = 'kh',
['ง'] = 'ng',
['จ'] = 'ch',
['ฉ'] = 'ch',
['ช'] = 'ch',
['ซ'] = 's',
['ฌ'] = 'ch',
['ญ'] = 'y',
['ฎ'] = 'd',
['ฏ'] = 't',
['ฐ'] = 'th',
['ฑ'] = 'th',
['ฒ'] = 'th',
['ณ'] = 'n',
['ด'] = 'd',
['ต'] = 't',
['ถ'] = 'th',
['ท'] = 'th',
['ธ'] = 'th',
['น'] = 'n',
['บ'] = 'b',
['ป'] = 'p',
['ผ'] = 'ph',
['ฝ'] = 'f',
['พ'] = 'ph',
['ฟ'] = 'f',
['ภ'] = 'ph',
['ม'] = 'm',
['ย'] = 'y',
['ร'] = 'r',
['ล'] = 'l',
['ว'] = 'w',
['ศ'] = 's',
['ษ'] = 's',
['ส'] = 's',
['ห'] = 'h',
['ฬ'] = 'l',
['ฮ'] = 'h',
['อ'] = ''
}
local finals = {
['ก'] = 'k',
['ข'] = 'k',
['ฃ'] = 'k',
['ค'] = 'k',
['ฅ'] = 'k',
['ฆ'] = 'k',
['จ'] = 't',
['ฉ'] = 't',
['ช'] = 't',
['ซ'] = 't',
['ฌ'] = 't',
['ฎ'] = 't',
['ฏ'] = 't',
['ฐ'] = 't',
['ฑ'] = 't',
['ฒ'] = 't',
['ด'] = 't',
['ต'] = 't',
['ถ'] = 't',
['ท'] = 't',
['ธ'] = 't',
['ศ'] = 't',
['ษ'] = 't',
['ส'] = 't',
['บ'] = 'p',
['ป'] = 'p',
['ผ'] = 'p',
['ฝ'] = 'p',
['พ'] = 'p',
['ฟ'] = 'p',
['ภ'] = 'p',
['ง'] = 'ng',
['ญ'] = 'n',
['ณ'] = 'n',
['น'] = 'n',
['ร'] = 'n',
['ล'] = 'n',
['ฬ'] = 'n',
['ม'] = 'm'
}
local vowels = {
-- Longest forms first.
['เียะ'] = 'ia',
['เีย'] = 'ia',
['เือะ'] = 'uea',
['เือ'] = 'uea',
['เือย'] = 'ueai',
['เียว'] = 'iao',
['ัวะ'] = 'ua',
['ัว'] = 'ua',
['เิ็ย'] = 'oei',
['เิย'] = 'oei',
['เย'] = 'oei',
['เอย'] = 'oei',
['็อย'] = 'oi',
['อย'] = 'oi',
['โย'] = 'oi',
['ุย'] = 'ui',
['วย'] = 'uai',
['ัย'] = 'ai',
['ไย'] = 'ai',
['าย'] = 'ai',
['ใ'] = 'ai',
['ไ'] = 'ai',
['เ็ว'] = 'eo',
['แ็ว'] = 'aeo',
['เา'] = 'ao',
['าว'] = 'ao',
['เอว'] = 'oeu',
['แว'] = 'aeo',
['เว'] = 'eo',
['โว'] = 'ou',
['ิว'] = 'io',
['ีว'] = 'io',
['ำ'] = 'am',
['ือ'] = 'ue',
['ื'] = 'ue',
['ึ'] = 'ue',
['ิ'] = 'i',
['ี'] = 'i',
['ุ'] = 'u',
['ู'] = 'u',
['เ็'] = 'e',
['เ'] = 'e',
['แ็'] = 'ae',
['แ'] = 'ae',
['โะ'] = 'o',
['โ'] = 'o',
['เาะ'] = 'o',
['อ'] = 'o',
['เอะ'] = 'oe',
['เอ'] = 'oe',
['เะ'] = 'e',
['ะ'] = 'a',
['ั'] = 'a',
['า'] = 'a'
}
local vowel_order = {
'เือย',
'เียว',
'เือะ',
'เียะ',
'เีย',
'เือ',
'ัวะ',
'ัว',
'เิ็ย',
'เิย',
'็อย',
'เอว',
'แ็ว',
'เ็ว',
'เอย',
'เา',
'าว',
'เว',
'แว',
'โว',
'าย',
'ไย',
'ัย',
'ุย',
'วย',
'อย',
'โย',
'เย',
'ำ',
'ิ',
'ี',
'ึ',
'ื',
'ุ',
'ู',
'เ็',
'แ็',
'เาะ',
'เอะ',
'เะ',
'ะ',
'ั',
'า',
'เ',
'แ',
'โ',
'อ',
'ใ',
'ไ'
}
local vowels_with_front = {
['เ'] = true,
['แ'] = true,
['โ'] = true,
['ใ'] = true,
['ไ'] = true
}
local consonant_set = {}
for c in pairs(consonants) do
consonant_set[c] = true
end
local valid_clusters = {
['กร'] = true,
['กล'] = true,
['กว'] = true,
['ขร'] = true,
['ขล'] = true,
['ขว'] = true,
['คร'] = true,
['คล'] = true,
['คว'] = true,
['ฆร'] = true,
['ฆล'] = true,
['จร'] = true,
['ตร'] = true,
['ปร'] = true,
['ปล'] = true,
['พร'] = true,
['พล'] = true,
['ผล'] = true,
['ทร'] = true,
['ธร'] = true,
['ศร'] = true,
['สร'] = true,
['สล'] = true,
['สว'] = true,
['ซร'] = true,
['ฝร'] = true,
['ฟร'] = true,
['หน'] = true,
['หม'] = true,
['หย'] = true,
['หร'] = true,
['หล'] = true,
['หว'] = true,
['หญ'] = true
}
local h_silent_clusters = {
['หง'] = true,
['หน'] = true,
['หม'] = true,
['หย'] = true,
['หร'] = true,
['หล'] = true,
['หว'] = true,
['หญ'] = true
}
local r_silent_clusters = {
['จร'] = true,
['ซร'] = true,
['ศร'] = true,
['สร'] = true
}
local function chars(text)
local result = {}
for c in mw.text.gsplit(text, '') do
table.insert(result, c)
end
return result
end
local function isConsonant(c)
return c and consonant_set[c] == true
end
local function isThai(c)
return c and mw.ustring.find(c, THAI) ~= nil
end
local function isTone(c)
return c == '่' or c == '้' or c == '๊' or c == '๋' or c == '์'
end
local function isFrontVowel(c)
return c and vowels_with_front[c] == true
end
local function getVowel(chars_, pos)
local remaining = table.concat(chars_, '', pos)
for _, form in ipairs(vowel_order) do
if mw.ustring.sub(remaining, 1, mw.ustring.len(form)) == form then
return form, vowels[form]
end
end
return nil, nil
end
local function skipMarks(chars_, pos)
while chars_[pos] and (
chars_[pos] == '็' or
chars_[pos] == '่' or
chars_[pos] == '้' or
chars_[pos] == '๊' or
chars_[pos] == '๋' or
chars_[pos] == '์' or
chars_[pos] == 'ํ' or
chars_[pos] == '๎'
) do
pos = pos + 1
end
return pos
end
local function findNextVowel(chars_, pos)
local i = pos
while chars_[i] do
if isFrontVowel(chars_[i]) then
return i
end
for _, form in ipairs(vowel_order) do
if mw.ustring.sub(table.concat(chars_, '', i), 1, mw.ustring.len(form)) == form then
return i
end
end
if chars_[i] == '์' then
i = i + 2
else
i = i + 1
end
end
return nil
end
local function hasVowelAt(chars_, pos)
local form = getVowel(chars_, pos)
return form ~= nil
end
local function getInitial(chars_, pos)
local c1 = chars_[pos]
if not isConsonant(c1) then
return '', pos
end
local c2 = chars_[pos + 1]
if c1 == 'ห' and c2 and h_silent_clusters[c1 .. c2] then
return consonants[c2], pos + 2, true, c1 .. c2
end
if c2 and valid_clusters[c1 .. c2] then
if c1 == 'ท' and c2 == 'ร' then
return 's', pos + 2, false, c1 .. c2
end
if r_silent_clusters[c1 .. c2] then
return consonants[c1], pos + 2, false, c1 .. c2
end
return consonants[c1] .. consonants[c2], pos + 2, false, c1 .. c2
end
return consonants[c1], pos + 1, false, c1
end
local function getCoda(chars_, pos)
local c = chars_[pos]
if not c or not isConsonant(c) then
return '', pos
end
if c == 'อ' or c == 'ห' or c == 'ฮ' then
return '', pos
end
if c == 'ย' or c == 'ว' then
return '', pos
end
if finals[c] then
return finals[c], pos + 1
end
return '', pos + 1
end
local function removeSilent(chars_)
local result = {}
local i = 1
while chars_[i] do
if chars_[i] == '์' then
i = i + 1
while chars_[i] and (
isConsonant(chars_[i]) or
chars_[i] == '็' or
chars_[i] == '่' or
chars_[i] == '้' or
chars_[i] == '๊' or
chars_[i] == '๋'
) do
i = i + 1
end
else
table.insert(result, chars_[i])
i = i + 1
end
end
return result
end
local function cleanWord(word)
word = mw.ustring.gsub(word, '์', '')
return word
end
local function parseSyllable(chars_, start)
local n = #chars_
local pos = start
if not chars_[pos] then
return nil, pos
end
local front = ''
if isFrontVowel(chars_[pos]) then
front = chars_[pos]
pos = pos + 1
end
local initial, afterInitial, silentH, cluster =
getInitial(chars_, pos)
if not chars_[pos] or not isConsonant(chars_[pos]) then
return nil, start + 1
end
pos = afterInitial
local vowelForm
local vowel
if front ~= '' then
local rest = front
for _, form in ipairs(vowel_order) do
local candidate = front .. mw.ustring.sub(
table.concat(chars_, '', pos),
1,
mw.ustring.len(form)
)
if vowels[candidate] then
vowelForm = candidate
vowel = vowels[candidate]
pos = pos + mw.ustring.len(form)
break
end
end
if not vowel then
if front == 'เ' then
if chars_[pos] == 'า' then
vowelForm = 'เา'
vowel = 'ao'
pos = pos + 1
else
vowelForm = 'เ'
vowel = 'e'
end
elseif front == 'แ' then
vowelForm = 'แ'
vowel = 'ae'
elseif front == 'โ' then
vowelForm = 'โ'
vowel = 'o'
elseif front == 'ใ' or front == 'ไ' then
vowelForm = front
vowel = 'ai'
end
end
else
vowelForm, vowel = getVowel(chars_, pos)
if vowelForm then
pos = pos + mw.ustring.len(vowelForm)
end
end
-- No written vowel: determine an implied vowel.
if not vowel then
local nextC = chars_[pos]
if nextC and isConsonant(nextC) then
local nextNext = chars_[pos + 1]
if nextNext and (
isFrontVowel(nextNext) or
hasVowelAt(chars_, pos + 1)
) then
-- Open syllable with an implied a.
vowelForm = ''
vowel = 'a'
else
-- Closed syllable with an implied o.
vowelForm = ''
vowel = 'o'
end
else
vowelForm = ''
vowel = 'a'
end
end
pos = skipMarks(chars_, pos)
local coda = ''
local codaPos = pos
if chars_[pos] and isConsonant(chars_[pos]) then
local nextC = chars_[pos]
local nextNext = chars_[pos + 1]
-- If the following consonant is followed by a vowel,
-- it normally starts the next syllable rather than being a coda.
if nextNext and (
isFrontVowel(nextNext) or
hasVowelAt(chars_, pos + 1)
) then
coda = ''
else
coda, codaPos = getCoda(chars_, pos)
end
end
pos = codaPos
pos = skipMarks(chars_, pos)
return {
initial = initial,
vowel = vowel,
vowelForm = vowelForm or '',
coda = coda,
next = pos,
cluster = cluster,
silentH = silentH
}, pos
end
local function shouldHyphen(previous, current)
if not previous or not current then
return false
end
-- A hyphen is used before a syllable beginning with a vowel.
if current.initial == '' and current.vowel ~= '' then
return true
end
-- A syllable ending in a vowel followed by initial ng.
if previous.coda == '' and current.initial == 'ng' then
return true
end
return false
end
local function romanizeWord(word)
word = cleanWord(word)
local c = chars(word)
c = removeSilent(c)
local syllables = {}
local pos = 1
local safety = 0
while pos <= #c and safety < 1000 do
safety = safety + 1
if not c[pos] then
break
end
if c[pos] == 'ๆ' then
table.insert(syllables, {
raw = 'ๆ',
initial = '',
vowel = '',
coda = ''
})
pos = pos + 1
elseif isConsonant(c[pos]) or isFrontVowel(c[pos]) then
local syllable, nextPos = parseSyllable(c, pos)
if syllable then
table.insert(syllables, syllable)
if nextPos <= pos then
pos = pos + 1
else
pos = nextPos
end
else
-- Unknown Thai structure: consume one character so that
-- the parser can never become stuck.
table.insert(syllables, {
raw = c[pos],
initial = consonants[c[pos]] or '',
vowel = 'a',
coda = ''
})
pos = pos + 1
end
else
pos = pos + 1
end
end
local output = {}
for i, s in ipairs(syllables) do
if s.raw == 'ๆ' then
table.insert(output, 'ๆ')
else
local result = (s.initial or '') .. (s.vowel or '') .. (s.coda or '')
if result == '' then
result = 'a'
end
if shouldHyphen(syllables[i - 1], s) then
table.insert(output, '-')
end
table.insert(output, result)
end
end
return table.concat(output)
end
local function romanizeThaiText(text)
local result = {}
local parts = mw.text.split(
text,
'(\1MANUALHYPHEN\1|\1MANUALSPACE\1)',
true
)
for _, part in ipairs(parts) do
if part == '\1MANUALHYPHEN\1' then
table.insert(result, '-')
elseif part == '\1MANUALSPACE\1' then
table.insert(result, ' ')
else
local last = 1
while true do
local startPos, endPos =
mw.ustring.find(part, THAI .. '+', last)
if not startPos then
table.insert(result, mw.ustring.sub(part, last))
break
end
if startPos > last then
table.insert(
result,
mw.ustring.sub(part, last, startPos - 1)
)
end
local word =
mw.ustring.sub(part, startPos, endPos)
table.insert(result, romanizeWord(word))
last = endPos + 1
end
end
end
return table.concat(result)
end
local function hasThai(text)
return mw.ustring.find(text, THAI) ~= nil
end
local function containsThai(text)
return mw.ustring.find(text, '[ก-๛]') ~= nil
end
local function hasOnlyThaiOrFormatting(text)
return not mw.ustring.find(text, '[ก-๛]')
end
local function validateRomanization(text)
-- The automatic result must never leak Thai characters.
if containsThai(text) then
return false
end
-- Thai marks should never survive either.
if mw.ustring.find(text, '[ะ-๛]') then
return false
end
return true
end
local function capitalizeWords(text)
return mw.ustring.gsub(
text,
'(%a)([%a]*)',
function(first, rest)
return mw.ustring.upper(first) .. rest
end
)
end
local function capitalizeSentence(text)
local first = mw.ustring.match(text, '^[%s%p]*([a-z])')
if not first then
return text
end
local upper = mw.ustring.upper(first)
return mw.ustring.gsub(
text,
'^([%s%p]*)' .. first,
'%1' .. upper,
1
)
end
local function convertThaiDigits(text)
local digits = {
['๐'] = '0',
['๑'] = '1',
['๒'] = '2',
['๓'] = '3',
['๔'] = '4',
['๕'] = '5',
['๖'] = '6',
['๗'] = '7',
['๘'] = '8',
['๙'] = '9'
}
for thai, latin in pairs(digits) do
text = mw.ustring.gsub(text, thai, latin)
end
return text
end
function p.romanize(text)
if not text or mw.text.trim(text) == '' then
return ''
end
if not hasThai(text) then
return ''
end
-- Manual spacing/hyphen controls.
text = text
:gsub('<->', '\1')
:gsub('<>', '\2')
text = convertThaiDigits(text)
local result = romanizeThaiText(text)
-- Restore manual separators.
result = result
:gsub('\1', '-')
:gsub('\2', ' ')
result = mw.ustring.gsub(result, '%s+', ' ')
result = mw.text.trim(result)
if not validateRomanization(result) then
return ''
end
return result
end
function p.main(frame)
local args = arguments.getArgs(frame, {
parentFirst = true,
wrappers = {'Template:Th-rm'}
})
local text = args[1]
if not text or mw.text.trim(text) == '' then
return ''
end
-- If there is no Thai at all, this module should not create
-- an automatic romanization.
if not hasThai(text) then
return ''
end
local result = p.romanize(text)
if result == '' then
return ''
end
if args.capi then
result = capitalizeWords(result)
end
if args.sent then
result = capitalizeSentence(result)
end
-- Automatically generated romanizations are deliberately marked
-- with the tooltip from Module:Tt.
return tt._main({
[1] = result,
[2] = AUTO_TOOLTIP,
['class'] = args.class
})
end
return p