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 = {
-- Longest forms first.
['เียะ'] = 'ia',
['เียะ'] = 'ia',
['เีย'] = 'ia',
['เีย'] = 'ia',
Line 116: Line 118:
['เิ็ย'] = 'oei',
['เิ็ย'] = 'oei',
['เิย'] = 'oei',
['เิย'] = 'oei',
['เย'] = 'oei',
['เอย'] = 'oei',
['เอย'] = 'oei',


['็อย'] = 'oi',
['็อย'] = 'oi',
['อย'] = 'oi',
['อย'] = 'oi',
['โย'] = 'oi',


['ุย'] = 'ui',
['ุย'] = 'ui',
Line 136: Line 136:
['เา'] = 'ao',
['เา'] = 'ao',
['าว'] = 'ao',
['าว'] = 'ao',
['เอว'] = 'oeu',
['แว'] = 'aeo',
['แว'] = 'aeo',
['เว'] = 'eo',
['เว'] = 'eo',
['โว'] = 'ou',


['ิว'] = '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 vowels_with_front = {
local consonant_set = {}
['เ'] = true,
['แ'] = true,
['โ'] = true,
['ใ'] = true,
['ไ'] = true
}


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,
['ฟร'] = true
['หน'] = true,
['หม'] = true,
['หย'] = 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
local function isTone(c)
return c == '่' or c == '้' or c == '๊' or c == '๋' or c == '์'
end
end


local function isFrontVowel(c)
local function isFrontVowel(c)
return c and vowels_with_front[c] == true
return c == 'เ' or c == 'แ' or c == 'โ' or c == 'ใ' or c == 'ไ'
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
end


local function skipMarks(chars_, pos)
local function skipMarks(c, pos)
while chars_[pos] and (
while c[pos] and (
chars_[pos] == '็' or
c[pos] == '็' or
chars_[pos] == '่' or
c[pos] == '่' or
chars_[pos] == '้' or
c[pos] == '้' or
chars_[pos] == '๊' or
c[pos] == '๊' or
chars_[pos] == '๋' or
c[pos] == '๋' or
chars_[pos] == '์' or
c[pos] == 'ํ' or
chars_[pos] == 'ํ' or
c[pos] == '๎'
chars_[pos] == '๎'
) do
) do
pos = pos + 1
pos = pos + 1
Line 353: Line 322:
end
end


local function findNextVowel(chars_, pos)
local function removeSilent(chars_)
local i = pos
local result = {}
local i = 1


while chars_[i] do
while chars_[i] do
if isFrontVowel(chars_[i]) then
if chars_[i] == '์' then
return i
-- ์ makes the following consonant(s) silent.
end
i = i + 1


for _, form in ipairs(vowel_order) do
while chars_[i] and (
if mw.ustring.sub(table.concat(chars_, '', i), 1, mw.ustring.len(form)) == form then
isConsonant(chars_[i]) or
return i
chars_[i] == '็' or
chars_[i] == '่' or
chars_[i] == '้' or
chars_[i] == '๊' or
chars_[i] == '๋'
) do
i = i + 1
end
end
end
if chars_[i] == '์' then
i = i + 2
else
else
table.insert(result, chars_[i])
i = i + 1
i = i + 1
end
end
end
end


return nil
return result
end
end


local function hasVowelAt(chars_, pos)
local function getVowel(c, pos)
local form = getVowel(chars_, pos)
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 form ~= nil
return nil, nil
end
end


local function getInitial(chars_, pos)
local function getInitial(c, pos)
local c1 = chars_[pos]
local c1 = c[pos]


if not isConsonant(c1) then
if not isConsonant(c1) then
Line 390: Line 371:
end
end


local c2 = chars_[pos + 1]
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, c1 .. c2
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
if c1 == 'ท' and c2 == 'ร' then
-- ร is silent in these clusters.
return 's', pos + 2, false, c1 .. c2
end
 
if r_silent_clusters[c1 .. c2] then
if r_silent_clusters[c1 .. c2] then
return consonants[c1], pos + 2, false, c1 .. c2
return consonants[c1], pos + 2, false
end
end


return consonants[c1] .. consonants[c2], pos + 2, false, c1 .. c2
return consonants[c1] .. consonants[c2], pos + 2, false
end
end


return consonants[c1], pos + 1, false, c1
return consonants[c1], pos + 1, false
end
end


local function getCoda(chars_, pos)
local function getCoda(c, pos)
local c = chars_[pos]
local current = c[pos]


if not c or not isConsonant(c) then
if not current or not isConsonant(current) then
return '', pos
return '', pos
end
end


