Module:Th-rm

Revision as of 17:26, 2 September 2026 by ReisuDesign (talk | contribs) (Created page with "local p = {} local U = mw.ustring local gsub = U.gsub local find = U.find local sub = U.sub local match = U.match local len = U.len local INITIAL = { ['ก'] = 'k', ['ข'] = 'kh', ['ฃ'] = 'kh', ['ค'] = 'kh', ['ฅ'] = 'kh', ['ฆ'] = 'kh', ['ง'] = 'ng', ['จ'] = 'ch', ['ฉ'] = 'ch', ['ช'] = 'ch', ['ซ'] = 's', ['ฌ'] = 'ch', ['ญ'] = 'y', ['ฎ'] = 'd', ['ฏ'] = 't', ['ฐ'] = 'th', ['ฑ'] = 'th', ['ฒ'] = 'th', ['ณ'] = 'n', ['ด'] = 'd'...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Module documentation

This module implements {{th-rm}}.

local p = {}

local U = mw.ustring
local gsub = U.gsub
local find = U.find
local sub = U.sub
local match = U.match
local len = U.len

local INITIAL = {
	['ก'] = '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 FINAL = {
	['ก'] = 'k',
	['ข'] = 'k', ['ฃ'] = 'k', ['ค'] = 'k', ['ฅ'] = 'k', ['ฆ'] = 'k',
	['ง'] = 'ng',
	['จ'] = 't', ['ฉ'] = 't', ['ช'] = 't', ['ฌ'] = '',
	['ซ'] = 't',
	['ญ'] = 'n',
	['ฎ'] = 't', ['ฏ'] = 't',
	['ฐ'] = 't', ['ฑ'] = 't', ['ฒ'] = 't',
	['ณ'] = 'n',
	['ด'] = 't',
	['ต'] = 't',
	['ถ'] = 't', ['ท'] = 't', ['ธ'] = 't',
	['น'] = 'n',
	['บ'] = 'p',
	['ป'] = 'p',
	['ผ'] = '', ['ฝ'] = '',
	['พ'] = 'p', ['ฟ'] = 'p', ['ภ'] = 'p',
	['ม'] = 'm',
	['ย'] = '',
	['ร'] = 'n',
	['ล'] = 'n',
	['ว'] = '',
	['ศ'] = 't', ['ษ'] = 't', ['ส'] = 't',
	['ห'] = '',
	['ฬ'] = 'n',
	['ฮ'] = ''
}

-- Vowel patterns are tested longest-first.
local VOWELS = {
	{ 'เ' , 'ะ', 'e' },
	{ 'เ' , '็', 'e' },
	{ 'เ' , '',  'e' },

	{ 'แ' , 'ะ', 'ae' },
	{ 'แ' , '',  'ae' },

	{ 'โ' , 'ะ', 'o' },
	{ 'โ' , '',  'o' },
	{ 'เ' , 'าะ', 'o' },
	{ ''  , 'อ', 'o' },

	{ 'เ' , 'อะ', 'oe' },
	{ 'เ' , 'ิ', 'oe' },
	{ 'เ' , 'อ', 'oe' },

	{ 'เ' , 'ียะ', 'ia' },
	{ 'เ' , 'ีย', 'ia' },

	{ 'เ' , 'ือะ', 'uea' },
	{ 'เ' , 'ือ', 'uea' },

	{ ''  , 'ัวะ', 'ua' },
	{ ''  , 'ัว', 'ua' },
	{ ''  , 'ว', 'ua' },

	{ 'ใ', '', 'ai' },
	{ 'ไ', '', 'ai' },
	{ '' , 'ัย', 'ai' },
	{ 'ไ', 'ย', 'ai' },
	{ '' , 'าย', 'ai' },

	{ 'เ', 'า', 'ao' },
	{ '' , 'าว', 'ao' },

	{ '' , 'ุย', 'ui' },

	{ 'โ', 'ย', 'oi' },
	{ '' , 'อย', 'oi' },

	{ 'เ', 'ย', 'oei' },
	{ 'เ', 'ือย', 'ueai' },
	{ '' , 'วย', 'uai' },

	{ '' , 'ิว', 'io' },

	{ 'เ', '็ว', 'eo' },
	{ 'เ', 'ว', 'eo' },

	{ 'แ', '็ว', 'aeo' },
	{ 'แ', 'ว', 'aeo' },

	{ 'เ', 'ียว', 'iao' },

	{ '' , 'ะ', 'a' },
	{ '' , 'ั', 'a' },
	{ '' , 'า', 'a' },
	{ '' , 'ำ', 'am' },

	{ '' , 'ิ', 'i' },
	{ '' , 'ี', 'i' },

	{ '' , 'ึ', 'ue' },
	{ '' , 'ื', 'ue' },
	{ '' , 'ือ', 'ue' },

	{ '' , 'ุ', 'u' },
	{ '' , 'ู', 'u' },

	{ '' , 'รร', 'a' }
}

local function isConsonant(c)
	return c and INITIAL[c] ~= nil
end

local function isThai(c)
	return c and match(c, '[ก-๛]') ~= nil
end

local function isToneMark(c)
	return c and match(c, '[่้๊๋]') ~= nil
end

local function isSilentMark(c)
	return c == '์'
end

local function clean(str)
	str = mw.text.unstrip(str or '')
	str = gsub(str, '<br%s*/?>', ' ')
	str = gsub(str, '<[^>]+>', '')
	return str
end

local function applyOrthographicExceptions(str)
	-- Silent letters marked with ์.
	-- Remove the marked letter and its ์ marker.
	str = gsub(str, '([ก-ฮ])์', '')

	-- Tone marks have no RTGS representation.
	str = gsub(str, '[่้๊๋]', '')

	-- ฤ
	str = gsub(str, 'ฤกษ์', 'roe')
	str = gsub(str, 'ฤิ', 'ri')
	str = gsub(str, 'ฤ', 'rue')
	str = gsub(str, 'ฤๅ', 'rue')

	-- ฦ / ฦๅ
	str = gsub(str, 'ฦๅ', 'lue')
	str = gsub(str, 'ฦ', 'lue')

	return str
end

local function getVowel(str, pos)
	local remaining = sub(str, pos, -1)

	-- Explicit special vowel patterns.
	local patterns = {
		{ 'เ', 'ื', 'อ', 'ย', 'ueai' },
		{ 'เ', 'ี', 'ย', 'ว', 'iao' },
		{ 'เ', 'ี', 'ย', 'ะ', 'ia' },
		{ 'เ', 'ี', 'ย', '',  'ia' },
		{ 'เ', 'ื', 'อ', 'ะ', 'uea' },
		{ 'เ', 'ื', 'อ', '',  'uea' },

		{ 'เ', '็', 'ว', '', 'eo' },
		{ 'เ', 'ว', '', '', 'eo' },
		{ 'แ', '็', 'ว', '', 'aeo' },
		{ 'แ', 'ว', '', '', 'aeo' },

		{ 'โ', 'ย', '', '', 'oi' },
		{ '', 'อ', 'ย', '', 'oi' },
		{ 'เ', 'ย', '', '', 'oei' },

		{ '', 'ว', 'ย', '', 'uai' },
		{ '', 'ุ', 'ย', '', 'ui' },
		{ '', 'ิ', 'ว', '', 'io' },

		{ 'เ', 'า', '', '', 'ao' },
		{ '', 'า', 'ว', '', 'ao' },

		{ '', 'ั', 'ว', 'ะ', 'ua' },
		{ '', 'ั', 'ว', '', 'ua' },
		{ '', 'ว', '', '', 'ua' },

		{ 'ใ', '', '', '', 'ai' },
		{ 'ไ', '', '', '', 'ai' },
		{ '', 'ั', 'ย', '', 'ai' },
		{ 'ไ', 'ย', '', '', 'ai' },
		{ '', 'า', 'ย', '', 'ai' },

		{ 'เ', 'า', 'ะ', '', 'ao' },
		{ 'โ', 'ะ', '', '', 'o' },
		{ 'เ', 'า', '', '', 'ao' },
		{ 'เ', 'าะ', '', '', 'o' },

		{ 'เ', 'อ', '', '', 'oe' },
		{ 'เ', 'ิ', '', '', 'oe' },
		{ 'เ', 'อะ', '', '', 'oe' },

		{ 'แ', 'ะ', '', '', 'ae' },
		{ 'แ', '', '', '', 'ae' },
		{ 'เ', 'ะ', '', '', 'e' },
		{ 'เ', '็', '', '', 'e' },
		{ 'เ', '', '', '', 'e' },
		{ 'โ', '', '', '', 'o' },

		{ '', 'ำ', '', '', 'am' },
		{ '', 'ะ', '', '', 'a' },
		{ '', 'ั', '', '', 'a' },
		{ '', 'า', '', '', 'a' },
		{ '', 'ิ', '', '', 'i' },
		{ '', 'ี', '', '', 'i' },
		{ '', 'ึ', '', '', 'ue' },
		{ '', 'ือ', '', '', 'ue' },
		{ '', 'ื', '', '', 'ue' },
		{ '', 'ุ', '', '', 'u' },
		{ '', 'ู', '', '', 'u' },
		{ '', 'อ', '', '', 'o' }
	}

	for _, v in ipairs(patterns) do
		local a, b, c, d, roman = v
		local pattern = a .. b .. c .. d
		if pattern ~= '' and sub(remaining, 1, len(pattern)) == pattern then
			return roman, len(pattern)
		end
	end

	-- Implied a.
	return 'a', 0
end

local function nextVowelPosition(str, pos)
	local i = pos
	while i <= len(str) do
		local c = sub(str, i, i)

		if c == 'เ' or c == 'แ' or c == 'โ' or c == 'ใ' or c == 'ไ' then
			return i
		end

		if match(c, '[ะาิีึืุูัำอ]') then
			return i
		end

		i = i + 1
	end

	return nil
end

local function romanizeSyllable(str, startPos)
	local pos = startPos
	local n = len(str)

	-- Preposed vowel.
	local preVowel = ''
	local preLen = 0
	local c = sub(str, pos, pos)

	if c == 'เ' or c == 'แ' or c == 'โ' or c == 'ใ' or c == 'ไ' then
		preVowel = c
		preLen = 1
		pos = pos + 1
	end

	-- Initial consonant.
	local initial = sub(str, pos, pos)

	if not isConsonant(initial) then
		-- Non-Thai character.
		return initial, 1
	end

	pos = pos + 1
	local initialRom = INITIAL[initial] or ''

	-- ห- exception.
	if initial == 'ห' then
		local second = sub(str, pos, pos)
		if second and match(second, '[งนมยรลวญณฬ]') then
			initialRom = INITIAL[second]
			pos = pos + 1
			initial = second
		end
	end

	-- Consonant clusters where ร is not represented.
	if (initial == 'จ' or initial == 'ซ' or initial == 'ศ' or initial == 'ส')
		and sub(str, pos, pos) == 'ร' then
		initialRom = INITIAL[initial]
		pos = pos + 1
	end

	-- Find the vowel from the current position, including preposed vowels.
	local vowelStart = startPos + preLen + 1
	local vowelRoman
	local vowelLen

	if preLen > 0 then
		vowelRoman, vowelLen = getVowel(str, startPos + 1)
	else
		vowelRoman, vowelLen = getVowel(str, pos)
	end

	-- If the vowel was explicitly consumed by getVowel, advance.
	if vowelLen > 0 then
		if preLen > 0 then
			pos = startPos + preLen + 1 + vowelLen
		else
			pos = pos + vowelLen
		end
	end

	-- Collect possible final consonant.
	local final = sub(str, pos, pos)
	local finalRom = ''

	if isConsonant(final) then
		local after = sub(str, pos + 1, pos + 1)

		-- A consonant followed by a vowel-starting syllable is normally
		-- treated as the next syllable's initial rather than this syllable's final.
		local nextIsVowel = after and (
			match(after, '[ะาิีึืุูัำอ]') or
			match(after, '[เแโใไ]')
		)

		if not nextIsVowel then
			finalRom = FINAL[final] or ''
			pos = pos + 1

			-- -ร following another final consonant is silent in RTGS.
			if final == 'ร' then
				finalRom = 'n'
			end
		end
	end

	-- ทร- exception.
	if initial == 'ท' and sub(str, pos, pos) == 'ร' then
		initialRom = 's'
		pos = pos + 1
	end

	return initialRom .. vowelRoman .. finalRom, math.max(1, pos - startPos)
end

local function romanizeWord(word)
	if word == '' then
		return ''
	end

	-- Manual markers:
	-- < > = force space
	-- <-> = force hyphen
	word = gsub(word, '< >', ' ')
	word = gsub(word, '<->', '-')

	local output = {}
	local pos = 1
	local n = len(word)

	while pos <= n do
		local c = sub(word, pos, pos)

		if c == '<' then
			local close = find(word, '>', pos + 1, true)
			if close then
				pos = close + 1
			else
				table.insert(output, c)
				pos = pos + 1
			end
		elseif not isThai(c) then
			table.insert(output, c)
			pos = pos + 1
		else
			local roman, consumed = romanizeSyllable(word, pos)
			table.insert(output, roman)
			pos = pos + consumed
		end
	end

	return table.concat(output)
end

local function capitalize(str, sentence)
	local chars = mw.text.split(str, '')
	local shouldCap = true

	for i, c in ipairs(chars) do
		if match(c, '%a') then
			if shouldCap then
				chars[i] = U.upper(c)
				shouldCap = false
			elseif sentence then
				shouldCap = false
			end
		elseif c == ' ' then
			if sentence then
				shouldCap = true
			else
				shouldCap = true
			end
		elseif match(c, '[%.!?]') then
			shouldCap = true
		end
	end

	return table.concat(chars)
end

function p.main(frame)
	local args = require('Module:Arguments').getArgs(frame, {
		parentFirst = true,
		wrappers = {'Template:Th-rm'}
	})

	if not args[1] or args[1] == '' then
		return ''
	end

	local str = clean(args[1])

	-- Don't romanize text which contains no Thai script.
	if not match(str, '[ก-๛]') then
		return ''
	end

	str = applyOrthographicExceptions(str)

	local result = romanizeWord(str)

	if args.capi then
		result = capitalize(result, false)
	elseif args.sent then
		result = capitalize(result, true)
	end

	return result
end

return p