Module:sa-utilities

From Wiktionary, the free dictionary
Jump to navigation Jump to search

These are the rules concerning transliteration in Sanskrit entries.

Transliteration

Wiktionary uses two transliteration systems for Devanāgarī.

  • In the mainspace, only IAST is used.
  • In some modules, SLP1 is used for convenience of encoding because of its one-to-one encoding with Devanāgarī.

See WT:About Sanskrit.

Vowels
Deva IAST SLP1 Deva IAST SLP1
अ, प a a आ, पा ā A
इ, पि i i ई, पी ī I
उ, पु u u ऊ, पू ū U
ए, पे e e ऐ, पै ai E
ओ, पो o o औ, पौ au O
ऋ, पृ f ॠ, पॄ F
ऌ, पॢ x ॡ, पॣ X
Consonants
Deva IAST SLP1 Deva IAST SLP1 Deva IAST SLP1 Deva IAST SLP1 Deva IAST SLP1
ka ka ca ca ṭa wa ta ta pa pa
kha Ka cha Ca ṭha Wa tha Ta pha Pa
ga ga ja ja ḍa qa da da ba ba
gha Ga jha Ja ḍha Qa dha Da bha Ba
ṅa Na ña Ya ṇa Ra na na ma ma
ya ya ra ra ḷa La la la va va
ha ha śa Sa ṣa za sa sa
Other symbols
Deva IAST SLP1 Deva IAST SLP1
अं, पं M अँ, पँ ~
अः, पः H '
x Z f V
. . . .
Numerals
Deva
IAST & SLP1 0 1 2 3 4 5 6 7 8 9

local export = {}

-- Common regex patterns:
export.consonant_list = "kKgGNcCjJYwWqQRtTdDnpPbBmyrlLvSzsh"
export.consonant = "[" .. export.consonant_list .. "]"
export.accent = "[/\\]"
export.vowel_list = "aAiIuUfFxXeEoO"
export.vowel = "[" .. export.vowel_list .. "]"
export.vowel_with_accent = export.vowel .. export.accent .. "?"
export.stop_list = "kKgGcCjJwWqQtTdDpPbB"
export.stop = "[" .. export.stop_list .. "]"

-- Abbreviated helper functions:
local match = mw.ustring.match
local gsub = mw.ustring.gsub
local gasub = string.gsub -- All we need for SLP1!  Much faster than mw.ustring.gsub
local sub = mw.ustring.sub
local lower = mw.ustring.lower
local upper = mw.ustring.upper

--[=[Detects whether a specified text ends in a given pattern.
	Parameters:
		text (String): the text to be tested
		pattern (String): the query pattern to be tested on the end of the text.
	Return:
		Boolean
]=]
local function ends_with(text, pattern)
	return match(text, pattern .. "$")
end

--[=[Detects whether a specified text begins in a given pattern.
	Parameters:
		text (String): the text to be tested
		pattern (String): the query pattern to be tested on the beginning of the text.
	Return:
		Boolean
]=]
local function starts_with(text, pattern)
	return match(text, "^" .. pattern)
end

-- Common transformation types:
--[=[ Increase a vowel one grade (guṇation). It is possible that this should
		include provisions for guṇation of sonorants into CV configurations
		(e.g. i/ī -> ya -> yā, etc.). Perhaps will need to be updated.
]=]
export.up_one_grade = {
	['a'] = 'A', ['A'] = 'A', ['a/'] = 'A/', ['A/'] = 'A/', ['a\\'] = 'A\\', ['A\\'] = 'A\\',
	['i'] = 'e', ['I'] = 'e', ['i/'] = 'e/', ['I/'] = 'e/', ['i\\'] = 'e\\', ['I\\'] = 'e\\',
	['u'] = 'o', ['U'] = 'o', ['u/'] = 'o/', ['U/'] = 'o/', ['u\\'] = 'o\\', ['U\\'] = 'o\\',
	['e'] = 'E', ['E'] = 'E', ['e/'] = 'E/', ['E/'] = 'E/', ['e\\'] = 'E\\', ['E\\'] = 'E\\',
	['o'] = 'O', ['O'] = 'O', ['o/'] = 'O/', ['O/'] = 'O/', ['o\\'] = 'O\\', ['O\\'] = 'O\\',
	['f'] = 'ar', ['F'] = 'ar', ['f/'] = 'a/r', ['F/'] = 'a/r', ['f\\'] = 'a\\r', ['F\\'] = 'a\\r',
	['x'] = 'al', ['x/'] = 'a/l',
}