if c == 'อ' or c == 'ห' or c == 'ฮ' then
-- These do not act as ordinary codas here.
if current == 'อ' or current == 'ห' or current == 'ฮ' then
return '', pos
return '', pos
end
end


if c == 'ย' or c == 'ว' then
-- ย and ว are generally part of the vowel/diphthong.
if current == 'ย' or current == 'ว' then
return '', pos
return '', pos
end
end


if finals[c] then
if finals[current] then
return finals[c], pos + 1
return finals[current], pos + 1
end
end


Line 433: Line 421:
end
end


local function removeSilent(chars_)
local function startsNextSyllable(c, pos)
local result = {}
if not c[pos] or not isConsonant(c[pos]) then
local i = 1
return false
end


while chars_[i] do
if isFrontVowel(c[pos + 1]) then
if chars_[i] == '์' then
return true
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


return result
local _, vowel = getVowel(c, pos + 1)
end
 
local function cleanWord(word)
word = mw.ustring.gsub(word, '์', '')


return word
return vowel ~= nil
end
end


local function parseSyllable(chars_, start)
local function parseSyllable(c, start)
local n = #chars_
local pos = start
local pos = start


if not chars_[pos] then
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(chars_[pos]) then
if isFrontVowel(c[pos]) then
front = chars_[pos]
front = c[pos]
pos = pos + 1
pos = pos + 1
end
end


local initial, afterInitial, silentH, cluster =
local initialPos = pos
getInitial(chars_, pos)
 
initial, pos, _, cluster =
getInitial(c, pos)


if not chars_[pos] or not isConsonant(chars_[pos]) then
if initial == '' and not isConsonant(c[initialPos]) then
return nil, start + 1
return nil, start + 1
end
end


pos = afterInitial
local vowel
 
local vowelForm
local vowelForm
local vowel


if front ~= '' then
if front ~= '' then
local rest = front
-- 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


for _, form in ipairs(vowel_order) do
elseif front == 'โ' and c[pos] == 'ะ' then
local candidate = front .. mw.ustring.sub(
vowel = 'o'
table.concat(chars_, '', pos),
vowelForm = 'โะ'
1,
pos = pos + 1
mw.ustring.len(form)
)


if vowels[candidate] then
elseif front == 'ใ' or front == 'ไ' then
vowelForm = candidate
vowel = 'ai'
vowel = vowels[candidate]
vowelForm = front
pos = pos + mw.ustring.len(form)
break
end
end


if not vowel then
else
if front == 'เ' then
vowel = vowels[front]
if chars_[pos] == 'า' then
vowelForm = front
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
end
else
else
vowelForm, vowel = getVowel(chars_, pos)
vowelForm, vowel = getVowel(c, pos)


if vowelForm then
if vowelForm then
Line 540: Line 518:
end
end


-- No written vowel: determine an implied vowel.
-- Implied vowels.
if not vowel then
if not vowel then
local nextC = chars_[pos]
if c[pos] and isConsonant(c[pos]) then
 
if startsNextSyllable(c, pos) then
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'
vowel = 'a'
else
else
-- Closed syllable with an implied o.
vowelForm = ''
vowel = 'o'
vowel = 'o'
end
end
else
else
vowelForm = ''
vowel = 'a'
vowel = 'a'
end
end
end
end


pos = skipMarks(chars_, pos)
pos = skipMarks(c, pos)


-- Final consonant.
local coda = ''
local coda = ''
local codaPos = pos
local codaPos = pos


if chars_[pos] and isConsonant(chars_[pos]) then
if c[pos] and isConsonant(c[pos]) then
local nextC = chars_[pos]
-- If another consonant begins the next syllable, don't consume
local nextNext = chars_[pos + 1]
-- it as a coda.
 
if startsNextSyllable(c, pos) then
-- 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 = ''
coda = ''
else
else
coda, codaPos = getCoda(chars_, pos)
coda, codaPos = getCoda(c, pos)
end
end
end
end


pos = codaPos
pos = codaPos
pos = skipMarks(chars_, pos)
pos = skipMarks(c, pos)


return {
return {
initial = initial,
initial = initial,
vowel = vowel,
vowel = vowel,
vowelForm = vowelForm or '',
coda = coda,
coda = coda,
next = pos,
next = pos
cluster = cluster,
silentH = silentH
}, pos
}, pos
end
end
Line 605: Line 563:
end
end


-- A hyphen is used before a syllable beginning with a vowel.
-- 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


