Module:Th-rm

Revision as of 17:27, 2 September 2026 by ReisuDesign (talk | contribs)
Module documentation

This module implements {{th-rm}}.

local p = {}

local U = mw.ustring
local gsub = U.gsub
local match = U.match
local sub = U.sub
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',
	['ฮ'] = ''
}

local CONSONANTS = 'กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรลวศษสหฬฮ'

local function isConsonant(c)
	return c and match(c, '[' .. CONSONANTS .. ']') ~= nil
end

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

local function isVowelMark(c)
	return c and match(c, '[ะาิีึืุูัำเแโใไอ]') ~= nil
end

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

local function removeMarks(str)
	-- Tone marks are not represented in RTGS.
	str = gsub(str, '[่้๊๋]', '')

	-- Mai thanthakhat makes the marked consonant silent.
	str = gsub(str, '([ก-ฮ])์', '')

	return str
end

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

	-- Longest / most specific patterns first.
	local patterns = {
		{ 'เ', 'ื', 'อ', 'ย', 'ueai' },
		{ 'เ', 'ี', 'ย', 'ว', 'iao' },
		{ 'เ', 'ี', 'ย', 'ะ', 'ia' },
		{ 'เ', 'ี', 'ย', '', 'ia' },
		{ 'เ', 'ื', 'อ', 'ะ', 'uea' },
		{ 'เ', 'ื', 'อ', '', 'uea' },

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

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

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

		{ 'เ', 'ื', 'อ', '', 'ue' },
		{ 'เ', 'ื', '', '', 'ue' },

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

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

		{ 'เ', 'ย', '', '', 'oei' },

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

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

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

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

		{ 'เ', 'ะ', '', '', 'e' },
		{ 'เ', '็', '', '', 'e' },
		{ 'เ', '', '', '', 'e' },

		{ '', 'ำ', '', '', 'am' },

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

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

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

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

	for _, v in ipairs(patterns) do
		local a = v[1] or ''
		local b = v[2] or ''
		local c = v[3] or ''
		local d = v[4] or ''
		local roman = v[5] or ''

		local pattern = a .. b .. c .. d

		if pattern ~= '' and sub(s, 1, len(pattern)) == pattern then
			return roman, len(pattern)
		end
	end

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

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

	local initial = ''
	local initialRom = ''

	-- Preposed vowels are written before the initial consonant.
	local pre = sub(str, pos, pos)

	if pre == 'เ' or pre == 'แ' or pre == 'โ' or pre == 'ใ' or pre == 'ไ' then
		pos = pos + 1
	end

	local first = sub(str, pos, pos)

	if not isConsonant(first) then
		return first, 1
	end

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

	-- ห + sonorant consonant: ห is not romanized.
	local second = sub(str, pos, pos)

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

	-- จร-, ซร-, ศร-, สร-: ร is not romanized.
	second = sub(str, pos, pos)

	if second == 'ร'
		and (initial == 'จ' or initial == 'ซ' or initial == 'ศ' or initial == 'ส') then
		pos = pos + 1
	end

	-- Read the vowel.
	local vowelPos

	if pre == 'เ' or pre == 'แ' or pre == 'โ' or pre == 'ใ' or pre == 'ไ' then
		vowelPos = start
	else
		vowelPos = pos
	end

	local vowel, vowelLen = getVowel(str, vowelPos)

	-- Advance over the vowel.
	if vowelLen > 0 then
		pos = vowelPos + vowelLen
	end

	-- If there was a preposed vowel, getVowel consumed from its position,
	-- so the resulting position already includes it.
	if vowelLen == 0 and (pre == 'เ' or pre == 'แ' or pre == 'โ' or pre == 'ใ' or pre == 'ไ') then
		pos = start + 1
	end

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

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

		-- If another vowel begins immediately afterward, this consonant
		-- belongs to the following syllable.
		if not isVowelMark(after) and not (after == 'เ' or after == 'แ' or after == 'โ' or after == 'ใ' or after == 'ไ') then
			finalRom = FINAL[final] or ''
			pos = pos + 1

			-- Final -ร after another final consonant.
			if final == 'ร' then
				finalRom = 'n'
			end
		end
	end

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

	-- -รร-
	-- With a final consonant, รร = a.
	-- Without a final consonant, รร = an.
	if sub(str, vowelPos, vowelPos + 1) == 'รร' then
		if finalRom ~= '' then
			vowel = 'a'
		else
			vowel = 'an'
		end
	end

	return initialRom .. vowel .. finalRom, math.max(1, pos - start)
end

local function romanizeWord(word)
	local result = {}
	local pos = 1
	local n = len(word)

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

		if c == '<' then
			local close = mw.ustring.find(word, '>', pos + 1, true)

			if close then
				local marker = sub(word, pos, close)

				if marker == '< >' then
					table.insert(result, ' ')
				elseif marker == '<->' then
					table.insert(result, '-')
				else
					table.insert(result, marker)
				end

				pos = close + 1
			else
				table.insert(result, c)
				pos = pos + 1
			end
		elseif isThai(c) then
			local roman, consumed = romanizeSyllable(word, pos)
			table.insert(result, roman)
			pos = pos + consumed
		else
			table.insert(result, c)
			pos = pos + 1
		end
	end

	return table.concat(result)
end

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

	for i, c in ipairs(chars) do
		if match(c, '[A-Za-z]') then
			if capitalize then
				chars[i] = U.upper(c)
				capitalize = false
			end
		elseif c == ' ' or c == '-' then
			capitalize = true
		end
	end

	return table.concat(chars)
end

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

	for i, c in ipairs(chars) do
		if match(c, '[A-Za-z]') then
			if capitalize then
				chars[i] = U.upper(c)
				capitalize = false
			end
		elseif match(c, '[.!?]') then
			capitalize = 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])

	-- Nothing to romanize.
	if not match(str, '[ก-๛]') then
		return ''
	end

	str = removeMarks(str)

	local result = romanizeWord(str)

	if args.capi then
		result = capitalizeWords(result)
	elseif args.sent then
		result = sentenceCase(result)
	end

	return result
end

return p