-- Lengthen a vowel
export.lengthen = {
	['a'] = 'A', ['A'] = 'A', ['a/'] = 'A/', ['A/'] = 'A/', ['a\\'] = 'A\\', ['A\\'] = 'A\\',
	['i'] = 'I', ['I'] = 'I', ['i/'] = 'I/', ['I/'] = 'I/', ['i\\'] = 'I\\', ['I\\'] = 'I\\',
	['u'] = 'U', ['U'] = 'U', ['u/'] = 'U/', ['U/'] = 'U/', ['u\\'] = 'U\\', ['U\\'] = 'U\\',
	['f'] = 'F', ['F'] = 'F', ['f/'] = 'F/', ['F/'] = 'F/', ['f\\'] = 'F\\', ['F\\'] = 'F\\',
}

-- Decrease vowel one grade (reverse of above)
export.shorten = {
	['a'] = 'a', ['A'] = 'a', ['a/'] = 'a/', ['A/'] = 'a/', ['a\\'] = 'a\\', ['A\\'] = 'a\\',
	['i'] = 'i', ['I'] = 'i', ['i/'] = 'i/', ['I/'] = 'i/', ['i\\'] = 'i\\', ['I\\'] = 'i\\',
	['u'] = 'u', ['U'] = 'u', ['u/'] = 'u/', ['U/'] = 'u/', ['u\\'] = 'u\\', ['U\\'] = 'u\\',
	['f'] = 'f', ['F'] = 'f', ['f/'] = 'f/', ['F/'] = 'f/', ['f\\'] = 'f\\', ['F\\'] = 'f\\',
}

-- Convert a monosegmental (or at least monoliteral) diphthong into a/ā + glide.
export.split_diphthong = {
	['e'] = 'ay', ['e/'] = 'a/y', ['e\\'] = 'a\\y',
	['E'] = 'Ay', ['E/'] = 'A/y', ['E\\'] = 'A\\y',
	['o'] = 'av', ['o/'] = 'a/v', ['o\\'] = 'a\\v',
	['O'] = 'Av', ['O/'] = 'A/v', ['O\\'] = 'A\\v',
}

-- reverse of above
local join_diphthong = {
	['ay'] = 'e', ['a/y'] = 'e/', ['a\\y'] = 'e\\',
	['Ay'] = 'E', ['A/y'] = 'E/', ['A\\y'] = 'E\\',
	['av'] = 'o', ['a/v'] = 'o/', ['a\\v'] = 'o\\',
	['Av'] = 'O', ['A/v'] = 'O/', ['A\\v'] = 'O\\',
}

-- Convert a syllabic sonorant to its associated consonantal form.	
export.vowel_to_cons = {
	['i'] = 'y', ['I'] = 'y',
	['u'] = 'v', ['U'] = 'v',
	['f'] = 'r', ['F'] = 'r',
	['x'] = 'l', ['X'] = 'l',
}

local cons_to_vowel = { ['y'] = 'i', ['r'] = 'f', ['v'] = 'u', ['l'] = 'x' }

-- Add a homorganic glide to a vowel
local insert_glide = {
	['i'] = 'iy', ['I'] = 'iy', ['i/'] = 'i/y', ['I/'] = 'i/y', ['i\\'] = 'i\\y', ['I\\'] = 'i\\y',
	['u'] = 'uv', ['U'] = 'uv', ['u/'] = 'u/v', ['U/'] = 'u/v', ['u\\'] = 'u\\v', ['U\\'] = 'u\\v',
}

--[=[Convert all unambiguous stops to their absolute final value. The equivalent
	values (e.g. k = k) may be redundant given the implementation of 
	absolute_final and internal_sandhi below. 
]=]
export.to_final = {
	['k'] = 'k', ['K'] = 'k', ['g'] = 'k', ['G'] = 'k',
	['c'] = 'k', ['C'] = 'w',			   ['J'] = 'w', -- value for 'J' is theoretical
	['w'] = 'w', ['W'] = 'w', ['q'] = 'w', ['Q'] = 'w', ['L'] = 'w',
	['t'] = 't', ['T'] = 't', ['d'] = 't', ['D'] = 't',
	['p'] = 'p', ['P'] = 'p', ['b'] = 'p', ['B'] = 'p',
	['Y'] = 'N', ['z'] = 'w',
}