-- A syllable ending in a vowel followed by initial ng.
-- 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)
word = cleanWord(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 not c[pos] then
if isConsonant(c[pos]) or isFrontVowel(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)
local syllable, nextPos = parseSyllable(c, pos)


Line 655: Line 599:
end
end
else
else
-- Unknown Thai structure: consume one character so that
-- Fail safely rather than looping forever.
-- the parser can never become stuck.
table.insert(syllables, {
raw = c[pos],
initial = consonants[c[pos]] or '',
vowel = 'a',
coda = ''
})
 
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, s in ipairs(syllables) do
for i, syllable in ipairs(syllables) do
if s.raw == 'ๆ' then
if shouldHyphen(syllables[i - 1], syllable) then
table.insert(output, 'ๆ')
table.insert(output, '-')
else
end
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)
table.insert(
end
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)


local parts = mw.text.split(
while pos <= length do
text,
-- Manual hyphen.
'(\1MANUALHYPHEN\1|\1MANUALSPACE\1)',
if mw.ustring.sub(text, pos, pos + 2) == '<->' then
true
table.insert(result, '-')
)
pos = pos + 3


for _, part in ipairs(parts) do
-- Manual space.
if part == '\1MANUALHYPHEN\1' then
elseif mw.ustring.sub(text, pos, pos + 1) == '<>' then
table.insert(result, '-')
elseif part == '\1MANUALSPACE\1' then
table.insert(result, ' ')
table.insert(result, ' ')
else
pos = pos + 2
local last = 1
 
-- 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)


while true do
if not isThai(character) then
local startPos, endPos =
break
mw.ustring.find(part, THAI .. '+', last)
end


if not startPos then
-- Stop before a manual control marker.
table.insert(result, mw.ustring.sub(part, last))
if mw.ustring.sub(text, pos, pos + 2) == '<->' then
break
break
end
end


if startPos > last then
if mw.ustring.sub(text, pos, pos + 1) == '<>' then
table.insert(
break
result,
mw.ustring.sub(part, last, startPos - 1)
)
end
end


local word =
pos = pos + 1
mw.ustring.sub(part, startPos, endPos)
end
 
local word = mw.ustring.sub(text, start, pos - 1)
 
table.insert(result, romanizeWord(word))


table.insert(result, romanizeWord(word))
-- Everything else is preserved.
else
table.insert(
result,
mw.ustring.sub(text, pos, pos)
)


last = endPos + 1
pos = pos + 1
end
end
end
end
end
Line 740: Line 690:
end
end


local function hasThai(text)
local function convertThaiDigits(text)
return mw.ustring.find(text, THAI) ~= nil
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


local function containsThai(text)
return text
return mw.ustring.find(text, '[ก-๛]') ~= nil
end
end


local function hasOnlyThaiOrFormatting(text)
local function hasThai(text)
return not mw.ustring.find(text, '[ก-๛]')
return mw.ustring.find(text, THAI) ~= nil
end
end


local function validateRomanization(text)
local function validateRomanization(text)
-- The automatic result must never leak Thai characters.
-- Never allow Thai characters to leak into the final result.
if containsThai(text) then
if hasThai(text) then
return false
end
 
-- Thai marks should never survive either.
if mw.ustring.find(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]*([a-z])')
local prefix, first = mw.ustring.match(
text,
'^([%s%p]*)(%a)'
)


if not first then
if not first then
Line 783: Line 744:
end
end


local upper = mw.ustring.upper(first)
return prefix ..
 
mw.ustring.upper(first) ..
return mw.ustring.gsub(
mw.ustring.sub(text, mw.ustring.len(prefix) + 2)
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
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
-- Manual spacing/hyphen controls.
text = text
:gsub('<->', '\1')
:gsub('<>', '\2')


text = convertThaiDigits(text)
text = convertThaiDigits(text)


local result = romanizeThaiText(text)
local result = romanizeThaiText(text)
-- Restore manual separators.
result = result
:gsub('\1', '-')
:gsub('\2', ' ')


result = mw.ustring.gsub(result, '%s+', ' ')
result = mw.ustring.gsub(result, '%s+', ' ')
Line 859: Line 785:
end
end


-- If there is no Thai at all, this module should not create
-- an automatic romanization.
if not hasThai(text) then
if not hasThai(text) then
return ''
return ''
Line 879: Line 803:
end
end


-- Automatically generated romanizations are deliberately marked
-- Automatically generated romanizations get a Tt warning.
-- with the tooltip from Module:Tt.
return tt._main({
return tt._main({
[1] = result,
[1] = result,