Module:Th-rm: Difference between revisions
ReisuDesign (talk | contribs) No edit summary |
ReisuDesign (talk | contribs) No edit summary |
||
| Line 8: | Line 8: | ||
local THAI = '[ก-๛]' | local THAI = '[ก-๛]' | ||
-- Initial consonants. | |||
local consonants = { | local consonants = { | ||
['ก'] = 'k', | ['ก'] = 'k', | ||
| Line 55: | Line 56: | ||
} | } | ||
-- Final consonants. | |||
local finals = { | local finals = { | ||
['ก'] = 'k', | ['ก'] = 'k', | ||
| Line 102: | Line 104: | ||
} | } | ||
-- Vowels. | |||
local vowels = { | local vowels = { | ||
['เียะ'] = 'ia', | ['เียะ'] = 'ia', | ||
['เีย'] = 'ia', | ['เีย'] = 'ia', | ||
| Line 116: | Line 118: | ||
['เิ็ย'] = 'oei', | ['เิ็ย'] = 'oei', | ||
['เิย'] = 'oei', | ['เิย'] = 'oei', | ||
['เอย'] = 'oei', | ['เอย'] = 'oei', | ||
['็อย'] = 'oi', | ['็อย'] = 'oi', | ||
['อย'] = 'oi', | ['อย'] = 'oi', | ||
['ุย'] = 'ui', | ['ุย'] = 'ui', | ||
| Line 136: | Line 136: | ||
['เา'] = 'ao', | ['เา'] = 'ao', | ||
['าว'] = 'ao', | ['าว'] = 'ao', | ||
['แว'] = 'aeo', | ['แว'] = 'aeo', | ||
['เว'] = 'eo', | ['เว'] = 'eo', | ||
['ิว'] = 'io', | ['ิว'] = 'io', | ||
| Line 148: | Line 146: | ||
['ือ'] = 'ue', | ['ือ'] = 'ue', | ||
['ื'] = 'ue', | ['ื'] = 'ue', | ||
['ึ'] = 'ue', | ['ึ'] = 'ue', | ||
| Line 178: | Line 175: | ||
} | } | ||
-- Longest vowel forms must be checked first. | |||
local vowel_order = { | local vowel_order = { | ||
'เือย', | 'เือย', | ||
| Line 190: | Line 188: | ||
'เิย', | 'เิย', | ||
'็อย', | '็อย', | ||
' | 'เ็ว', | ||
'แ็ว', | 'แ็ว', | ||
'เอย', | 'เอย', | ||
'เา', | 'เา', | ||
| Line 198: | Line 195: | ||
'เว', | 'เว', | ||
'แว', | 'แว', | ||
'าย', | 'าย', | ||
'ไย', | 'ไย', | ||
| Line 205: | Line 201: | ||
'วย', | 'วย', | ||
'อย', | 'อย', | ||
'ำ', | 'ำ', | ||
'ิว', | |||
'ีว', | |||
'ิ', | 'ิ', | ||
'ี', | 'ี', | ||
| Line 218: | Line 214: | ||
'เาะ', | 'เาะ', | ||
'เอะ', | 'เอะ', | ||
'เอ', | |||
'เะ', | 'เะ', | ||
'ะ', | 'ะ', | ||
| Line 230: | Line 227: | ||
} | } | ||
local | local consonant_set = {} | ||
} | |||
for c in pairs(consonants) do | for c in pairs(consonants) do | ||
consonant_set[c] = true | consonant_set[c] = true | ||
end | end | ||
-- Thai consonant clusters. | |||
local valid_clusters = { | local valid_clusters = { | ||
['กร'] = true, | ['กร'] = true, | ||
| Line 270: | Line 261: | ||
['ซร'] = true, | ['ซร'] = true, | ||
['ฝร'] = true, | ['ฝร'] = true, | ||
['ฟร | ['ฟร'] = true | ||
} | } | ||
-- ห is silent in these clusters when followed by a vowel. | |||
local h_silent_clusters = { | local h_silent_clusters = { | ||
['หง'] = true, | ['หง'] = true, | ||
| Line 291: | Line 276: | ||
} | } | ||
-- ร is silent in these clusters when followed by a vowel. | |||
local r_silent_clusters = { | local r_silent_clusters = { | ||
['จร'] = true, | ['จร'] = true, | ||
| Line 314: | Line 300: | ||
local function isThai(c) | local function isThai(c) | ||
return c and mw.ustring.find(c, THAI) ~= nil | return c and mw.ustring.find(c, THAI) ~= nil | ||
end | end | ||
local function isFrontVowel(c) | local function isFrontVowel(c) | ||
return c | return c == 'เ' or c == 'แ' or c == 'โ' or c == 'ใ' or c == 'ไ' | ||
end | end | ||
local function skipMarks( | local function skipMarks(c, pos) | ||
while | while c[pos] and ( | ||
c[pos] == '็' or | |||
c[pos] == '่' or | |||
c[pos] == '้' or | |||
c[pos] == '๊' or | |||
c[pos] == '๋' or | |||
c[pos] == 'ํ' or | |||
c[pos] == '๎' | |||
) do | ) do | ||
pos = pos + 1 | pos = pos + 1 | ||
| Line 353: | Line 322: | ||
end | end | ||
local function | local function removeSilent(chars_) | ||
local i = | local result = {} | ||
local i = 1 | |||
while chars_[i] do | while chars_[i] do | ||
if | if chars_[i] == '์' then | ||
-- ์ makes the following consonant(s) silent. | |||
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 | end | ||
else | else | ||
table.insert(result, chars_[i]) | |||
i = i + 1 | i = i + 1 | ||
end | end | ||
end | end | ||
return | return result | ||
end | end | ||
local function | local function getVowel(c, pos) | ||
local form = | local remaining = table.concat(c, '', pos) | ||
for _, form in ipairs(vowel_order) do | |||
local length = mw.ustring.len(form) | |||
if mw.ustring.sub(remaining, 1, length) == form then | |||
return form, vowels[form] | |||
end | |||
end | |||
return | return nil, nil | ||
end | end | ||
local function getInitial( | local function getInitial(c, pos) | ||
local c1 = | local c1 = c[pos] | ||
if not isConsonant(c1) then | if not isConsonant(c1) then | ||
| Line 390: | Line 371: | ||
end | end | ||
local c2 = | local c2 = c[pos + 1] | ||
-- ห is not pronounced in หง หน หม หย หร หล หว หญ. | |||
if c1 == 'ห' and c2 and h_silent_clusters[c1 .. c2] then | if c1 == 'ห' and c2 and h_silent_clusters[c1 .. c2] then | ||
return consonants[c2], pos + 2, true | return consonants[c2], pos + 2, true | ||
end | |||
-- ทร is pronounced s in the relevant Sanskrit-derived words. | |||
-- This is the broad automatic fallback; exceptions can be manually | |||
-- supplied with th_rm. | |||
if c1 == 'ท' and c2 == 'ร' then | |||
return 's', pos + 2, false | |||
end | end | ||
if c2 and valid_clusters[c1 .. c2] then | if c2 and valid_clusters[c1 .. c2] then | ||
-- ร is silent in these clusters. | |||
if r_silent_clusters[c1 .. c2] then | if r_silent_clusters[c1 .. c2] then | ||
return consonants[c1], pos + 2, false | return consonants[c1], pos + 2, false | ||
end | end | ||
return consonants[c1] .. consonants[c2], pos + 2, false | return consonants[c1] .. consonants[c2], pos + 2, false | ||
end | end | ||
return consonants[c1], pos + 1, false | return consonants[c1], pos + 1, false | ||
end | end | ||
local function getCoda( | local function getCoda(c, pos) | ||
local c | local current = c[pos] | ||
if not | if not current or not isConsonant(current) then | ||
return '', pos | return '', pos | ||
end | end | ||
if | -- These do not act as ordinary codas here. | ||
if current == 'อ' or current == 'ห' or current == 'ฮ' then | |||
return '', pos | return '', pos | ||
end | end | ||
if | -- ย and ว are generally part of the vowel/diphthong. | ||
if current == 'ย' or current == 'ว' then | |||
return '', pos | return '', pos | ||
end | end | ||
if finals[ | if finals[current] then | ||
return finals[ | return finals[current], pos + 1 | ||
end | end | ||
| Line 433: | Line 421: | ||
end | end | ||
local function | local function startsNextSyllable(c, pos) | ||
if not c[pos] or not isConsonant(c[pos]) then | |||
return false | |||
end | |||
if isFrontVowel(c[pos + 1]) then | |||
return true | |||
end | end | ||
local _, vowel = getVowel(c, pos + 1) | |||
local | |||
return | return vowel ~= nil | ||
end | end | ||
local function parseSyllable( | local function parseSyllable(c, start) | ||
local pos = start | local pos = start | ||
if not | if not c[pos] then | ||
return nil, pos | return nil, pos | ||
end | end | ||
local initial = '' | |||
local cluster = false | |||
-- Preposed vowels. | |||
local front = '' | local front = '' | ||
if isFrontVowel( | if isFrontVowel(c[pos]) then | ||
front = | front = c[pos] | ||
pos = pos + 1 | pos = pos + 1 | ||
end | end | ||
local initial, | local initialPos = pos | ||
getInitial( | |||
initial, pos, _, cluster = | |||
getInitial(c, pos) | |||
if | if initial == '' and not isConsonant(c[initialPos]) then | ||
return nil, start + 1 | return nil, start + 1 | ||
end | end | ||
local vowel | |||
local vowelForm | local vowelForm | ||
if front ~= '' then | if front ~= '' then | ||
-- Handle the vowel forms which contain a preposed vowel. | |||
if front == 'เ' and c[pos] == 'ี' and c[pos + 1] == 'ย' then | |||
vowel = 'ia' | |||
vowelForm = 'เีย' | |||
pos = pos + 2 | |||
elseif front == 'เ' and c[pos] == 'ื' and c[pos + 1] == 'อ' then | |||
vowel = 'uea' | |||
vowelForm = 'เือ' | |||
pos = pos + 2 | |||
elseif front == 'เ' and c[pos] == 'อ' then | |||
vowel = 'oe' | |||
vowelForm = 'เอ' | |||
pos = pos + 1 | |||
elseif front == 'เ' and c[pos] == 'า' then | |||
vowel = 'ao' | |||
vowelForm = 'เา' | |||
pos = pos + 1 | |||
elseif front == 'เ' and c[pos] == '็' then | |||
vowel = 'e' | |||
vowelForm = 'เ็' | |||
pos = pos + 1 | |||
elseif front == 'แ' and c[pos] == '็' then | |||
vowel = 'ae' | |||
vowelForm = 'แ็' | |||
pos = pos + 1 | |||
elseif front == 'โ' and c[pos] == 'ะ' then | |||
vowel = 'o' | |||
vowelForm = 'โะ' | |||
pos = pos + 1 | |||
elseif front == 'ใ' or front == 'ไ' then | |||
vowel = 'ai' | |||
vowelForm = front | |||
else | |||
vowel = vowels[front] | |||
vowelForm = front | |||
end | end | ||
else | else | ||
vowelForm, vowel = getVowel( | vowelForm, vowel = getVowel(c, pos) | ||
if vowelForm then | if vowelForm then | ||
| Line 540: | Line 518: | ||
end | end | ||
-- | -- Implied vowels. | ||
if not vowel then | if not vowel then | ||
if c[pos] and isConsonant(c[pos]) then | |||
if startsNextSyllable(c, pos) then | |||
if | |||
vowel = 'a' | vowel = 'a' | ||
else | else | ||
vowel = 'o' | vowel = 'o' | ||
end | end | ||
else | else | ||
vowel = 'a' | vowel = 'a' | ||
end | end | ||
end | end | ||
pos = skipMarks( | pos = skipMarks(c, pos) | ||
-- Final consonant. | |||
local coda = '' | local coda = '' | ||
local codaPos = pos | local codaPos = pos | ||
if | if c[pos] and isConsonant(c[pos]) then | ||
-- If another consonant begins the next syllable, don't consume | |||
-- it as a coda. | |||
if startsNextSyllable(c, pos) then | |||
-- If the | |||
-- it | |||
if | |||
coda = '' | coda = '' | ||
else | else | ||
coda, codaPos = getCoda( | coda, codaPos = getCoda(c, pos) | ||
end | end | ||
end | end | ||
pos = codaPos | pos = codaPos | ||
pos = skipMarks( | pos = skipMarks(c, pos) | ||
return { | return { | ||
initial = initial, | initial = initial, | ||
vowel = vowel, | vowel = vowel, | ||
coda = coda, | coda = coda, | ||
next = pos | next = pos | ||
}, pos | }, pos | ||
end | end | ||
| Line 605: | Line 563: | ||
end | end | ||
-- | -- Before a succeeding syllable beginning with a vowel. | ||
if current.initial == '' and current.vowel ~= '' then | if current.initial == '' and current.vowel ~= '' then | ||
return true | return true | ||
end | end | ||
-- | -- Before ng when the preceding syllable ends in a vowel. | ||
if previous.coda == '' and current.initial == 'ng' then | if previous.coda == '' and current.initial == 'ng' then | ||
return true | return true | ||
| Line 619: | Line 577: | ||
local function romanizeWord(word) | local function romanizeWord(word) | ||
local c = chars(word) | local c = chars(word) | ||
c = removeSilent(c) | c = removeSilent(c) | ||
| Line 631: | Line 587: | ||
safety = safety + 1 | safety = safety + 1 | ||
if | if isConsonant(c[pos]) or isFrontVowel(c[pos]) then | ||
local syllable, nextPos = parseSyllable(c, pos) | local syllable, nextPos = parseSyllable(c, pos) | ||
| Line 655: | Line 599: | ||
end | end | ||
else | else | ||
-- | -- Fail safely rather than looping forever. | ||
pos = pos + 1 | pos = pos + 1 | ||
end | end | ||
else | else | ||
-- Thai punctuation/marks that aren't part of a syllable. | |||
pos = pos + 1 | pos = pos + 1 | ||
end | end | ||
| Line 673: | Line 610: | ||
local output = {} | local output = {} | ||
for i, | for i, syllable in ipairs(syllables) do | ||
if shouldHyphen(syllables[i - 1], syllable) then | |||
table.insert(output, '-') | |||
end | |||
table.insert( | |||
output, | |||
(syllable.initial or '') .. | |||
(syllable.vowel or '') .. | |||
(syllable.coda or '') | |||
) | |||
end | end | ||
| Line 694: | Line 626: | ||
end | end | ||
-- Parse the text while treating <> and <-> as hard boundaries. | |||
-- | |||
-- <> = literal space | |||
-- <-> = literal hyphen | |||
-- | |||
-- Because each Thai section is romanized independently, automatic | |||
-- hyphenation can never cross one of these markers. | |||
local function romanizeThaiText(text) | local function romanizeThaiText(text) | ||
local result = {} | local result = {} | ||
local pos = 1 | |||
local length = mw.ustring.len(text) | |||
while pos <= length do | |||
-- Manual hyphen. | |||
if mw.ustring.sub(text, pos, pos + 2) == '<->' then | |||
table.insert(result, '-') | |||
pos = pos + 3 | |||
-- Manual space. | |||
elseif mw.ustring.sub(text, pos, pos + 1) == '<>' then | |||
table.insert(result, ' ') | table.insert(result, ' ') | ||
pos = pos + 2 | |||
local | |||
-- Thai run. | |||
elseif isThai(mw.ustring.sub(text, pos, pos)) then | |||
local start = pos | |||
while pos <= length do | |||
local character = mw.ustring.sub(text, pos, pos) | |||
if not isThai(character) then | |||
break | |||
end | |||
if | -- Stop before a manual control marker. | ||
if mw.ustring.sub(text, pos, pos + 2) == '<->' then | |||
break | break | ||
end | end | ||
if | if mw.ustring.sub(text, pos, pos + 1) == '<>' then | ||
break | |||
end | end | ||
local word = | pos = pos + 1 | ||
end | |||
local word = mw.ustring.sub(text, start, pos - 1) | |||
table.insert(result, romanizeWord(word)) | |||
-- Everything else is preserved. | |||
else | |||
table.insert( | |||
result, | |||
mw.ustring.sub(text, pos, pos) | |||
) | |||
pos = pos + 1 | |||
end | end | ||
end | end | ||
| Line 740: | Line 690: | ||
end | end | ||
local function | local function convertThaiDigits(text) | ||
local digits = { | |||
end | ['๐'] = '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 | |||
return | |||
end | end | ||
local function | local function hasThai(text) | ||
return | return mw.ustring.find(text, THAI) ~= nil | ||
end | end | ||
local function validateRomanization(text) | local function validateRomanization(text) | ||
-- | -- Never allow Thai characters to leak into the final result. | ||
if | if hasThai(text) then | ||
return false | return false | ||
end | end | ||
| Line 777: | Line 735: | ||
local function capitalizeSentence(text) | local function capitalizeSentence(text) | ||
local first = mw.ustring.match(text, '^[%s%p]*( | local prefix, first = mw.ustring.match( | ||
text, | |||
'^([%s%p]*)(%a)' | |||
) | |||
if not first then | if not first then | ||
| Line 783: | Line 744: | ||
end | end | ||
return prefix .. | |||
mw.ustring.upper(first) .. | |||
mw.ustring.sub(text, mw.ustring.len(prefix) + 2) | |||
end | end | ||
| Line 819: | Line 754: | ||
end | end | ||
-- Do not automatically romanize Latin-only text. | |||
if not hasThai(text) then | if not hasThai(text) then | ||
return '' | return '' | ||
end | end | ||
text = convertThaiDigits(text) | text = convertThaiDigits(text) | ||
local result = romanizeThaiText(text) | local result = romanizeThaiText(text) | ||
result = mw.ustring.gsub(result, '%s+', ' ') | result = mw.ustring.gsub(result, '%s+', ' ') | ||
| Line 859: | Line 785: | ||
end | end | ||
if not hasThai(text) then | if not hasThai(text) then | ||
return '' | return '' | ||
| Line 879: | Line 803: | ||
end | end | ||
-- Automatically generated romanizations | -- Automatically generated romanizations get a Tt warning. | ||
return tt._main({ | return tt._main({ | ||
[1] = result, | [1] = result, | ||