-- Convert dental to retroflex
local function dental_to_retroflex(str)
	local changes = {
		['t'] = 'w', ['T'] = 'W', ['d'] = 'q', ['D'] = 'Q', ['n'] = 'R',
	}
	return gasub(str, '.', changes)
end

-- Remove aspiration
export.deaspirate = {
	['K'] = 'k', ['G'] = 'g',
	['C'] = 'c', ['J'] = 'j',
	['W'] = 'w', ['Q'] = 'q',
	['T'] = 't', ['D'] = 'd',
	['P'] = 'p', ['B'] = 'b',
}

export.aspirate = {
	['g'] = 'G', ['j'] = 'J', ['q'] = 'Q', ['d'] = 'D', ['b'] = 'B'
}

export.to_voiced = {
	['k'] = 'g', ['c'] = 'j', ['w'] = 'q', ['t'] = 'd', ['p'] = 'b'
}

export.homorganic_nasal = {
	['k'] = 'N', ['K'] = 'N', ['g'] = 'N', ['G'] = 'N',
	['c'] = 'Y', ['C'] = 'Y', ['j'] = 'Y', ['J'] = 'Y',
	['w'] = 'R', ['W'] = 'R', ['q'] = 'R', ['Q'] = 'R',
	['t'] = 'n', ['T'] = 'n', ['d'] = 'n', ['D'] = 'n',
	['p'] = 'm', ['P'] = 'm', ['b'] = 'm', ['B'] = 'm',
	['r'] = 'M', ['l'] = 'M', -- or 'l~'
	['S'] = 'M', ['z'] = 'M', ['s'] = 'M', ['h'] = 'M'
}

--[=[Detects whether a word is monosyllabic. This function does not apply sandhi
		to determing whether a potential phonemic form like /dā́rv/ would be
		syllabified to [dā́ru]. This might need to be changed.
	Parameters:
		text (String): the text to be checked for monosyllabicity
	Return:
		Boolean
]=]
function export.is_monosyllabic(text)
	return match(text, "^" .. export.consonant .. "*" .. export.vowel_with_accent .. "[HM~]?" .. export.consonant .. "*$")
end

--[=[Transforms a word to its absolute final sandhi form.
	Parameters:
		text (String): the text to be converted to final sandhi position
		ambig_final (String): an indication of what outcome j/ś/h should have as
			these will unpredictably produce either a retroflex or a velar
			stop in final position (e.g. spáś- > spáṭ 'spy' vs. náś- > nák 'night').
			Required for j/ś/h-final strings.
	Return:
		String
]=]
local function absolute_final(text, ambig_final, j_to_z, h_to_g)
	if ends_with(text, "kz") then -- final -kṣ > ṭ
		text = gasub(text, "..$", "w")
	-- final r + stop is allowed in stem, see Whitney §150b (endings -s/-t after consonant have been eliminated previously)
	elseif ends_with(text, export.vowel_with_accent .. "r[" .. export.stop_list .. "SzhL]") then 
		-- do nothing
	elseif ends_with(text, export.consonant .. export.consonant) then -- at least 2 consonants
		-- Take the first of the cluster.
		text = gasub(text, "(" .. export.consonant .. "+)$",
			function(cluster) return sub(cluster, 1, 1) end)
	end
	-- ḷh, v, and y are not handled as they should not appear finally. Perhaps wrong.
	if ambig_final then -- in case of -j/ś/h, but also to override the normal value in a few exceptional cases
		text = gasub(text, ".$", ambig_final)
	elseif ends_with(text, "[kwtpNRnmlaAiIuUeEoOfFxXH][/\\]?") then
		-- do nothing
	elseif ends_with(text, "M") then -- just in case, ṃ > m
		text = gasub(text, ".$", "m")
	elseif ends_with(text, "[sr]") then -- convert to final visarga
		text = gasub(text, ".$", "H")
	elseif ends_with(text, "[KgGcCJWqQTdDPbBYzL]") then -- Handle final stops.
		text = gasub(text, ".$", export.to_final)
	elseif ends_with(text, "[jhS]") then -- see also ambig_final above
		if j_to_z then
			text = gasub(text, "j$", "w")
		elseif h_to_g then
			text = gasub(text, "h$", "k")
		else 
			text = gasub(text, ".$", {['j'] = 'k', ['h'] = 'w', ['S'] = 'w'}) -- most common values
		end
	end
	return text
end

--[=[Applying retroflexion to a stem and ending without joining them.
		This include RUKI, ṣ-cluster harmony, and nasal retroflexions.
	Parameters:
		stem (String): the stem to receive an ending
		ending (String): the ending to be affixed
	Return:
		String (stem), String (ending)
]=]
function export.retroflexion(stem, ending, no_retroflex_root_s)
	-- Does the stem end in a RUKI environment?
	if ends_with(stem, "[iIeEfFxuUoOrk][/\\]?[HM]?") then
		ending = gasub(ending, "^s([^rfF])", "z%1")			-- Convert ending-initial s > ṣ not followed by [rṛṝ].
		ending = gasub(ending, "^z[tTdDn]*", dental_to_retroflex)
	end
	-- Does the stem end in a RUKI environment followed by s and the ending not start with [rṛṝ]?
	if ends_with(stem, "[iIeEfFxuUoOrk][/\\]?[HM]?s") and starts_with(ending, "[^rfF]") 
	and not no_retroflex_root_s == true then
		stem = gasub(stem, "s$", "z")						-- Convert stem-final s > ṣ
	end
	if ends_with(stem, "[zqwR]") then	-- Does the stem end in a retroflex?
		-- Convert an ending-initial dental (cluster) to retroflex
		ending = gasub(ending, "^[tTdDn]*", dental_to_retroflex)
	end
	-- Does the stem contain a nasal harmony trigger without intervening blockers?
	if ends_with(stem, "[zrfF][^cCjJYwWqQRtTdDnSsl]*") then
		 -- Convert all retroflexable n > ṇ in the ending
		ending = gasub(ending, "^([^cCjJYwWqQRtTdDnSsl]*)n([aAiIeEfFxuUoOynmv])", "%1R%2")
	end
	-- Does the stem contain a nasal harmony trigger without intervening blockers and stem-final n?
	if ends_with(stem, "[zrfF][^cCjJYwWqQRtTdDnSsl]*n") and starts_with(ending, "[aAiIeEfFxuUoOynmv]") then
		stem = gasub(stem, "n$", "R")	-- Convert stem-final n > ṇ
	end
	-- For safety, does the ending contain an unblocked nasal harmony trigger and un retroflexed n?
	ending = gasub(ending,
		"([zrfF][^cCjJYwWqQRtTdDnSsl]*)n([aAiIeEfFxuUoOynmv])", "%1R%2")
	return stem, ending
end

--[=[Combine a stem and ending while modifiying the accentuation. This does not currently
		account for mobility of accent between the stem and ending.
	Parameters:
		stem (String): the stem to receive an ending
		ending (String): the ending to be affixed
		has_accent (Boolean): whether the word has an accent to be modified at all
		accent_override (Boolean): whether to strip the stem of accent
		mono (Boolean): whether the stem is monosyllabic AND susceptible to 
			accentual mobility (cf. pā́dam ~ padā́ vs. gā́m ~ gávā). This should
			perhaps be renamed or removed as redundant to accent_override in this
			function. This should not be applied to *all* monosyllabic nouns!
		recessive (Boolean): whether the accent must be moved to the leftmost vowel (e.g. in the vocative)
	Return:
		String
]=]
local function combine_accent(stem, ending, has_accent, accent_override, mono, recessive)
	if has_accent then
		if recessive then
			local combined = stem .. ending				 -- combine word
			combined = gasub(combined, export.accent, "") -- remove any accent
			combined = gasub(combined, "^([^" .. export.vowel_list .. "]-)(" .. export.vowel .. ")", "%1%2/") -- accent first vowel of combined form
			return combined
		elseif accent_override then
			stem = gasub(stem, export.accent, "")		-- remove all accents from stem
		elseif mono and match(ending, export.accent) then
			stem = gasub(stem, export.accent, "")		-- remove all accents from stem
			ending = gasub(ending, "\\", "/")			-- convert svarita to udatta on ending
		-- If both the stem and ending are accented, remove the ending accent. This may be too simple.
		elseif match(stem, export.accent) and match(ending, export.accent) then
			ending = gasub(ending, export.accent, "")
		end
	end
	return stem .. ending
end

local function aspirate_diaspirate(stem)
	stem = gasub(stem, "(.*)([gdb])([^gdb]+[gdb]?)$", function(pre, cons, post) return (pre .. export.aspirate[cons]) .. post end)
	return stem
end

--[=[Return a word-stem combined with a given ending while handling internal
		sandhi and accentuation. Please see the implementation for details.
	Parameters:
		input_table (Table): This table is expected to contain the items:
			stem (String): the stem to receive an ending
			ending (String): the ending to be affixed
			has_accent (Boolean): whether the word has an accent to be modified
			mono (Boolean): whether the stem is monosyllabic OR behaves like a monosyllable (e.g. root noun compounds)
			accent_override (Boolean): whether to strip the stem of accent since
				some non-monosyllabic forms (e.g. present participles and gen.pl.)
				may show stem-to-ending accentual mobility.
			recessive (Boolean): whether the accent must be moved to the leftmost vowel (e.g. in the vocative)
			j_to_z (Boolean): whether to convert j > ṣ before {t, th} instead of j > k
			h_to_g (Boolean): whether to convert h > g before {t, th, d, dh} (duh + -tá = dugdhá) instead of h + t > ḍh (lih + -tá = līḍhá)
			non_final (Boolean): if true, will not apply word-final sandhi (i.e. convert s to visarga)
	Return:
		String
]=]
function export.internal_sandhi(input_table)
	local stem, ending = input_table.stem, input_table.ending
	local last		-- last segment of the stem
	local acc		-- the accent					
	local first		-- the first segment of the ending
	local combined	-- the combined form
	-- explicitly ignored are CV, C + semivowel, or C + nasal
	if starts_with(ending, export.consonant) then
		-- {i, u} > {ī, ū} /__[rs]C if stem is monosyllabic (e.g. gir, pur  +  ā-śis)
		if ends_with(stem, "[iu][/\\]?[rs]") and input_table.root then
			stem = gasub(stem, "([iu][/\\]?)([rs])$", function(vow, cons) return export.lengthen[vow] .. cons end)
		-- consonant + y/r/v + consonant from ending
		-- note that sequences like 'trv' can occur, e.g. śatrvos, gen.dual of śatru, so the case ending should be given as 'os' there, not 'vos'
		elseif ends_with(stem, export.consonant .. "[yrv]") then   -- e.g. śvan > śunā / cakruḥ > cakṛma
			stem = gasub(stem, ".$", cons_to_vowel)
		-- no word-initial clusters with y-, for verb 'eti, yanti'
		elseif stem == "y" then stem = "i" 
		-- e.g. yuvan > yūnā  / iyaja > īje (Whitney §800j) / babhūva > babhūtha (§800d)
		elseif (ends_with(stem, "[uU][/\\]?v") or ends_with(stem, "[iI][/\\]?y") ) and starts_with(ending, "[^yrl]") then 
			if ends_with(stem, export.consonant..export.consonant.."[iu][/\\]?[yv]") then
				-- do nothing
			else
				stem = gasub(stem, "([iIuU][/\\]?)[yv]$", function(vow) return export.lengthen[vow] end)
			end
		elseif ends_with(stem, "[aA][/\\]?[vy]") and starts_with(ending, "[^yrl]") then
			stem = gasub(stem, "([aA][/\\]?[vy])$", function(diph) return join_diphthong[diph] end)
		-- historical vocalisation of PIE nasal vowels; omitting endings starting with 'y' (optative) as 'gmy' actually occurs
		elseif ends_with(stem, export.consonant .. "[nmY]") then 
			if starts_with(ending, "[mv]") then
				stem = gasub(stem, ".$", "an")
			elseif starts_with(ending, "[tTDBs]") then
				stem = gasub(stem, ".$", "a")
			end
		end
	end
	-- remove -s/-t verbal endings after consonant (Whitney §555) 
	-- plus -s after nominal consonant stem (used to provoke a long vowel in -ir/-ur stems)
	if ends_with(stem, "[" .. export.consonant_list .. "M]") and match(ending, "^[st]$") then 
		ending = gasub(ending, ".", "")
	end
	if ending == "" then
		if ends_with(stem, "[mM]") then   -- final 'm' of a root becomes 'n'
			stem = gasub(stem, ".$", "n")
		elseif input_table.diaspirate then     -- budh > bhut, etc.
			stem = aspirate_diaspirate(stem)
		end	
		-- combine_accent for vocatives without ending
		combined = combine_accent(stem, ending, input_table.has_accent, input_table.accent_override, input_table.mono, input_table.recessive)
		return absolute_final(combined, input_table.ambig_final, input_table.j_to_z, input_table.h_to_g)
	-- all cases of ending starts with vowel
	elseif starts_with(ending, export.vowel) then
		if ends_with(stem, export.vowel_with_accent) then -- stem ends with vowel
			-- strip last vowel and accent off stem
			stem, last, acc = match(stem, "^(.*)(" .. export.vowel .. ")(" .. export.accent .. "?)$")
			-- strip first vowel off ending
			first, ending = match(ending, "^(.)(.*)$")
			-- monosyllabic semivowel-final stems (this applies to root noun compounds as well)
			if match(last, '[iIuU]') and ( input_table.mono or input_table.no_syncope ) then	
				stem = stem .. insert_glide[last .. acc]
				ending = first .. ending
			elseif export.split_diphthong[last] then			-- Can a stem-final guna and vrddhi be split into V + sonorant?
				stem = stem .. export.split_diphthong[last .. acc]
				ending = first .. ending
			elseif lower(last) == lower(first) then				-- homorganic vowels
				ending = upper(first) .. acc .. ending
			elseif lower(last) == "a" then						-- gunation and vrddhization after stem-final a.
				ending = export.up_one_grade[first .. acc] .. ending
			elseif export.vowel_to_cons[last] then			-- Can the stem-final vowel be made consonantal?
				if ends_with(stem, "Lh?") then
					stem = gasub(stem, "L$", "q"); stem = gasub(stem, "Lh$", "Q") -- convert ḷ(h) to ḍ(h)
				end
				stem = stem .. export.vowel_to_cons[last]	
				ending = first .. (acc == "/" and "\\" or "") .. ending		-- Convert stem-final udatta to ending-initial svarita
				if match(stem, "^" .. export.consonant .. "*yv$") then	-- dyu- > divā
					stem = gasub(stem, "..$", "iv")
				end
			end
		end
	-- endings starting with consonants
	-- for middle s-aorist 2nd plural
	elseif ends_with(stem, export.vowel_with_accent .. 'M?') and starts_with(ending, "sD") then
		if ends_with(stem, "[iIeEfFxuUoO][/\\]?") then
			ending = gasub(ending, '^..', 'Q')
		else 
			ending = gasub(ending, '^..', 'D')
			stem = gasub(stem, 'M$', 'n')
		end
	-- n > ñ /__{c, j}
	elseif ends_with(stem, "[cj]") and starts_with(ending, "n") then
		ending = gasub(ending, "^.", "Y")
	-- All other C- + -C interactions 
	elseif ends_with(stem, export.consonant) and starts_with(ending, export.consonant) then
		if ends_with(stem, "Lh?") then 
			stem = gasub(stem, "L$", "q"); stem = gasub(stem, "Lh$", "Q")
		end
		if input_table.final then  -- for nominal endings -bhyām, -bhis, -bhyas, -su (see Whitney §111a)
			if ends_with(stem, "kz") then -- final -kṣ > ṭ
				stem = gasub(stem, "..$", "w")
			-- final r + stop is allowed in stem, see Whitney §150b
			elseif ends_with(stem, export.vowel_with_accent .. "r[" .. export.stop_list .. "SzhL]") then 
				-- do nothing
			elseif ends_with(stem, export.consonant .. export.consonant) then -- at least 2 consonants
				-- take the first of the cluster
				stem = gasub(stem, "(" .. export.consonant .. "+)$",
					function(cluster) return sub(cluster, 1, 1) end)
			end
			if ends_with(stem, "[jSh]") or input_table.ambig_final then 
				stem = gasub(stem, ".$", input_table.ambig_final) -- in case of -j/ś/h, but also to override the normal value in a few exceptional cases
			elseif ends_with(stem, "[KgGcCJWqQTdDPbBz]") then
				stem = gasub(stem, ".$", export.to_final)
			elseif ends_with(stem, "m") then
				stem = gasub(stem, ".$", "n")
			-- Loss of {z, ẓ} with compensatory lengthening before voiced consonant. (FIXME: but what about the médha- < *mázdha- type?)
			elseif ends_with(stem, "[aA][/\\]?[sHr]") and starts_with(ending, "[rgGdDbByvjJqQlLhnm]") then
				stem = gasub(stem, "([aA])([/\\]?)[sHr]$",
					function(vow, acc) return (vow == "a" and "o" or "A") .. acc end)
			-- final s-allophones
			elseif ends_with(stem, "s") then
				if starts_with(ending, "[kKpPzsS]") then		-- visarga
					stem = gasub(stem, ".$", "H")
				elseif starts_with(ending, "[gGjJqQdDbByvlLhnm]") then	-- s > r 
					stem = gasub(stem, ".$", "r")
				-- outcommented because not relevant to internal sandhi
--				elseif starts_with(ending, "[cCwW]") then		-- homorganic fricative
--					local homorg_s = {
--							['c'] = 'S', ['C'] = 'S',
--							['w'] = 'z', ['W'] = 'z',
--						}
--					stem = gasub(stem, ".$", homorg_s[sub(ending, 1, 1)])
--				elseif starts_with(ending, "r") then		-- Loss of z before r with compensatory lengthening before voiced consonant.
--					stem = gasub(stem, "(" .. export.vowel .. "[/\\])[sHr]$", function(vow) return export.lengthen[vow] or vow end)
				end
			end
		else
			-- s-aorist (Whitney §881)
			if (ends_with(stem, "[^rnm]") and starts_with(ending, "s[tT]")) or starts_with(ending, "sD") then
				ending = gasub(ending, '^s', '')
			end
			-- diaspirate verbs with -dhve/-dhvam ending, see paper "Sanskrit as she has been misanalyzed ..." (Janda & Joseph, 2002), p.28-29
			if input_table.diaspirate and starts_with(ending, "Dv") then
				stem = aspirate_diaspirate(stem)
			elseif (ends_with(stem, "jj") or ends_with(stem, "Sc")) and starts_with(ending, "[tTdDs]") then
				stem = gasub(stem, ".$", "") -- further treated below
			end
			if ends_with(stem, "kz") then -- roots on -kṣ
				if starts_with(ending, "s") then
					stem = gasub(stem, "..$", 'k')
				elseif starts_with(ending, "[tT]") then
					stem = gasub(stem, "Nkz$", 'Mz')
					stem = gasub(stem, "kz$", 'z')
				elseif starts_with(ending, "[dD]") then
					stem = gasub(stem, "Nkz$", 'Rq')
					stem = gasub(stem, "kz$", 'q')
				end
			elseif ends_with(stem, "[cJ]") and starts_with(ending, "[tTdDs]") then
				stem = gasub(stem, "Y.$", 'Nk')
				stem = gasub(stem, "[cJ]$", 'k')
			elseif ends_with(stem, "[jCSzh]") and starts_with(ending, "s") then
				stem = gasub(stem, "[YM].$", 'Nk')
				stem = gasub(stem, "[jCSzh]$", 'k')
			elseif ends_with(stem, "[jCSz]") and starts_with(ending, "[tTdD]") then
				if input_table.j_to_z or input_table.ambig_final == "w" or ends_with(stem, "[CSz]") then
					if starts_with(ending, "[tT]") then
						stem = gasub(stem, 'Y[jC]$', 'Mz')
						stem = gasub(stem, "[jCS]$", 'z')
					else
						stem = gasub(stem, '[YM].$', 'Rq')
						stem = gasub(stem, '[jCSz]$', 'q')
					end
				else
					stem = gasub(stem, "Yj$", 'Nk')
					stem = gasub(stem, "j$", 'k')
				end
			elseif ends_with(stem, "h") and starts_with(ending, "[tTdD]") then
				if input_table.h_to_g then
					stem = gasub(stem, "h$", "g")
					ending = gasub(ending, "^.", "D")
				else
					if ends_with(stem, "va/?h") then -- root 'vah' (sometimes also 'sah', see Whitney §224b)
						stem = gasub(stem, "a(/?)h$", "o%1")
					else -- {a, i, u}h + {t, th, d, dh} > {ā, ī, ū}ḍh (Bartholomae's Law with retroflection and compensatory lengthening)
						stem = gasub(stem, "([aiu]?)([/\\]?)h$", function(vow, acc) return (export.lengthen[vow] or "") .. acc end)
						stem = gasub(stem, 'M$', 'R')
					end
					ending = gasub(ending, "^.", "Q")
				end
			-- e.g. śādhi (Whitney §166)
			elseif ends_with(stem, "As") and match(ending, "^Di/?$") then
				stem = gasub(stem, "s$", "")
			-- following Whitney's convention at §612 of interpreting 'dhv' from 's + dhv' as 'ddhv' (see also §166 and §232)
			-- sometimes also in nouns + -bhis, etc. (normally handled by input_table.final above)
			elseif ends_with(stem, ".s") and starts_with(ending, "[DB]") then 
				stem = gasub(stem, "Ms$", "nd")
				stem = gasub(stem, "s$", "d")
			-- for the (basically theoretical) middle voice of 'asti'
			elseif stem == "s" and starts_with(ending, "[sD]") then stem = ""
			elseif ends_with(stem, "m") then	-- if stem ends in m
				--  m > ṃ before {h, ś, ṣ, s} and homorganic before stops (Whitney §212)
				if starts_with(ending, "[kKgGcCjJwWqQtTdDSzsh]") then	
					stem = gasub(stem, "m$", export.homorganic_nasal[sub(ending, 1, 1)])
				elseif starts_with(ending, "[mv]") then
					stem = gasub(stem, "m$", "n")
				end
			elseif ends_with(stem, "n") then	-- if stem ends in n
				if starts_with(ending, "[Szs]") then		-- n > ṃ /__{ś, ṣ, s}
					stem = gasub(stem, ".$", "M")
				end
			elseif ends_with(stem, "[KGWQTDPB]") and starts_with(ending, "[^NYRnmyrlv]") then  -- Whitney §153
				-- Bartholomae's Law
				if ends_with(stem, "[GQDB]") and starts_with(ending, "[tT]") then
					ending = gasub(ending, "^.", "D")
				end
				stem = gasub(stem, ".$", export.deaspirate)
			end
			-- {g, ḍ, d, b} > {k, ṭ, t, p} before following unvoiced stop/sibilant
			if ends_with(stem, "[gqbd]") and starts_with(ending, "[kKcCwWtTpPSzs]") then 
				stem = gasub(stem, ".$", export.to_final)
			end
		end
		-- general rules for both 'final' (pāda) endings and others  (outcommented lines are only relevant for external sandhi)
		if ends_with(stem, "[kwp]") then
			if starts_with(ending, "[gGjJqQdDbB]") then -- {k, ṭ, p} > {g, ḍ, b} before following voiced stop
				stem = gasub(stem, ".$", export.to_voiced)
--			elseif starts_with(ending, "h") then		-- {k, ṭ, p}h > {ggh, ḍḍh, bbh}
--				stem = gasub(stem, ".$", export.to_voiced)
--				ending = gasub(ending, "^.", gasub(stem, ".$", {['k'] = 'G', ['w'] = 'Q', ['p'] = 'B'}))
			end
		elseif ends_with(stem, "t") then
			if starts_with(ending, "[gGdDbB]") then	-- t > d /__{g(h), d(h), b(h)} 
				stem = gasub(stem, ".$", "d")
--			elseif starts_with(ending, "h") then		-- t + h > ddh
--				stem = gasub(stem, ".$", "d")
--				ending = gasub(ending, "^.", "D")
--			elseif starts_with(ending, "[cCjJwWqQ]") then	-- homorganic with following stop
--				local t_pre_palatal_retroflex = 
--					{ ['c'] = 'c', ['C'] = 'c', ['j'] = 'j', ['J'] = 'j',
--					['w'] = 'w', ['W'] = 'w', ['q'] = 'q', ['Q'] = 'q', }
--				stem = gasub(stem, ".$", t_pre_palatal_retroflex[sub(ending, 1, 1)])
--			elseif starts_with(ending, "S") then		-- tś > cch
--				stem = gasub(stem, ".$", "c")
--				ending = gasub(ending, "^.", "C")
--			elseif starts_with(ending, "l") then
--				stem = gasub(stem, ".$", "l")
			end
		end
		-- note that nominal endings with -bh also trigger aspiration throwback
		if input_table.diaspirate and ends_with(stem, "[^GDBh]") and starts_with(ending, "[^QD]") then 
       		stem = aspirate_diaspirate(stem)
		end
	end
	stem, ending = export.retroflexion(stem, ending, input_table.no_retroflex_root_s)
	combined = combine_accent(stem, ending, input_table.has_accent, input_table.accent_override, input_table.mono, input_table.recessive)
	if input_table.non_final then return combined end
	return absolute_final(combined)
end

return export