Wikikamus
mswiktionary
https://ms.wiktionary.org/wiki/Wikikamus:Laman_Utama
MediaWiki 1.47.0-wmf.24
case-sensitive
Media
Khas
Perbincangan
Pengguna
Perbincangan pengguna
Wikikamus
Perbincangan Wikikamus
Fail
Perbincangan fail
MediaWiki
Perbincangan MediaWiki
Templat
Perbincangan templat
Bantuan
Perbincangan bantuan
Kategori
Perbincangan kategori
Lampiran
Perbincangan lampiran
Rima
Perbincangan rima
Tesaurus
Perbincangan tesaurus
Indeks
Perbincangan indeks
Petikan
Perbincangan petikan
Rekonstruksi
Perbincangan rekonstruksi
Padanan isyarat
Perbincangan padanan isyarat
Konkordans
Perbincangan konkordans
TimedText
TimedText talk
Modul
Perbincangan modul
Acara
Perbincangan acara
Templat:t+
10
4364
379767
187365
2026-10-20T08:07:20Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris
379767
wikitext
text/x-wiki
<includeonly>{{#invoke:translations|show|interwiki=tpos}}</includeonly><noinclude>{{t+|mul|istilah}}{{documentation}}</noinclude>
j1kku2sf6fohpkigcarfut0qklfx718
Modul:kv-translit
828
10083
379720
95132
2026-10-19T23:24:41Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/90808257|90808257]])
379720
Scribunto
text/plain
local export = {}
local tab = {
-- standard
["А"]="A", ["Б"]="B", ["В"]="V", ["Г"]="G", ["Д"]="D", ["Е"]="E", ["Ё"]="O", ["Ж"]="Ž", ["З"]="Z", ["И"]="I", ["І"]="I", ["Й"]="J",
["К"]="K", ["Л"]="L", ["М"]="M", ["Н"]="N", ["О"]="O", ["Ӧ"]="Ö", ["П"]="P", ["Р"]="R", ["С"]="S", ["Т"]="T", ["У"]="U", ["Ф"]="F",
["Х"]="X", ["Ц"]="C", ["Ч"]="Ć", ["Ш"]="Š", ["Щ"]="Šč", ["Ъ"]="", ["Ы"]="Y", ["Ь"]="", ["Э"]="E", ["Ю"]="U", ["Я"]="A",
['а']='a', ['б']='b', ['в']='v', ['г']='g', ['д']='d', ['е']='e', ['ё']='o', ['ж']='ž', ['з']='z', ['и']='i', ['і']='i', ['й']='j',
['к']='k', ['л']='l', ['м']='m', ['н']='n', ['о']='o', ['ӧ']='ö', ['п']='p', ['р']='r', ['с']='s', ['т']='t', ['у']='u', ['ф']='f',
['х']='x', ['ц']='c', ['ч']='ć', ['ш']='š', ['щ']='šč', ['ъ']='', ['ы']='y', ['ь']='', ['э']='e', ['ю']='u', ['я']='a',
-- dialectal
['Ө']='Å', ['Ӱ']='Ü',
['ө']='å', ['ӱ']='ü',
-- Molodtsov
["Ԁ"]="D", ["Ԃ"]="Ď", ["Җ"]="Dž", ["Ԅ"]="Ź", ["Ԇ"]="Dź", ["Ј"]="J", ["Ԉ"]="Ľ", ["Ԋ"]="Ń", ["Ԍ"]="Ś", ["Ԏ"]="Ť",
['ԁ']='d', ['ԃ']='ď', ['җ']='dž', ['ԅ']='ź', ['ԇ']='dź', ['ј']='j', ['ԉ']='ľ', ['ԋ']='ń', ['ԍ']='ś', ['ԏ']='ť',
}
function export.tr(text, lang, sc)
local language = lang
-- Ё needs converting if is decomposed
text = text:gsub("ё","ё"):gsub("Ё","Ё")
-- тш
text = mw.ustring.gsub(text, "Тш", "Č")
text = mw.ustring.gsub(text, "тш", "č")
-- soft consonants
text = mw.ustring.gsub(text, "([Дд])з", "%1зQ")
text = mw.ustring.gsub(text, "([ДЗЛНСТдзлнст])([еёияюь])", "%1Q%2")
-- geminates
text = mw.ustring.gsub(text, "([ДЗЛНСТдзлнст])([ДЗЛНСТдзлнст])([еёияюь])", "%1Q%2Q%3")
if language ~= "kpv" then
text = mw.ustring.gsub(text, "([БВГЖКМПРФХЦЧШЩбвгжкмпрфхцчшщ])([ёяюь])", "%1Q%2")
end
-- soft vowels after a vowel or at the beginning of a word become j-
text = mw.ustring.gsub(text, "([АОÖӨУӰЫЕЯЁЮИІЕЪЬЙаоöөуӱыэяёюиіеъьй%A][\204\129\204\128]?)([еёюя])", "%1j%2")
text = mw.ustring.gsub(text, "^Е", "Jе")
text = mw.ustring.gsub(text, "^Ё", "Jё")
text = mw.ustring.gsub(text, "^Ю", "Jю")
text = mw.ustring.gsub(text, "^Я", "jя")
text = mw.ustring.gsub(text, "^([еёюя])", "j%1")
-- palatalisation
text = mw.ustring.gsub(text, "ДQ", "Ď")
text = mw.ustring.gsub(text, "дQ", "ď")
text = mw.ustring.gsub(text, "ЗQ", "Ź")
text = mw.ustring.gsub(text, "зQ", "ź")
text = mw.ustring.gsub(text, "ЛQ", "Ľ")
text = mw.ustring.gsub(text, "лQ", "ľ")
text = mw.ustring.gsub(text, "НQ", "Ń")
text = mw.ustring.gsub(text, "нQ", "ń")
text = mw.ustring.gsub(text, "СQ", "Ś")
text = mw.ustring.gsub(text, "сQ", "ś")
text = mw.ustring.gsub(text, "ТQ", "Ť")
text = mw.ustring.gsub(text, "тQ", "ť")
text = mw.ustring.gsub(text, "Q", "ʹ")
return (mw.ustring.gsub(text,'.',tab))
end
return export
3n51zvenoubyfcps8r57kc5jrtcryb2
Templat:kesalahan ejaan
10
10430
379759
115586
2026-10-20T04:41:47Z
SNN95
2113
379759
wikitext
text/x-wiki
{{check deprecated lang param usage|lang={{{lang|}}}|<!--
-->{{#invoke:form of/templates|form_of_t|kesalahan ejaan bagi|cat=Kesalahan ejaan|withcap=1|withdot=1}}<!--
-->}}<!--
--><noinclude>{{documentation}}</noinclude>
p9u3a3lgcbaxmckh7zk59jov30u3no5
Templat:t-check
10
10506
379730
187622
2026-10-19T23:59:07Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/88378015|88378015]])
379730
wikitext
text/x-wiki
<includeonly>{{#invoke:translations|show|check=1}}</includeonly><noinclude>{{t-check|mul|istilah}}{{documentation}}</noinclude>
h7egncvbeo6do2f63czur3ih2qdh14h
Modul:ru-common
828
11593
379773
104915
2026-10-20T08:20:30Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/92714446|92714446]])
379773
Scribunto
text/plain
local export = {}
--[==[ intro:
Author: Benwing; some very early work by Rua and Atitarev
This module holds some commonly used functions for the Russian language. It's generally for use from other modules, not
`#invoke`, although some functions can be invoked from a template (`export.iotation()`, `export.reduce_stem()`,
`export.dereduce_stem()`, actually added to support calling from a bot script rather than from a user template; and
`export.make_unstressed()`, called from {{tl|R:ru:Vasmer}}). There may be issues when invoking such functions from
templates when transliteration is present, due to the need for the transliteration to be decomposed, as mentioned below
(all strings from Wiktionary pages are normally in composed form).
NOTE NOTE NOTE: All functions assume that transliteration (but not Russian) has had its acute and grave accents
decomposed using `export.decompose()`. This is the first thing that should be done to all user-specified
transliteration and any transliteration we compute that we expect to work with.
]==]
local m_str_utils = require("Module:string utilities")
local m_table_tools = require("Module:require when needed")("Module:table tools")
-- Prevents an infinite require loop since ru-translit requires a different function in this module.
local m_ru_translit = require("Module:require when needed")("Module:ru-translit")
local m_headword_utilities = require("Module:require when needed")("Module:headword utilities")
local m_table = require("Module:require when needed")("Module:table")
local gsplit = m_str_utils.gsplit
local split = m_str_utils.split
local toNFD = mw.ustring.toNFD
local u = m_str_utils.char
local ufind = mw.ustring.find
local ugsub = mw.ustring.gsub
local ulen = m_str_utils.len
local ulower = m_str_utils.lower
local umatch = mw.ustring.match
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local usub = m_str_utils.sub
local insert = table.insert
local concat = table.concat
local AC = u(0x0301) -- acute = ́
local GR = u(0x0300) -- grave = ̀
local CFLEX = u(0x0302) -- circumflex = ̂
local BREVE = u(0x0306) -- breve ̆
local DIA = u(0x0308) -- diaeresis = ̈
local CARON = u(0x030C) -- caron ̌
local OGONEK = u(0x0328) -- ogonek ̨
local PSEUDOVOWEL = u(0xFFF1) -- pseudovowel placeholder
local PSEUDOCONS = u(0xFFF2) -- pseudoconsonant placeholder
-- any stress
export.stress = AC .. GR
-- regex for any optional stress(s)
export.opt_stress = "[" .. export.stress .. "]*"
-- any accent
export.accent = export.stress .. DIA .. BREVE .. CARON .. OGONEK
-- regex for any optional accent(s)
export.opt_accent = "[" .. export.accent .. "]*"
-- any composed Cyrillic vowel with grave accent
export.composed_grave_vowel = "ѐЀѝЍ"
-- any Cyrillic vowel except ёЁ
export.vowel_no_jo = "аАеЕиИӥӤіІїЇоОуУыЫѣѢэЭюЮяЯѵѴ" .. PSEUDOVOWEL .. export.composed_grave_vowel
-- any Cyrillic vowel, including ёЁ
export.vowel = export.vowel_no_jo .. "ёЁ"
-- any vowel in transliteration
export.tr_vowel = "aAeEěĚɛƐiIoOǒǑǫǪuUyY" .. PSEUDOVOWEL
-- any consonant in transliteration, omitting "j" and the soft/hard sign
export.tr_cons_no_approx = "bBcCčČdDfFgGkKlLmMnNpPrRsSšŠtTvVxXzZžŽ" .. PSEUDOCONS
-- any consonant in transliteration, including soft/hard sign
export.tr_cons = export.tr_cons_no_approx .. "jJʹʺ"
-- regex for any consonant in transliteration, including soft/hard sign,
-- optionally followed by any accent
export.tr_cons_acc_re = "[" .. export.tr_cons .. "]" .. export.opt_accent
-- any Cyrillic consonant except sibilants and ц
export.cons_except_sib_c = "бБвВгГдДзЗйЙкКлЛмМнНпПрРсСтТфФхХъЪьЬѳѲ" .. PSEUDOCONS
-- Cyrillic sibilant consonants
export.sib = "жЖчЧшШщЩ"
-- Cyrillic sibilant consonants and ц
export.sib_c = export.sib .. "цЦ"
-- any Cyrillic consonant
export.cons = export.cons_except_sib_c .. export.sib_c
-- Cyrillic velar consonants
export.velar = "гГкКхХ"
-- uppercase Cyrillic
export.uppercase = "АБВГДЕЀЁЖЗИЍӤІЇЙКЛМНОПРСТУФХЦЧШЩЪЫЬѢЭЮЯѲѴ"
local function ine(x)
return x ~= "" and x or nil
end
--[==[
Apply Proto-Slavic iotation. This is the change that is affected by a Slavic -j- after a consonant.
`stem` is the Cyrillic stem and `tr` its optional manual translit. If `shch` is given, т iotates to щ instead of ч.
Normally return two values, the iotated Cyrillic stem and corresponding transliteration (which will be {nil} if passed
in as {nil}). If invoked from a template or bot, however, the return value will be a single string; if manual
transliteration is present, the return value will be a string of the form `CYRILLIC//TRANSLIT` with two slahes
separating the Cyrillic from the translit.
]==]
function export.iotation(stem, tr, shch)
local combine_tr = false
-- so this can be called from a template (or usually, a bot)
if type(stem) == "table" then
stem, tr, shch = ine(stem.args[1]), ine(stem.args[2]), ine(stem.args[3])
combine_tr = true
end
stem = ugsub(stem, "[сх]$", "ш")
stem = ugsub(stem, "с[кт]$", "щ")
stem = ugsub(stem, "[кц]$", "ч")
-- normally "т" is iotated as "ч" but there are many verbs that are iotated with "щ"
if shch == "щ" then
stem = stem:gsub("т$", "щ")
else
stem = stem:gsub("т$", "ч")
end
stem = ugsub(stem, "[гдз]$", "ж")
stem = ugsub(stem, "[бвмпфѳ]$", "%0л")
-- ѵ is equivalent to в after certain vowels
stem = ugsub(stem, "[аАеЕѐЀиИѝЍӥӤіІїЇѣѢэЭяЯ]" .. export.opt_stress .. "ѵ$", "%0л")
if tr then
tr = tr:gsub("[sx]$", "š")
:gsub("s[kt]$", "šč")
:gsub("[kc]$", "č")
-- normally "т" is iotated as "ч" but there are many verbs that are iotated with "щ"
if shch == "щ" then
tr = tr:gsub("t$", "šč")
else
tr = tr:gsub("t$", "č")
end
tr = tr:gsub("[dgz]$", "ž")
:gsub("[bfmpv]$", "%0l")
end
if combine_tr then
return export.combine_russian_tr(stem, tr)
end
return stem, tr
end
do
-- A word needs accents if it is unstressed and contains more than one
-- vowel, unless it's a prefix or suffix
local function word_needs_accents(word)
return not (word:sub(1, 1) == "-" or word:sub(-1) == "-") and
export.is_unstressed(word) and not export.is_monosyllabic(word)
end
-- Does a set of Cyrillic words in connected text need accents? We need to
-- split by word and check each one.
function export.needs_accents(text)
for word in gsplit(text, "%s+") do
if word_needs_accents(word) then
return true
end
end
return false
end
end
--[==[
True if either:
# A vowel is marked with explicit primary stress.
# The word has a single jo in it (which doesn't have secondary stress).
This is because a word with multiple jos requires explicit stress to be marked, since we can't infer where primary
stress lies. `Jo` can be `ё`, `ѣ̈`, or `я̈` (e.g. `сѣ̈дла`, plural of `сѣдло́`).
]==]
function export.is_stressed(word)
if word:find(AC) then
return true
end
word = toNFD(word)
return (
select(2, ugsub(word, "[еѣяЕѢЯ]" .. DIA, "")) == 1 and
not umatch(word, "[еѣяЕѢЯ]" .. DIA .. GR)
)
end
--[==[
True if a Cyrillic word requires explicit stress.
]==]
function export.is_unstressed(word)
return not export.is_stressed(word)
end
--[==[
True if Cyrillic word is stressed on the last syllable.
]==]
function export.is_ending_stressed(word)
return ufind(word, "[ёЁ][^" .. export.vowel .. "]*$") or
ufind(word, "[" .. export.vowel .. "][́̈][^" .. export.vowel .. "]*$")
end
do
local function word_is_multi_stressed(word)
-- Allow ё along with another stress, e.g. [[трёхэта́жный]], [[гёрлфре́нд]] or [[Са́мсё]].
return ufind(word, "[" .. export.vowel .. "][́̈].*[" .. export.vowel .. "][́̈]")
end
--[==[
True if a Cyrillic term has two or more stresses (acute or diaeresis) in a given word.
]==]
function export.is_multi_stressed(text)
for word in gsplit(text, "[%s-]+") do
if word_is_multi_stressed(word) then
return true
end
end
return false
end
end
--[==[
True if Cyrillic word is stressed on the first syllable.
]==]
function export.is_beginning_stressed(word)
return ufind(word, "^[^" .. export.vowel .. "]*[ёЁ]") or
ufind(word, "^[^" .. export.vowel .. "]*[" .. export.vowel .. "]́")
end
--[==[
True if Cyrillic word has no vowel. Don't treat suffixes as nonsyllabic even if they have no vowel, as they are
generally added onto words with vowels.
]==]
function export.is_nonsyllabic(word)
return not ufind(word, "^%-") and not ufind(word, "[" .. export.vowel .. "]")
end
--[==[
True if Cyrillic word has no more than one vowel; includes non-syllabic stems such as `льд-`.
]==]
function export.is_monosyllabic(word)
return not ufind(word, "[" .. export.vowel .. "].*[" .. export.vowel .. "]")
end
local recomposer = {
-- Cyrillic letters
["е" .. DIA] = "ё",
["Е" .. DIA] = "Ё",
["и" .. DIA] = "ӥ",
["И" .. DIA] = "Ӥ",
["и" .. BREVE] = "й",
["И" .. BREVE] = "Й",
["і" .. DIA] = "ї",
["І" .. DIA] = "Ї",
-- Latin letters
["c" .. CARON] = "č",
["C" .. CARON] = "Č",
["e" .. CARON] = "ě",
["E" .. CARON] = "Ě",
["o" .. CARON] = "ǒ",
["O" .. CARON] = "Ǒ",
["o" .. OGONEK] = "ǫ",
["O" .. OGONEK] = "Ǫ",
["s" .. CARON] = "š",
["S" .. CARON] = "Š",
["z" .. CARON] = "ž",
["Z" .. CARON] = "Ž",
-- used in ru-pron:
["ж" .. BREVE] = "ӂ", -- used in ru-pron
["Ж" .. BREVE] = "Ӂ",
["j" .. CFLEX] = "ĵ",
["J" .. CFLEX] = "Ĵ",
["j" .. CARON] = "ǰ",
-- no composed uppercase equivalent of J-caron
["ʒ" .. CARON] = "ǯ",
["Ʒ" .. CARON] = "Ǯ",
}
--[==[
Decompose acute, grave, etc. on letters (esp. Latin) into individivual character + combining accent. But recompose
Cyrillic and Latin characters that we want to treat as units and get caught in the crossfire. We mostly want acute and
grave decomposed; perhaps should just explicitly decompose those and no others.
]==]
function export.decompose(text)
return (ugsub(toNFD(text), ".[" .. BREVE .. DIA .. CARON .. OGONEK .. "]", recomposer))
end
function export.assert_decomposed(text)
assert(not ufind(text, "[áéíóúýàèìòùỳäëïöüÿÁÉÍÓÚÝÀÈÌÒÙỲÄËÏÖÜŸ]"))
end
--[==[
Transliterate text and then apply acute/grave decomposition.
]==]
function export.translit(text, no_include_monosyllabic_jo_accent)
local jo_accent = not no_include_monosyllabic_jo_accent and "mono" or nil
return export.decompose(m_ru_translit.tr(text, nil, nil, jo_accent))
end
--[==[
Recompose acutes and graves into preceding vowels. Probably not necessary.
]==]
function export.recompose(text)
return mw.ustring.toNFC(text)
end
local grave_decomposer = {
["ѐ"] = "е" .. GR,
["Ѐ"] = "Е" .. GR,
["ѝ"] = "и" .. GR,
["Ѝ"] = "И" .. GR,
}
--[==[
Decompose precomposed Cyrillic chars w/grave accent; not necessary for acute accent as there aren't precomposed Cyrillic
chars w/acute accent, and undesirable for precomposed `ё` and `Ё`.
]==]
function export.decompose_grave(word)
return (ugsub(word, "[ѐЀѝЍ]", grave_decomposer))
end
local grave_deaccenter = {
[GR] = "", -- grave accent
["ѐ"] = "е", -- composed Cyrillic chars w/grave accent
["Ѐ"] = "Е",
["ѝ"] = "и",
["Ѝ"] = "И",
}
local deaccenter = mw.clone(grave_deaccenter)
deaccenter[AC] = "" -- acute accent
--[==[
Remove acute and grave accents; don't affect composed diaeresis in `ёЁ` or uncomposed diaeresis in `-ѣ̈-` (as in plural
`сѣ̈дла` of `сѣдло́`). NOTE: Translit must already be decomposed! See comment at top.
]==]
function export.remove_accents(word, tr)
local ru_removed = ugsub(word, "[́̀ѐЀѝЍ]", deaccenter)
if not tr then
return ru_removed, nil
end
return ru_removed, (ugsub(tr, "[" .. AC .. GR .. "]", deaccenter))
end
--[==[
Remove grave accents; don't affect acute or composed diaeresis in `ёЁ` or uncomposed diaeresis in `-ѣ̈-` (as in plural
`сѣ̈дла` of `сѣдло́`). NOTE: Translit must already be decomposed! See comment at top.
]==]
function export.remove_grave_accents(word, tr)
local ru_removed = ugsub(word, "[̀ѐЀѝЍ]", grave_deaccenter)
if not tr then
return ru_removed, nil
end
return ru_removed, (ugsub(tr, GR, ""))
end
--[==[
Remove acute and grave accents in monosyllabic words; don't affect diaeresis (composed or uncomposed) because it
indicates a change in vowel quality, which still applies to monosyllabic words. Don't change suffixes, where a
"monosyllabic" stress is still significant (e.g. `-ча́т` short masculine of `-ча́тый`, vs. `-́чат` short masculine of
`-́чатый`). NOTE: Translit must already be decomposed! See comment at top.
]==]
function export.remove_monosyllabic_accents(word, tr)
if export.is_monosyllabic(word) and not ufind(word, "^%-") then
return export.remove_accents(word, tr)
else
return word, tr
end
end
local destresser = mw.clone(deaccenter)
destresser["ё"] = "е"
destresser["Ё"] = "Е"
destresser[DIA] = "" -- diaeresis
-- Subfunction of split_syllables(). On input we get sections of text consisting of CONSONANT - VOWEL - CONSONANT -
-- VOWEL ... - CONSONANT, where CONSONANT consists of zero or more consonants and VOWEL consists of exactly one vowel
-- plus any following accent(s); we combine these into syllables as required by split_syllables().
local function combine_captures(captures)
if captures[1] and not captures[2] then
return captures
end
local combined = {}
for i = 1, (#captures - 1), 2 do
insert(combined, captures[i] .. captures[i+1])
end
combined[#combined] = combined[#combined] .. captures[#captures]
return combined
end
--[==[
Split Russian text and transliteration into syllables. Syllables end with vowel + accent(s), except for the last
syllable, which includes any trailing consonants. NOTE: Translit must already be decomposed! See comment at top.
]==]
function export.split_syllables(ru, tr)
-- Split into alternating consonant/vowel sequences, as described in
-- combine_captures().
local rusyllables = combine_captures(split(ru, "([" .. export.vowel .. "]" .. export.opt_accent .. ")"))
local trsyllables
if tr then
export.assert_decomposed(tr)
trsyllables = combine_captures(split(tr, "([" .. export.tr_vowel .. "]" .. export.opt_accent .. ")"))
if #rusyllables ~= #trsyllables then
error("Russian " .. ru .. " doesn't have same number of syllables as translit " .. tr)
end
end
--error(concat(rusyllables, "/") .. "(" .. #rusyllables .. (trsyllables and (") || " .. concat(trsyllables, "/") .. "(" .. #trsyllables .. ")") or ""))
return rusyllables, trsyllables
end
--[==[
Split Russian word and transliteration into hyphen-separated components. Rejoining with `concat(..., "-")` will
recover the original word. If the original word ends in a hyphen, that hyphen gets included with the preceding component(this is the only case when an individual component has a hyphen in it).
]==]
function export.split_hyphens(ru, tr)
local rucomponents = split(ru, "%-")
if rucomponents[#rucomponents] == "" and #rucomponents > 1 then
rucomponents[#rucomponents - 1] = rucomponents[#rucomponents - 1] .. "-"
table.remove(rucomponents)
end
local trcomponents
if tr then
trcomponents = split(tr, "%-")
if trcomponents[#trcomponents] == "" and #trcomponents > 1 then
trcomponents[#trcomponents - 1] = trcomponents[#trcomponents - 1] .. "-"
table.remove(trcomponents)
end
if #rucomponents ~= #trcomponents then
error("Russian " .. ru .. " doesn't have same number of hyphenated components as translit " .. tr)
end
end
return rucomponents, trcomponents
end
--[==[
Apply `j` correction, converting `je` to `e` after consonants, `jo` to `o` after a sibilant, `ju` to `u` after hard
sibilant. NOTE: Translit must already be decomposed! See comment at top.
]==]
function export.j_correction(tr)
tr = ugsub(tr, "([" .. export.tr_cons_no_approx .. "]" .. export.opt_accent ..")[Jj]([EeĚě])", "%1%2")
tr = ugsub(tr, "([žščŽŠČ])[Jj]([Oo])", "%1%2")
return (ugsub(tr, "([žšŽŠ])[Jj]([Uu])", "%1%2"))
end
local function make_unstressed_ru(ru)
-- The following regexp has grave+acute+diaeresis after the bracket
return (ugsub(ru, "[̀́̈ёЁѐЀѝЍ]", destresser))
end
--[==[
Remove all stress marks (acute, grave, diaeresis). NOTE: Translit must already be decomposed! See comment at top.
]==]
function export.make_unstressed(ru, tr)
-- so this can be called from a template (specifically {{R:ru:Vasmer}})
if type(ru) == "table" then
ru, tr = ine(ru.args[1]), ine(ru.args[2])
end
if not tr then
return make_unstressed_ru(ru), nil
end
-- In the presence of TR, we need to do things the hard way: Splitting
-- into syllables and only converting Latin o to e opposite a ё.
local rusyl, trsyl = export.split_syllables(ru, tr)
for i=1,#rusyl do
if ufind(rusyl[i], "[ёЁ]") then
trsyl[i] = ugsub(trsyl[i], "[Oo]", {["O"] = "E", ["o"] = "e"})
end
rusyl[i] = make_unstressed_ru(rusyl[i])
-- the following should still work as it will affect accents only
trsyl[i] = make_unstressed_ru(trsyl[i])
end
-- Also need to apply j correction as otherwise we'll have je after cons, etc.
return concat(rusyl, ""), export.j_correction(concat(trsyl, ""))
end
local function remove_jo_ru(word)
return (ugsub(word, "[̈ёЁ]", destresser))
end
local function make_unstressed_once_ru(word)
-- leave graves alone
return (ugsub(word, "([́̈ёЁ])([^́̈ёЁ]*)$", function(x, rest) return destresser[x] .. rest; end, 1))
end
--[==[
Remove diaeresis stress marks only. NOTE: Translit must already be decomposed! See comment at top.
]==]
function export.remove_jo(ru, tr)
if not tr then
return remove_jo_ru(ru), nil
end
-- In the presence of TR, we need to do things the hard way: Splitting
-- into syllables and only converting Latin o to e opposite a ё.
local rusyl, trsyl = export.split_syllables(ru, tr)
for i=1,#rusyl do
if ufind(rusyl[i], "[ёЁ]") then
trsyl[i] = ugsub(trsyl[i], "[Oo]", {["O"] = "E", ["o"] = "e"})
end
rusyl[i] = remove_jo_ru(rusyl[i])
-- the following should still work as it will affect accents only
trsyl[i] = make_unstressed_once_ru(trsyl[i])
end
-- Also need to apply j correction as otherwise we'll have je after cons, etc.
return concat(rusyl, ""),
export.j_correction(concat(trsyl, ""))
end
local function map_last_hyphenated_component(fn, ru, tr)
if ufind(ru, "%-") then
-- If there is a hyphen, do it the hard way by splitting into
-- individual components and doing the last one. Otherwise we just do
-- the whole string.
local rucomponents, trcomponents = export.split_hyphens(ru, tr)
local lastru, lasttr = fn(rucomponents[#rucomponents],
trcomponents and trcomponents[#trcomponents] or nil)
rucomponents[#rucomponents] = lastru
ru = concat(rucomponents, "-")
if trcomponents then
trcomponents[#trcomponents] = lasttr
tr = concat(trcomponents, "-")
end
return ru, tr
end
return fn(ru, tr)
end
-- Make last stressed syllable (acute or diaeresis) unstressed; leave unstressed; leave graves alone; if `NOCONCAT`,
-- return individual syllables. NOTE: Translit must already be decomposed! See comment at top.
local function make_unstressed_once_after_hyphen_split(ru, tr, noconcat)
if not tr then
return make_unstressed_once_ru(ru), nil
end
-- In the presence of TR, we need to do things the hard way, as with
-- make_unstressed().
local rusyl, trsyl = export.split_syllables(ru, tr)
for i=#rusyl,1,-1 do
local stressed = export.is_stressed(rusyl[i])
if stressed then
if ufind(rusyl[i], "[ёЁ]") then
trsyl[i] = ugsub(trsyl[i], "[Oo]", {["O"] = "E", ["o"] = "e"})
end
rusyl[i] = make_unstressed_once_ru(rusyl[i])
-- the following should still work as it will affect accents only
trsyl[i] = make_unstressed_once_ru(trsyl[i])
break
end
end
if noconcat then
return rusyl, trsyl
end
-- Also need to apply j correction as otherwise we'll have je after cons
return concat(rusyl, ""),
export.j_correction(concat(trsyl, ""))
end
--[==[
Make last stressed syllable (acute or diaeresis) to the right of any hyphen unstressed (unless the hyphen is
word-final); leave graves alone. We don't destress a syllable to the left of a hyphen unless the hyphen is word-final
(i.e. a prefix). Otherwise e.g. the accents in the first part of words like ко́е-како́й and а́льфа-лу́ч won't remain. NOTE:
Translit must already be decomposed! See comment at top.
]==]
function export.make_unstressed_once(ru, tr)
return map_last_hyphenated_component(make_unstressed_once_after_hyphen_split, ru, tr)
end
local function make_unstressed_once_at_beginning_ru(word)
-- leave graves alone
return (ugsub(word, "^([^́̈ёЁ]*)([́̈ёЁ])", function(rest, x) return rest .. destresser[x]; end, 1))
end
--[==[
Make first stressed syllable (acute or diaeresis) unstressed; leave graves alone; if `NOCONCAT`, return individual
syllables. NOTE: Translit must already be decomposed! See comment at top.
]==]
function export.make_unstressed_once_at_beginning(ru, tr, noconcat)
if not tr then
return make_unstressed_once_at_beginning_ru(ru), nil
end
-- In the presence of TR, we need to do things the hard way, as with
-- make_unstressed().
local rusyl, trsyl = export.split_syllables(ru, tr)
for i=1,#rusyl do
local stressed = export.is_stressed(rusyl[i])
if stressed then
if ufind(rusyl[i], "[ёЁ]") then
trsyl[i] = ugsub(trsyl[i], "[Oo]", {["O"] = "E", ["o"] = "e"})
end
rusyl[i] = make_unstressed_once_at_beginning_ru(rusyl[i])
-- the following should still work as it will affect accents only
trsyl[i] = make_unstressed_once_at_beginning_ru(trsyl[i])
break
end
end
if noconcat then
return rusyl, trsyl
end
-- Also need to apply j correction as otherwise we'll have je after cons
return concat(rusyl, ""),
export.j_correction(concat(trsyl, ""))
end
--[==[
Subfunction of `make_ending_stressed()`, `make_beginning_stressed()`, which add an acute accent to a syllable that may
already have a grave accent; in such a case, remove the grave. NOTE: Translit must already be decomposed! See comment at
top.
]==]
function export.correct_grave_acute_clash(word, tr)
word = ugsub(word, "([̀ѐЀѝЍ])́", function(x) return grave_deaccenter[x] .. AC; end)
word = ugsub(word, AC .. GR, AC)
if not tr then
return word, nil
end
tr = ugsub(tr, GR .. AC, AC)
tr = ugsub(tr, AC .. GR, AC)
return word, tr
end
local function make_ending_stressed_ru(word)
-- If already ending stressed, just return word so we don't mess up ё
if export.is_ending_stressed(word) then
return word
end
-- Destress the last stressed syllable
word = make_unstressed_once_ru(word)
-- Add an acute to the last syllable
word = ugsub(word, "([" .. export.vowel_no_jo .. "])([^" .. export.vowel .. "]*)$",
"%1́%2")
-- If that caused an acute and grave next to each other, remove the grave
return export.correct_grave_acute_clash(word)
end
-- Remove the last primary stress from the word and put it on the final syllable. Leave grave accents alone except in
-- the last syllable. If final syllable already has primary stress, do nothing. NOTE: Translit must already be
-- decomposed! See comment at top.
local function make_ending_stressed_after_hyphen_split(ru, tr)
if not tr then
return make_ending_stressed_ru(ru), nil
end
-- If already ending stressed, just return ru/tr so we don't mess up ё
if export.is_ending_stressed(ru) then
return ru, tr
end
-- Destress the last stressed syllable; pass in "noconcat" so we get
-- the individual syllables back
local rusyl, trsyl = make_unstressed_once_after_hyphen_split(ru, tr, "noconcat")
-- Add an acute to the last syllable of both Russian and translit
rusyl[#rusyl] = ugsub(rusyl[#rusyl], "([" .. export.vowel_no_jo .. "])",
"%1" .. AC)
trsyl[#trsyl] = ugsub(trsyl[#trsyl], "([" .. export.tr_vowel .. "])",
"%1" .. AC)
-- If that caused an acute and grave next to each other, remove the grave
rusyl[#rusyl], trsyl[#trsyl] =
export.correct_grave_acute_clash(rusyl[#rusyl], trsyl[#trsyl])
-- j correction didn't get applied in make_unstressed_once because
-- we short-circuited it and made it return lists of syllables
return concat(rusyl, ""),
export.j_correction(concat(trsyl, ""))
end
--[==[
Remove the last primary stress from the portion of the word to the right of any hyphen (unless the hyphen is word-final)
and put it on the final syllable. Leave grave accents alone except in the last syllable. If final syllable already has
primary stress, do nothing. (See `make_unstressed_once()` for why we don't affect stresses to the left of a hyphen.)
NOTE: Translit must already be decomposed! See comment at top.
]==]
function export.make_ending_stressed(ru, tr)
return map_last_hyphenated_component(make_ending_stressed_after_hyphen_split, ru, tr)
end
local function make_beginning_stressed_ru(word)
-- If already beginning stressed, just return word so we don't mess up ё
if export.is_beginning_stressed(word) then
return word
end
-- Destress the first stressed syllable
word = make_unstressed_once_at_beginning_ru(word)
-- Add an acute to the first syllable
word = ugsub(word, "^([^" .. export.vowel .. "]*)([" .. export.vowel_no_jo .. "])",
"%1%2́")
-- If that caused an acute and grave next to each other, remove the grave
return export.correct_grave_acute_clash(word)
end
--[==[
Remove the first primary stress from the word and put it on the initial syllable. Leave grave accents alone except in
the first syllable. If initial syllable already has primary stress, do nothing. NOTE: Translit must already be
decomposed! See comment at top.
]==]
function export.make_beginning_stressed(ru, tr)
if not tr then
return make_beginning_stressed_ru(ru), nil
end
-- If already beginning stressed, just return ru/tr so we don't mess up ё
if export.is_beginning_stressed(ru) then
return ru, tr
end
-- Destress the first stressed syllable; pass in "noconcat" so we get
-- the individual syllables back
local rusyl, trsyl = export.make_unstressed_once_at_beginning(ru, tr, "noconcat")
-- Add an acute to the first syllable of both Russian and translit
rusyl[1] = ugsub(rusyl[1], "([" .. export.vowel_no_jo .. "])",
"%1" .. AC)
trsyl[1] = ugsub(trsyl[1], "([" .. export.tr_vowel .. "])",
"%1" .. AC)
-- If that caused an acute and grave next to each other, remove the grave
rusyl[1], trsyl[1] = export.correct_grave_acute_clash(rusyl[1], trsyl[1])
-- j correction didn't get applied in make_unstressed_once_at_beginning
-- because we short-circuited it and made it return lists of syllables
return concat(rusyl, ""),
export.j_correction(concat(trsyl, ""))
end
-- used for tracking and categorization
local hard_cons = {"hard-cons", "cons"}
local hard_vowel = {"vowel", "hard-vowel"}
local i_vowel = {"i", "vowel", "soft-vowel"}
local sibilant_cons = {"sibilant", "cons"}
local soft_vowel = {"vowel", "soft-vowel"}
local velar_cons = {"velar", "cons"}
local trailing_letter_type = {
["ш"] = sibilant_cons,
["щ"] = sibilant_cons,
["ч"] = sibilant_cons,
["ж"] = sibilant_cons,
["ц"] = {"c", "cons"},
["к"] = velar_cons,
["г"] = velar_cons,
["х"] = velar_cons,
["ь"] = {"soft-cons", "cons"},
["ъ"] = hard_cons,
["й"] = {"palatal", "cons"},
["а"] = hard_vowel,
["я"] = soft_vowel,
["э"] = hard_vowel,
["е"] = soft_vowel,
["ѐ"] = soft_vowel,
["ѣ"] = soft_vowel,
["и"] = i_vowel,
["ѝ"] = i_vowel,
["ӥ"] = i_vowel,
["і"] = i_vowel,
["ї"] = i_vowel,
["ѵ"] = i_vowel,
["ы"] = hard_vowel,
["о"] = hard_vowel,
["ё"] = soft_vowel,
["у"] = hard_vowel,
["ю"] = soft_vowel,
}
function export.get_stem_trailing_letter_type(stem)
return trailing_letter_type[ulower(usub(export.remove_accents(stem), -1))] or hard_cons
end
--[==[
Reduce stem by eliminating the "epenthetic" vowel. Applies to nominative singular masculine 2nd-declension hard and
soft, and 3rd-declension feminine in -ь (e.g. [[любовь]]). `STEM` should be the result after calling
`detect_stem_type()`, but with final -й if present. Normally returns two arguments (`STEM` and `TR`), but can be called
from a template or bot using `#invoke` and will return one argument (`STEM`, or `STEM//TR` if `TR` is present). Returns
{nil} if unable to reduce. NOTE: Translit must already be decomposed! See comment at top.
]==]
function export.reduce_stem(stem, tr, soft_n)
local pre, letter, post
local pretr, lettertr, posttr
local combine_tr = false
-- test cases with translit:
-- =p.reduce_stem("фе́ез", "fɛ́jez") -> фе́йз, fɛ́jz
-- =p.reduce_stem("фе́йез", "fɛ́jez") -> фе́йз, fɛ́jz
-- =p.reduce_stem("фе́без", "fɛ́bez") -> фе́бз, fɛ́bz
-- =p.reduce_stem("фе́лез", "fɛ́lez") -> фе́льз, fɛ́lʹz
-- =p.reduce_stem("феёз", p.decompose("fɛjóz")) -> фейз, fɛjz
-- =p.reduce_stem("фейёз", p.decompose("fɛjjóz")) -> фейз, fɛjz
-- =p.reduce_stem("фебёз", p.decompose("fɛbjóz")) -> фебз, fɛbz
-- =p.reduce_stem("фелёз", p.decompose("fɛljóz")) -> фельз, fɛlʹz
-- =p.reduce_stem("фе́бей", "fɛ́bej") -> фе́бь, fɛ́bʹ
-- =p.reduce_stem("фебёй", p.decompose("fɛbjój")) -> фебь, fɛbʹ
-- =p.reduce_stem("фе́ей", "fɛ́jej") -> фе́йй, fɛ́jj
-- =p.reduce_stem("феёй", p.decompose("fɛjój")) -> фейй, fɛjj
-- so this can be called from a template (or usually, a bot)
if type(stem) == "table" then
stem, tr = ine(stem.args[1]), ine(stem.args[2])
combine_tr = true
end
pre, letter, post = umatch(stem, "^(.*)([оОеЕёЁ])́?([" .. export.cons .. "]+)$")
if not pre then
return nil, nil
end
if tr then
-- FIXME, may not be necessary to write the posttr portion as a
-- consonant + zero or more consonant/accent combinations -- when will
-- we ever get an accent after a consonant? That would indicate a
-- failure of the decompose mechanism.
pretr, lettertr, posttr = umatch(tr, "^(.*)([oOeE])́?([" .. export.tr_cons .. "][" .. export.tr_cons .. export.accent .. "]*)$")
if not pretr then
return nil, nil -- should not happen unless tr is really messed up
end
-- Remove any trailing j's on the translit stem which don't have a
-- corresponding й on the Cyrillic stem. This avoids excess j's with
-- cases like индонези́ец//indonɛzíjec, but retains multiple j's where
-- necessary.
pretr = ugsub(pretr, "[jJ]+$", function(m)
return usub(m, 1, ulen(umatch(pre, "[йЙ]*$")))
end)
end
if letter == "О" or letter == "о" then
-- FIXME, what about when the accent is on the removed letter?
if post == "й" or post == "Й" then
-- FIXME, is this correct?
return nil, nil
end
letter = ""
else
local is_upper = ufind(post, "[" .. export.uppercase .. "]")
if ufind(pre, "[" .. export.vowel .. "]́?$") then
letter = is_upper and "Й" or "й"
elseif post == "й" or post == "Й" then
letter = is_upper and "Ь" or "ь"
post = ""
if posttr then
posttr = ""
end
elseif ufind(post, "[" .. export.velar .. "]$") and ufind(pre, "[" .. export.cons_except_sib_c .. "]$") or
ufind(post, "[^йЙ" .. export.velar .. "]$") and ufind(pre, "[лЛ]$") or
soft_n and ufind(pre, "[нН]$") then
letter = is_upper and "Ь" or "ь"
else
letter = ""
end
end
stem = pre .. letter .. post
if tr then
tr = pretr .. export.translit(letter) .. posttr
end
if combine_tr then
return export.combine_russian_tr(stem, tr)
else
return stem, tr
end
end
--[==[
Generate the dereduced stem given `STEM` and `EPENTHETIC_STRESS` (which indicates whether the epenthetic vowel should be
stressed); this is without any terminating non-syllabic ending, which is added if needed by the calling function.
Normally returns two arguments (`STEM` and `TR`), but can be called from a template using `#invoke` (or by bot) and will
return one argument (`STEM`, or `STEM//TR` if `TR` is present). Returns {nil} if unable to dereduce. NOTE: Translit
must already be decomposed! See comment at top.
]==]
function export.dereduce_stem(stem, tr, epenthetic_stress)
local combine_tr = false
-- so this can be called from a template (or usually, a bot)
if type(stem) == "table" then
stem, tr, epenthetic_stress = ine(stem.args[1]), ine(stem.args[2]), ine(stem.args[3])
combine_tr = true
end
if epenthetic_stress then
stem, tr = export.make_unstressed_once(stem, tr)
end
local pre, letter, post
local pretr, lettertr, posttr
-- FIXME!!! Deal with this special case
--if not (z.stem_type == "soft" and _.equals(z.stress_type, {"b", "f"}) -- we should ignore asterix for 2*b and 2*f (so to process it just like 2b or 2f)
-- or _.contains(z.specific, "(2)") and _.equals(z.stem_type, {"velar", "letter-ц", "vowel"})) -- and also the same for (2)-specific and 3,5,6 stem-types
--then
-- I think this corresponds to our -ья and -ье types, which we
-- handle separately
--if z.stem_type == "vowel" then -- 1).
-- if _.equals(z.stress_type, {"b", "c", "e", "f", "f'", "b'" }) then -- gen_pl ending stressed -- TODO: special vars for that
-- z.stems["gen_pl"] = _.replace(z.stems["gen_pl"], "ь$", "е́")
-- else
-- z.stems["gen_pl"] = _.replace(z.stems["gen_pl"], "ь$", "и")
-- end
--end
pre, letter, post = umatch(stem, "^(.*)([" .. export.cons .. "])([" .. export.cons .. "])$")
if tr then
pretr, lettertr, posttr = umatch(tr, "^(.*)(" .. export.tr_cons_acc_re .. ")(" .. export.tr_cons_acc_re .. ")$")
if pre and not pretr then
return nil, nil -- should not happen unless tr is really messed up
end
end
if pre then
local is_upper = ufind(post, "[" .. export.uppercase .. "]")
local epvowel
if ufind(letter, "[ьйЬЙ]") then
letter = ""
lettertr = ""
if ufind(post, "[цЦ]$") or not epenthetic_stress then
epvowel = is_upper and "Е" or "е"
else
epvowel = is_upper and "Ё" or "ё"
end
elseif ufind(letter, "[" .. export.cons_except_sib_c .. "]") and ufind(post, "[" .. export.velar .. "]") or
ufind(letter, "[" .. export.velar .. "]") then
epvowel = is_upper and "О" or "о"
elseif post == "ц" or post == "Ц" then
epvowel = is_upper and "Е" or "е"
elseif epenthetic_stress then
if ufind(letter, "[" .. export.sib .. "]") then
epvowel = is_upper and "О́" or "о́"
else
epvowel = is_upper and "Ё" or "ё"
end
else
epvowel = is_upper and "Е" or "е"
end
assert(epvowel)
stem = pre .. letter .. epvowel .. post
if tr then
tr = pretr .. lettertr .. export.translit(epvowel) .. posttr
tr = export.j_correction(tr)
end
if epenthetic_stress then
stem, tr = export.make_ending_stressed(stem, tr)
end
if combine_tr then
return export.combine_russian_tr(stem, tr)
else
return stem, tr
end
end
return nil, nil
end
--[==[
Parse an entry that potentially has final footnote symbols and initial `*` for a hypothetical entry into initial
symbols, text and final symbols.
]==]
function export.split_symbols(entry, do_subscript)
local prefentry, finalnotes = m_table_tools.separate_notes(entry)
local initnotes, text = umatch(prefentry, "(%*?)(.*)$")
return initnotes, text, finalnotes
end
--------------------------------------------------------------------------
-- Used for manual translit --
--------------------------------------------------------------------------
--[==[
Transliterate text after removing any links. The translit is decomposed in the process.
]==]
function export.translit_no_links(text)
return export.translit(require("Module:links").remove_links(text))
end
--[==[
Split a combined Cyrillic/translit string (either in the form `CYRILLIC//TRANSLIT` or just `CYRILLIC`) into its
components, decompose the translit, and either return a two-element list of `CYRILLIC` and `TRANSLIT` (if `dopair` is
given), or two separate return values (if `dopair` is not specified).
]==]
function export.split_russian_tr(term, dopair)
local ru, tr
if not term:find("//") then
ru = term
else
local splitvals = split(term, "//")
if #splitvals ~= 2 then
error("Must have at most one // in a Russian//translit expr: '" .. term .. "'")
end
ru, tr = splitvals[1], export.decompose(splitvals[2])
end
if dopair then
return {ru, tr}
else
return ru, tr
end
end
--[==[
Combine Cyrillic and translit into a single string separated by `//`. If translit is {nil}, the return value will be
the same as the passed-in Cyrillic. `ru` can be a string (which any manual translit in `tr`) or a two-element table of
`{CYRILLIC, TRANSLIT}`.
]==]
function export.combine_russian_tr(ru, tr)
if type(ru) == "table" then
ru, tr = unpack(ru)
end
if tr then
return ru .. "//" .. tr
else
return ru
end
end
local function concat_maybe_moving_notes(x, y, movenotes)
if movenotes then
local xentry, xnotes = m_table_tools.separate_notes(x)
local yentry, ynotes = m_table_tools.separate_notes(y)
return xentry .. yentry .. xnotes .. ynotes
else
return x .. y
end
end
--[==[
Concatenate two Cyrillic strings `RU1` and `RU2` that may have corresponding manual transliteration `TR1` and `TR2`
(which should be {nil} if there is no manual translit, and otherwise should be decomposed). If `DOPAIR`, return a
two-item list of the combined Cyrillic and manual translit (which will be {nil} if both `TR1` and `TR2` are {nil});
else, return two values, the combined Cyrillic and manual translit. If `MOVENOTES`, extract any footnote symbols at the
end of `RU1` and move them to the end of the concatenated string, before any footnote symbols for `RU2`; same thing goes
for `TR1` and `TR2`.
]==]
function export.concat_russian_tr(ru1, tr1, ru2, tr2, dopair, movenotes)
local ru, tr
if not tr1 and not tr2 then
ru = concat_maybe_moving_notes(ru1, ru2, movenotes)
else
if not tr1 then
tr1 = export.translit_no_links(ru1)
end
if not tr2 then
tr2 = export.translit_no_links(ru2)
end
ru, tr = concat_maybe_moving_notes(ru1, ru2, movenotes), export.j_correction(concat_maybe_moving_notes(tr1, tr2, movenotes))
end
if dopair then
return {ru, tr}
else
return ru, tr
end
end
--[==[
Concatenate two Cyrillic/translit combinations (where each combination is a two-element list of `{CYRILLIC, TRANSLIT}`
where `TRANSLIT` may be nil) by individually concatenating the Cyrillic and translit portions, and return a concatenated
combination as a two-element list. If the manual translit portions of both terms on entry are {nil}, the result will
also have {nil} manual translit. If `MOVENOTES`, extract any footnote symbols at the end of `TERM1` and move them after
the concatenated string and before any footnote symbols at the end of `TERM2`.
]==]
function export.concat_paired_russian_tr(term1, term2, movenotes)
assert(type(term1) == "table")
assert(type(term2) == "table")
local ru1, tr1 = term1[1], term1[2]
local ru2, tr2 = term2[1], term2[2]
return export.concat_russian_tr(ru1, tr1, ru2, tr2, "dopair", movenotes)
end
function export.concat_forms(forms)
local joined_rutr = {}
for _, form in ipairs(forms) do
insert(joined_rutr, export.combine_russian_tr(form))
end
return concat(joined_rutr, ",")
end
--[==[
Given a list of forms, where each form is a two-element list of `{CYRILLIC, TRANSLIT}`, strip footnote symbols from the
end of the Cyrillic and translit.
]==]
function export.strip_notes_from_forms(forms)
local newforms = {}
for _, form in ipairs(forms) do
local ru, tr = form[1], form[2]
ru, _ = m_table_tools.separate_notes(ru)
if tr then
tr, _ = m_table_tools.separate_notes(tr)
end
insert(newforms, {ru, tr})
end
return newforms
end
--[==[
Given a list of forms, where each form is a two-element list of `{CYRILLIC, TRANSLIT}`, unzip into parallel lists of
Cyrillic and translit. The latter list may have gaps in it.
]==]
function export.unzip_forms(forms)
local rulist = {}
local trlist = {}
for i, form in ipairs(forms) do
local ru, tr = form[1], form[2]
rulist[i] = ru
trlist[i] = tr
end
return rulist, trlist
end
--[==[
Given parallel lists of Cyrillic and translit (where the latter list may have gaps in it), return a list of forms,
where each form is a two-element list of `{CYRILLIC, TRANSLIT}`.
]==]
function export.zip_forms(rulist, trlist)
local forms = {}
for i, ru in ipairs(rulist) do
insert(forms, {ru, trlist[i]})
end
return forms
end
local function any_forms_have_translit(forms)
for _, form in ipairs(forms) do
if form[2] then
return true
end
end
return false
end
--[==[
Given a list of forms, where each form is a two-element list of `{CYRILLIC, TRANSLIT}`, combine forms with identical
Cyrillic, concatenating the translit with a comma+space in between.
]==]
function export.combine_translit_of_duplicate_forms(forms)
if not forms[1] then
return forms
end
-- Optimization to avoid creating a new list in the majority case when no translit exists.
if not any_forms_have_translit(forms) then
return forms
end
local newforms = {}
insert(newforms, {forms[1][1], forms[1][2]})
for i = 2, #forms do
local found_duplicate = false
for j = 1, #newforms do
-- If the Russian of the next form is the same as that of the last one, combine their translits and modify
-- newforms[] in-place. Otherwise add the next form to newforms[]. Make sure to clone the form rather than
-- just appending it directly since we may modify it in-place; we don't want to side-effect `forms` as
-- passed in.
if forms[i][1] == newforms[j][1] then
local tr1 = newforms[j][2]
local tr2 = forms[i][2]
if not tr1 and not tr2 then
-- this shouldn't normally happen
else
tr1 = tr1 or export.translit_no_links(newforms[j][1])
tr2 = tr2 or export.translit_no_links(forms[i][1])
if tr1 == tr2 then
-- this shouldn't normally happen
else
newforms[j][2] = tr1 .. ", " .. tr2
end
end
found_duplicate = true
break
end
end
if not found_duplicate then
insert(newforms, {forms[i][1], forms[i][2]})
end
end
return newforms
end
function export.any_termobjs_have_translit(termobjs)
for _, termobj in ipairs(termobjs) do
if termobj.tr then
return true
end
end
return false
end
--[==[
Given a list of term objects, where each object is a table of `{term = CYRILLIC, tr = TRANSLIT, ...}`, combine forms
with identical Cyrillic and identical ancillary properties, concatenating the translit with a comma+space in between.
Also recompose all transliteration.
]==]
function export.combine_translit_of_duplicate_termobjs_and_recompose(termobjs)
if not termobjs[1] then
return termobjs
end
-- Optimization to avoid creating a new list in the majority case when no translit exists.
if not export.any_termobjs_have_translit(termobjs) then
return termobjs
end
local newtermobjs = {}
insert(newtermobjs, m_table.shallowCopy(termobjs[1]))
for i = 2, #termobjs do
local found_duplicate = false
for j = 1, #newtermobjs do
-- If the Russian of the next termobj is the same as that of the last one, combine their translits and
-- modify newtermobjs[] in-place. Otherwise add the next termobj to newtermobjs[]. Make sure to clone the
-- termobj rather than just appending it directly since we may modify it in-place; we don't want to
-- side-effect `termobjs` as passed in.
if termobjs[i].term == newtermobjs[j].term and
m_headword_utilities.termobj_decorations_equal(termobjs[i], newtermobjs[j]) then
local tr1 = newtermobjs[j].tr
local tr2 = termobjs[i].tr
if not tr1 and not tr2 then
-- this shouldn't normally happen
else
tr1 = tr1 or export.translit_no_links(newtermobjs[j].term)
tr2 = tr2 or export.translit_no_links(termobjs[i].term)
if tr1 == tr2 then
-- this shouldn't normally happen
else
newtermobjs[j].tr = tr1 .. ", " .. tr2
end
end
found_duplicate = true
break
end
end
if not found_duplicate then
insert(newtermobjs, m_table.shallowCopy(termobjs[i]))
end
end
for _, newtermobj in ipairs(newtermobjs) do
if newtermobj.tr then
newtermobj.tr = export.recompose(newtermobj.tr)
end
end
return newtermobjs
end
--[==[
Given a list of forms, where each form is a two-element list of `{CYRILLIC, TRANSLIT}`, split cases where two different
transliterations have been packed into a single translit field by creating two adjacent term/translit pairs. This is
the opposite operation of `combine_translit_of_duplicate_forms()`.
]==]
function export.split_translit_of_duplicate_forms(forms)
if not forms[1] then
return forms
end
-- Optimization to avoid creating a new list in the majority case when no translit exists.
if not export.any_forms_have_translit(forms) then
return forms
end
local newforms = {}
for _, form in ipairs(forms) do
local ru, tr = unpack(form)
if not tr or not tr:find(",") or ru:find(",") then
insert(newforms, form)
else
local split_trs = split(tr, ",%s*")
local default_tr = export.translit_no_links(ru)
for _, split_tr in ipairs(split_trs) do
if split_tr == default_tr then
split_tr = nil
end
insert(newforms, {ru, split_tr})
end
end
end
return newforms
end
--[==[
Given a list of forms, where each form is a two-element list of `{CYRILLIC, TRANSLIT}`, split cases where two different
transliterations have been packed into a single translit field by creating two adjacent term/translit pairs. This is
the opposite operation of `combine_translit_of_duplicate_forms()`.
]==]
function export.split_translit_of_duplicate_termobjs_and_decompose(termobjs)
if not termobjs[1] then
return termobjs
end
-- Optimization to avoid creating a new list in the majority case when no translit exists.
if not export.any_termobjs_have_translit(termobjs) then
return termobjs
end
local newobjs = {}
for _, obj in ipairs(termobjs) do
local tr = obj.tr
if tr then
tr = export.decompose(tr)
end
if not tr or not tr:find(",") or obj.term:find(",[^%s]") then
if not tr or tr == obj.tr then
insert(newobjs, obj)
else
obj = m_table.shallowCopy(obj)
obj.tr = tr
insert(newobjs, obj)
end
else
local term = obj.term
local embedded_comma_in_term = not not term:find(",")
-- If there's an embedded comma in the term, we want to split on commas not followed by a space; otherwise,
-- we split on commas whether or not followed by a space. To implement "comma not followed by space", we
-- can use a "frontier pattern"; the one below matches if the character after the comma is in the specified
-- set (which is anything but a comma or space) and the comma itself is not in the specified set (in other
-- words, it is a comma or space, which matches because it's a comma). This doesn't work correctly if there
-- are two commas in a row, but that should not happen as there should not be an empty translit.
local split_trs = split(tr, embedded_comma_in_term and ",%f[^,%s]" or ",%s*")
local default_tr = export.translit_no_links(term)
for _, split_tr in ipairs(split_trs) do
if split_tr == default_tr then
split_tr = nil
end
local newobj = m_table.shallowCopy(obj)
newobj.tr = split_tr
insert(newobjs, newobj)
end
end
end
return newobjs
end
function export.strip_ending(ru, tr, ending)
local strippedru = ugsub(ru, ending .. "$", "")
if strippedru == ru then
error("Argument " .. ru .. " doesn't end with expected ending " .. ending)
end
ru = strippedru
tr = export.strip_tr_ending(tr, ending)
return ru, tr
end
function export.strip_tr_ending(tr, ending)
if not tr then return nil end
local endingtr = ugsub(export.translit_no_links(ending), "^([Jj])", "%1?")
local strippedtr = ugsub(tr, endingtr .. "$", "")
if strippedtr == tr then
error("Translit " .. tr .. " doesn't end with expected ending " .. endingtr)
end
return strippedtr
end
return export
t31hgzrt5gr58kpnh1unp48r74ofiv6
Templat:grc-IPA
10
11660
379726
246335
2026-10-19T23:49:47Z
Hakimi97
2668
Kemas kini kod templat
379726
wikitext
text/x-wiki
{{#invoke:grc-pronunciation|create}}<noinclude>{{documentation}}<templatedata>
{
"params": {
"1": {
"aliases": [
"w"
],
"description": "perkataan yang hendak disalin. Jika nama kemasukan mengandungi huruf α, ι, atau υ, dan ia bukan sebahagian daripada diftong, tambah makron selepas ia untuk menandakannya sebagai panjang, atau brif untuk menandainya sebagai pendek.",
"autovalue": "",
"example": "ᾰ̔́πᾱς",
"type": "string",
"default": "page title"
}
},
"description": "Menjana sebutan AFA dalam lema bahasa Yunani Purba.",
"format": "inline"
}
</templatedata>
</noinclude>
ppk6lf4bkfa3hz4stnbkzweoag3r1bo
Modul:grc-pronunciation
828
11661
379727
212942
2026-10-19T23:53:28Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/88536903|88536903]])
379727
Scribunto
text/plain
local export = {}
local m_a = require("Module:accent qualifier")
local m_data = mw.loadData("Module:grc-pronunciation/data")
local m_grc_accent = require("Module:grc-accent")
local m_grc_utils = require("Module:grc-utilities")
local m_grc_utils_data = require("Module:grc-utilities/data")
local m_IPA = require("Module:IPA")
local m_str_utils = require("Module:string utilities")
-- [[Module:grc-utilities]] converts sequences of diacritics to the order required by this module,
-- then replaces combining macrons and breves with spacing ones.
local diacritic = m_grc_utils_data.diacritic
local diacritics = m_grc_utils_data.diacritics
local canonicalize = m_grc_utils.canonicalize
local concat = table.concat
local find_ambig = m_grc_accent.find_ambig
local floor = math.floor
local format_categories = require("Module:utilities").format_categories
local full_link = m_grc_utils.link
local gsplit = m_str_utils.gsplit
local insert = table.insert
local is_preview = require("Module:pages").is_preview
local list_to_text = mw.text.listToText
local mark_implied_length = m_grc_accent.mark_implied_length
local max = math.max
local split = m_str_utils.split
local rearrangeDiacritics = m_grc_utils.pronunciationOrder
local rfind = m_str_utils.find
local usub = m_str_utils.sub
local rmatch = m_str_utils.match
local rsubn = m_str_utils.gsub
local strip_accent = m_grc_accent.strip_accent
local toNFD = mw.ustring.toNFD
local ulen = m_str_utils.len
local ulower = m_str_utils.lower
local U = m_str_utils.char
local pagename = mw.loadData("Module:headword/data").pagename
local lang = require("Module:languages").getByCode("grc")
local periods = {'cla', 'koi1', 'koi2', 'byz1', 'byz2'}
local inlinePeriods = {'cla', 'koi2', 'byz2'}
local function fetch(s, i)
--[==[
because we fetch a single character at a time so often
out of bounds fetch gives ''
]==]
i = tonumber(i)
if type(i) ~= "number" then
error("fetch requires a number or a string equivalent to a number as its second argument.")
end
if i == 0 then
return ""
end
local n = 0
for ch in s:gmatch(".[\128-\191]*") do
n = n + 1
if n == i then
return ch
end
end
return ""
end
--Combining diacritics are tricky.
local tie = U(0x35C) -- tie bar
local nonsyllabic = U(0x32F) -- combining inverted breve below
local voiceless = U(0x325) -- combining ring below
local aspirated = 'ʰ'
local macron = '¯'
local breve = '˘'
local subI = U(0x345)
local function is(text, X)
if not text or not X then
return false
end
local pattern = m_data.chars[X] or error('No data for "' .. X .. '".', 2)
if X == "frontDiphth" or X == "Greekdiacritic" then
pattern = "^" .. pattern .. "$"
else
pattern = "^[" .. pattern .. "]$"
end
return rfind(text, pattern)
end
local env_functions = {
preFront = function(term, index)
local letter1, letter2 = fetch(term, index + 1), fetch(term, index + 2)
return is(strip_accent(letter1), "frontVowel") or (is(strip_accent(letter1 .. letter2), "frontDiphth") and not is(letter2, "iDiaer"))
end,
isIDiphth = function(term, index)
local letter = fetch(term, index + 1)
return strip_accent(letter) == 'ι' and not m_data[letter].diaer
end,
isUDiphth = function(term, index)
local letter = fetch(term, index + 1)
return strip_accent(letter) == 'υ' and not m_data[letter].diaer
end,
hasMacronBreve = function(term, index)
local letter = fetch(term, index + 1)
return letter == macron or letter == breve
end,
hasSubI = function(term, index)
local letter = fetch(term, index + 1)
return letter == subI
end,
}
local function decode(condition, x, term)
--[==[
"If" and "and" statements.
Note that we're finding the last operator first,
which means that the first will get ultimately get decided first.
If + ("and") or / ("or") is found, the function is called again,
until if-statements are found.
In if-statements:
* A number represents the character under consideration:
-1 is the previous character, 0 is the current, and 1 is the next.
* Equals sign (=) checks to see if the character under consideration
is equal to a character.
* Period (.) plus a word sends the module to the corresponding entry
in the letter's data table.
* Tilde (~) calls a function on the character under consideration,
if the function exists.
]==]
if condition:find("/", nil, true) then -- logical or
for or_condition in gsplit(condition, "/", true, true) do
if decode(or_condition, x, term) then
return true
end
end
return false
elseif condition:find("+", nil, true) then -- logical and
for and_condition in gsplit(condition, "+", true, true) do
if not decode(and_condition, x, term) then
return false
end
end
return true
elseif condition:find("=", nil, true) then
local offset, ch = condition:match("^(.-)=(.*)$")
return ch == fetch(term, x + offset) -- out of bounds fetch gives ''
elseif condition:find(".", nil, true) then
local offset, quality = condition:match("^(.-)%.(.*)$")
local character = fetch(term, x + offset)
return m_data[character] and m_data[character][quality] or false
elseif condition:find("~", nil, true) then
local offset, func = condition:match("^(.-)~(.*)$")
return env_functions[func] and env_functions[func](term, x + offset) or false
end
end
local function check(p, x, term)
if type(p) ~= "table" then
return p
end
--This table is sequential, with a variable number of entries.
for _, possP in ipairs(p) do
if type(possP) ~= "table" then
return possP
end
--This table is paired, with two values: a condition and a result.
if decode(possP[1], x, term) then
return check(possP[2], x, term)
end
end
end
local function convert_term(term, periodstart)
if not term then error('The variable "term" in the function "convert_term" is nil.') end
local IPAs = {}
local start
local outPeriods = {}
if periodstart and periodstart ~= "" then
start = false
else
start = true
end
for _, period in ipairs(periods) do
if period == periodstart then
start = true
end
if start then
IPAs[period] = {}
insert(outPeriods, period)
end
end
local length, x, advance, letter, p = ulen(term), 1, 0, '', nil
while x <= length do
letter = fetch(term, x)
local data = m_data[letter]
if not data then -- no data found
-- explicit pass
else
-- check to see if a multicharacter search is warranted
advance = data.pre and check(data.pre, x, term) or 0
p = (advance ~= 0) and m_data[usub(term, x, x + advance)].p or data.p
for _, period in ipairs(outPeriods) do
insert(IPAs[period], check(p[period], x, term))
end
x = x + advance
end
x = x + 1
end
--Concatenate the IPAs
for _, period in ipairs(outPeriods) do
IPAs[period] = { IPA = concat(IPAs[period], '')}
end
return IPAs, outPeriods
end
local function find_syllable_break(word, nVowel, wordEnd)
if not word then error('The variable "word" in the function "find_syllable_break" is nil.') end
if wordEnd then
return ulen(word)
elseif is(fetch(word, nVowel - 1), "liquid") then
if is(fetch(word, nVowel - 2), "obst") then
return nVowel - 3
elseif fetch(word, nVowel - 2) == aspirated and is(fetch(word, nVowel - 3), "obst") then
return nVowel - 4
else
return nVowel - 2
end
elseif is(fetch(word, nVowel - 1), "cons") then
return nVowel - 2
elseif fetch(word, nVowel - 1) == aspirated and is(fetch(word, nVowel - 2), "obst") then
return nVowel - 3
elseif fetch(word, nVowel - 1) == voiceless and is(fetch(word, nVowel - 2), "liquid") then
return nVowel - 3
else
return nVowel - 1
end
end
local function syllabify_word(word)
local syllables = {}
--[[ cVowel means "current vowel", nVowel "next vowel",
sBreak "syllable break". ]]--
local cVowel, nVowel, sBreak, stress, wordEnd, searching
while word ~= '' do
cVowel, nVowel, sBreak, stress = false, false, false, false
--First thing is to find the first vowel.
searching = 1
local cVowelFound = false
while not cVowel do
local letter = fetch(word, searching)
local nextLetter = fetch(word, searching + 1)
if cVowelFound then
if (is(letter, "vowel") and nextLetter ~= nonsyllabic) or is(letter, "cons") or letter == '' or letter == 'ˈ' then
cVowel = searching - 1
elseif is(letter, "diacritic") then
searching = searching + 1
elseif letter == tie then
cVowelFound = false
searching = searching + 1
else
searching = searching + 1
end
else
if is(letter, "vowel") then
cVowelFound = true
elseif letter == 'ˈ' then
stress = true
end
searching = searching + 1
end
end
--Next we try and find the next vowel or the end.
searching = cVowel + 1
while (not nVowel) and (not wordEnd) do
local letter = fetch(word, searching)
if is(letter, "vowel") or letter == 'ˈ' then
nVowel = searching
elseif letter == '' then
wordEnd = true
else
searching = searching + 1
end
end
--Finally we find the syllable break point.
sBreak = find_syllable_break(word, nVowel, wordEnd)
--Pull everything up to and including the syllable Break.
local syllable = usub(word, 1, sBreak)
--If there is a stress accent, then we need to move it to the
--beginning of the syllable.
if stress then
syllable = 'ˈ' .. rsubn(syllable, 'ˈ', '')
stress = false
end
insert(syllables, syllable)
word = usub(word, sBreak + 1)
end
return (concat(syllables, '.'):gsub('%.ˈ', 'ˈ'))
end
local function syllabify(IPAs, periods)
--Syllabify
local word_ipa = ''
local ipa = {}
for _, period in ipairs(periods) do
ipa = {}
for _, word in ipairs(split(IPAs[period].IPA, ' ')) do
word_ipa = syllabify_word(word)
if word_ipa then
insert(ipa, word_ipa)
end
end
IPAs[period].IPA = concat(ipa, ' ')
end
return IPAs
end
local function make_ambig_note(ambig)
-- The table ambig is filled with all the ambiguous vowels that have been found in the term.
local ambig_note = ""
if ambig and #ambig > 0 then
-- Generate warning text in preview mode.
if is_preview() then
local s, pronoun
if #ambig > 1 then
s, pronoun = "s", "each one"
else
s, pronoun = "", "it"
end
ambig_note = ("\n<p>Mark the vowel length of the ambiguous vowel%s %s by adding a macron after %s if it is long, or a breve if it is short. By default, [[Module:grc-pronunciation]] assumes it is short if unmarked." ..
"<br/><small>[This message shows only in preview mode.]</small></p>\n"):format(s, list_to_text(ambig), pronoun)
end
ambig_note = ambig_note .. format_categories({"Ancient Greek terms with incomplete pronunciation"}, lang)
end
return ambig_note
end
local function make_table(IPAs, ambig, periods)
--Final format
local inlineProns = {}
local listOfProns = {}
local fullProns = {}
local periods2 = {}
for _, period in ipairs(periods) do
insert(fullProns, '* ' .. m_a.format_qualifiers(lang, {"grc-" .. period}) .. ' ' ..
m_IPA.format_IPA_full {
lang = lang,
items = {{pron = '/' .. IPAs[period].IPA .. '/'}},
})
periods2[period] = true
end
for _, period in ipairs(inlinePeriods) do
if periods2[period] then
local pron = '/' .. IPAs[period].IPA .. '/'
insert(inlineProns, {pron = pron})
insert(listOfProns, pron)
end
end
local inlineIPAlength = floor(max(ulen("AFA(kekunci): " .. concat(listOfProns, ' → ') or "") * 0.68, ulen("AFA (Bandar Konstatinople abad ke-15M) (kekunci): /" .. IPAs.byz2.IPA .. "/") * 0.68))
local inline = '\n<div class="vsShow" style="display:none">\n* ' .. m_IPA.format_IPA_full {
lang = lang,
items = inlineProns,
separator = " → ",
} .. "</div>"
local full = '\n<div class="vsHide">\n' .. concat(fullProns, '\n') .. make_ambig_note(ambig) .. '</div>'
return '<div class="vsSwitcher" data-toggle-category="sebutan" style="width: ' .. inlineIPAlength .. 'em; max-width:100%;"><span class="vsToggleElement" style="float: right;"> </span>' .. inline .. full .. '\n</div>'
end
local function get_IPA(term, period)
local ambig
if period == "cla" then
ambig = find_ambig(term)
end
term = canonicalize(ulower(term))
for alias, letter in pairs(m_data.aliases) do
term = term:gsub(alias, letter)
end
term = mark_implied_length(term)
local decomposed = toNFD(term)
if rfind(decomposed, "[εοηω]" .. diacritic .. "*[" .. diacritics.spacing_macron .. diacritics.spacing_breve .. diacritics.breve .. diacritics.macron .. "]") then
error("Macrons and breves cannot be placed after the letters ε, ο, η, or ω.")
end
term = rearrangeDiacritics(term)
local IPAs, periods = convert_term(term, period)
IPAs = syllabify(IPAs, periods)
return make_table(IPAs, ambig, periods)
end
function export.create(frame)
local args = require("Module:parameters").process(frame:getParent().args, {
[1] = {default = pagename},
["period"] = {default = "cla"},
})
return get_IPA(args[1], args.period)
end
function export.example(frame)
local args = require("Module:parameters").process(frame:getParent().args, {
[1] = true
})
local output = { '{| class="wikitable"' }
local terms = mw.text.split(args[1], ",%s+")
for _, term in pairs(terms) do
local period = rmatch(term, "%(period ?= ?([^%)]+)%)") or "cla"
local entry = rmatch(term, "([^%(]+) %(") or term or error('No term found in "' .. term .. '".')
local link = full_link(entry)
local IPA = get_IPA(entry, period)
insert(output, "\n|-\n| " .. link .. " || " .. IPA)
end
insert(output, "\n|}")
return concat(output)
end
return export
--Things we still need:
--Voicing of sigma around (after?) voiced stops.
--Proper alerts for editors, especially on ambiguous vowels.
ateywixrn57ssspolu8atynteontlgj
Modul:grc-accent
828
11662
379729
111960
2026-10-19T23:56:20Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/90598436|90598436]])
379729
Scribunto
text/plain
local export = {}
local m_data = mw.loadData("Module:grc-utilities/data")
local m_grc_utils = require("Module:grc-utilities")
local m_str_utils = require("Module:string utilities")
local m_table = require("Module:table")
local diacritic = m_data.diacritic
local diacritics = m_data.diacritics
local ACUTE = diacritics.acute
local GRAVE = diacritics.grave
local CIRCUMFLEX = diacritics.circum
local DIAERESIS = diacritics.diaeresis
local SMOOTH = diacritics.smooth
local ROUGH = diacritics.rough
local MACRON = diacritics.macron
local BREVE = diacritics.breve
local SUBSCRIPT = diacritics.subscript
local UNDERTIE = m_str_utils.char(0x035C)
local diacritic_pattern = m_data.all
local diacritic_groups = m_data.diacritic_groups
local tonal_diacritic = diacritic_groups[3] -- acute, grave, circumflex
local long_diacritics = MACRON .. SUBSCRIPT .. CIRCUMFLEX
local optional_length = m_data.length.optional
local either_vowel = "[ΑαΙιΥυ]"
local canonicalize = m_grc_utils.canonicalize
local concat = table.concat
local contains_vowel = m_grc_utils.contains_vowel
local find = m_str_utils.find
local gsub = m_str_utils.gsub
local insert = table.insert
local lower = m_str_utils.lower
local mark_implied_length -- defined below
local match = m_str_utils.match
local shallow_copy = m_table.shallowCopy
local standard_diacritics = m_grc_utils.standardDiacritics
local tag = m_grc_utils.tag
local tokenize = m_grc_utils.tokenize
local toNFD = mw.ustring.toNFD
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local function if_not_empty(var)
if var == "" then
return nil
else
return var
end
end
local function is_diphthong(token)
return find(token, "[ΑαΕεΗηΙιΟοΥυΩω]" .. optional_length .. DIAERESIS .. "?[ΙιΥυ]") and true or false
end
local libraryUtil = require('libraryUtil')
local checkType = libraryUtil.checkType
local checkTypeMulti = libraryUtil.checkTypeMulti
local function _check(funcName, expectType)
if type(expectType) == "string" then
return function(argIndex, arg, nilOk)
checkType(funcName, argIndex, arg, expectType, nilOk)
end
else
return function(argIndex, arg, expectType, nilOk)
if type(expectType) == "table" then
checkTypeMulti(funcName, argIndex, arg, expectType, nilOk)
else
checkType(funcName, argIndex, arg, expectType, nilOk)
end
end
end
end
--[[
A vowel with a breve or a lone epsilon or omicron is considered short.
Everything else is considered long, including unmarked alphas, iotas, and
upsilons. Sigh.
]]
local function is_short(token)
if find(token, BREVE) or find(token, '[ΕΟεο]') and not find(token, '[ιυ]') then
return true
else
return false
end
end
local function conditional_gsub(...)
local str, count = gsub(...)
if count and count > 0 then
return str
else
return nil
end
end
local accent_adding_functions = {
-- This will not throw an error if η or ω has a macron on it.
[CIRCUMFLEX] = function(vowel)
return (gsub(
vowel,
"([ΑαΗηΙιΥυΩω])" .. MACRON .. "?(" .. diacritic_groups[2] .. "?)(" .. SUBSCRIPT .. "?)$",
"%1%2" .. CIRCUMFLEX .. "%3"
))
end,
[ACUTE] = function(vowel)
return (
conditional_gsub(vowel,
"([Εε])([Ωω])",
"%1" .. ACUTE .. "%2") or
gsub(vowel,
"([ΑαΕεΗηΙιΟοΥυΩω]" .. optional_length .. diacritic_groups[2] .. "?)(" .. SUBSCRIPT .. "?)$",
"%1" .. ACUTE .. "%2"))
end,
[MACRON] = function(vowel)
if find(vowel, "[" .. long_diacritics .. "]") or is_diphthong(vowel) then
return vowel
elseif find(vowel, "[ΕΟεο]") then
error("The vowel " .. vowel ..
" is short, so a macron cannot be added to it.")
else
return (gsub(vowel, "(" .. either_vowel .. ")", "%1" .. MACRON))
end
end,
[BREVE] = function(vowel)
if find(vowel, "[" .. long_diacritics .. "]") then
error("The vowel " .. vowel ..
" has an iota subscript, a macron, or a circumflex, so a breve cannot be added to it.")
elseif is_diphthong(vowel) then
error("The vowel " .. vowel ..
" is a diphthong, so a breve cannot be added to it.")
else
return (gsub(vowel, "(" .. either_vowel .. ")", "%1" .. BREVE))
end
end,
-- This will insert a diaeresis on a single iota or upsilon, or on a
-- iota or upsilon that is the second element of a diphthong.
-- It does nothing if the vowel has a breathing on it.
[DIAERESIS] = function(vowel)
return (gsub(
vowel,
"([ΙιΥυ]" .. optional_length .. ")(" .. tonal_diacritic .. "?)$",
"%1" .. DIAERESIS .. "%2"
))
end
}
-- Assumes decomposed vowels (NFD).
local function add(vowel, accent)
if type(accent_adding_functions[accent]) == "function" then
return accent_adding_functions[accent](vowel)
end
error("No function for adding " .. accent)
end
function export.strip_accent(word)
word = toNFD(word)
-- Parentheses suppress second return value of gsub, the number of substitutions.
return (gsub(word, diacritic_pattern, ''))
end
function export.strip_tone(word)
word = toNFD(word)
if find(word, CIRCUMFLEX) then
word = shallow_copy(tokenize(word))
for i = 1, #word do
-- Add a macron to every vowel with a circumflex and remove the circumflex.
word[i] = gsub(word[i],
'^([αΑιΙυΥ])(' .. diacritic_groups[2] .. '*)' .. CIRCUMFLEX .. '$',
'%1' .. MACRON .. '%2')
end
word = table.concat(word)
end
return (gsub(word, tonal_diacritic, ''))
end
function export.ult(word)
word = toNFD(word)
if find(word, tonal_diacritic) then
return word
end
word = shallow_copy(tokenize(word))
for i, token in m_table.reverseIpairs(word) do
if contains_vowel(token) then
--fortunately accents go last in combining order
word[i] = add(token, ACUTE)
break
end
end
return concat(word, '')
end
--[[ WARNING: Given an unmarked α ι υ, this function will return a circmflex.
That said, if you ran into this situation in the first place, you probably
are doing something wrong. ]] --
function export.circ(word)
word = toNFD(word)
if find(word, tonal_diacritic) then
return word
end
word = shallow_copy(tokenize(word))
for i, token in m_table.reverseIpairs(word) do
if contains_vowel(token) then
if is_short(token) then
word[i] = add(token, ACUTE)
else
word[i] = add(token, CIRCUMFLEX)
end
break
end
end
return concat(word, '')
end
function export.penult(orig)
local word = toNFD(orig)
if find(word, tonal_diacritic) then
return word
end
word = shallow_copy(tokenize(word))
local syllables = 0
for i, token in m_table.reverseIpairs(word) do
if token == '-' then
return orig
elseif contains_vowel(token) then
syllables = syllables + 1
if syllables == 2 then
word[i] = add(token, ACUTE)
return concat(word, '')
end
end
end
return export.circ(orig)
end
function export.pencirc(orig)
local word = toNFD(orig)
if find(word, tonal_diacritic) then
return word
end
word = shallow_copy(tokenize(word))
local syllables = 0
local long_ult = false
for i, token in m_table.reverseIpairs(word) do
if token == '-' then return orig end
if contains_vowel(token) then
syllables = syllables + 1
if syllables == 1 and not is_short(token) then
long_ult = true
if word[#word] == 'αι' or word[#word] == 'οι' then long_ult = false end
elseif syllables == 2 then
if is_short(token) or long_ult then
word[i] = add(token, ACUTE)
else
word[i] = add(token, CIRCUMFLEX)
end
return concat(word, '')
end
end
end
return export.circ(orig)
end
function export.antepenult(orig)
local word = toNFD(orig)
if find(word, tonal_diacritic) then
return word
end
word = shallow_copy(tokenize(word))
local syllables = 0
local long_ult = false
for i, token in m_table.reverseIpairs(word) do
if token == '-' then return orig end
if contains_vowel(token) then
syllables = syllables + 1
if syllables == 1 and not is_short(token) then
long_ult = true
if word[#word] == 'αι' or word[#word] == 'οι' then long_ult = false end
elseif syllables == 2 and long_ult then
word[i] = add(token, ACUTE)
return concat(word, '')
elseif syllables == 3 then
word[i] = add(token, ACUTE)
return concat(word, '')
end
end
end
return export.pencirc(orig)
end
--[[
Counts from the beginning or end of the word, and returns the position and
type of the first accent found. Position means the number of vowels
(syllables) that have been encountered, not the number of characters.
Arguments:
- word: string (Ancient Greek word)
- from_end: boolean (whether to count from the end of the word)
]]
local accent_cache = {[true] = {}, [false] = {}}
function export.detect_accent(word, from_end)
local check = _check("detect_accent")
check(1, word, "string")
check(2, from_end, "boolean", true)
local cache = accent_cache[from_end == true][toNFD(word)]
if cache then
return unpack(cache)
end
local names = {
[ACUTE] = "acute",
[GRAVE] = "grave",
[CIRCUMFLEX] = "circumflex",
}
local syllable = 0
local accent_name
for _, token in (from_end and m_table.reverseIpairs or ipairs)(tokenize(word)) do
if contains_vowel(token) then
syllable = syllable + 1
accent_name = names[match(token, tonal_diacritic)]
if accent_name then
accent_cache[from_end == true][toNFD(word)] = {syllable, accent_name}
return syllable, accent_name
end
end
end
return nil
end
--[[
Returns classification based on first accent found
when traveling back from the end of the word.
]]
function export.get_accent_term(word)
local syllable, accent_name = export.detect_accent(word, true)
local terms = {
["grave"] = {"barytone"},
["acute"] = {"oxytone", "paroxytone", "proparoxytone"},
["circumflex"] = {"perispomenon", "properispomenon"},
}
local ordinals = {"first", "second", "third", "fourth", "fifth",}
local term
if syllable and accent_name then
term = terms[accent_name][syllable]
end
if term then
return term
else
return nil,
syllable and 'There is no term for a word with a ' .. accent_name ..
' accent on the ' .. ordinals[syllable] ..
' syllable from the end of the word.'
or 'No accent found.'
end
end
-- is_noun is a boolean or nil; if it is true, αι and οι will be
-- treated as short.
function export.get_length(token, short_diphthong)
local token = toNFD(lower(token))
if not contains_vowel(token) then
return nil
-- error("The thing supplied to get_length does not have any vowels")
end
-- η, ω; ᾳ, ῃ, ῳ; ᾱ, ῑ, ῡ; diphthongs
if find(token, "[ηω" .. long_diacritics .. "]") then
return "long"
end
if short_diphthong and find(token, "^[αο]ι") then
return "short"
end
if is_diphthong(token) then
return "long"
end
-- ε, ο; ᾰ, ῐ, ῠ
if find(token, "[εο" .. BREVE .. "]") then
return "short"
end
-- anything else
return "either"
end
-- Takes a table of tokens and returns a table containing tables of each vowel's
-- characteristics.
function export.get_vowel_info(tokens, short_diphthong)
if type(tokens) ~= "table" then
error("The argument to get_vowel_info must be a table.")
end
local vowels = {}
local vowel_i = 1
if find(tokens[#tokens], m_data.consonant .. "$") then
short_diphthong = false
end
for i, token in m_table.reverseIpairs(tokens) do
if contains_vowel(token) then
if vowel_i ~= 1 then
short_diphthong = false
end
local length, accent =
export.get_length(token, short_diphthong),
if_not_empty(match(token,
"[" .. ACUTE .. GRAVE .. CIRCUMFLEX .. "]"))
vowels[vowel_i] = {
index = i,
length = length,
accent = accent,
}
vowel_i = vowel_i + 1
end
end
return vowels
end
function export.mark_implied_length(word, return_tokens, short_diphthong)
word = toNFD(word)
-- Do nothing if there are no vowel letters that could be ambiguous.
if not find(word, either_vowel) then
if return_tokens then
return tokenize(word)
else
return word
end
end
local tokens = shallow_copy(tokenize(word))
local vowels = export.get_vowel_info(tokens, short_diphthong)
if #vowels >= 2 then
local ultima = vowels[1]
local ultima_i = ultima.index
local penult = vowels[2]
local penult_i = penult.index
if penult.length == "either" and ultima.length == "short" then
if penult.accent == CIRCUMFLEX then
tokens[penult_i] = add(tokens[penult_i], MACRON)
elseif penult.accent == ACUTE then
tokens[penult_i] = add(tokens[penult_i], BREVE)
end
elseif penult.length == "long" and ultima.length == "either" then
if penult.accent == CIRCUMFLEX then
tokens[ultima_i] = add(tokens[ultima_i], BREVE)
elseif penult.accent == ACUTE then
tokens[ultima_i] = add(tokens[ultima_i], MACRON)
end
end
local antepenult = vowels[3]
if antepenult and antepenult.accent and ultima.length == "either" then
tokens[ultima_i] = add(tokens[ultima_i], BREVE)
end
end
if return_tokens then
return tokens
else
return concat(tokens)
end
end
mark_implied_length = export.mark_implied_length
-- Returns a table of any ambiguous vowels in the text, language-tagged.
function export.find_ambig(text, noTag)
local lengthDiacritic = "[" .. MACRON .. BREVE .. CIRCUMFLEX .. SUBSCRIPT .. "]"
local aiu_diacritic = "^(" .. either_vowel .. ")(" .. diacritic .. "*)$"
-- breaks the word into units
local output, vowels = {}, {}
for _, token in ipairs(tokenize(mark_implied_length(standard_diacritics(text)))) do
local canonical = canonicalize(token)
if not match(canonical, m_data.consonant) then
local vowel, dia = match(canonical, aiu_diacritic)
if vowel and (dia == "" or not match(dia, lengthDiacritic)) then
insert(output, noTag and token or tag(token))
-- Lists the vowel letters that are ambiguous, for categorization purposes.
vowels[lower(vowel)] = true
end
end
end
return output, vowels
end
-- Returns the length of a syllable specified by its position from the end of the word.
function export.length_at(word, syllable)
local tokens = tokenize(word)
if type(word) ~= "string" then
error("First argument of length_at should be a string.")
end
if type(syllable) ~= "number" then
error("Second argument of length_at should be a number.")
end
local syllable_count = 0
for _, token in m_table.reverseIpairs(tokens) do
local length = export.get_length(token)
if length then
syllable_count = syllable_count + 1
if syllable_count == syllable then
return length
end
end
end
if syllable_count < syllable then
error("Length for syllable " .. syllable .. " from the end of the word was not found.")
end
end
local function find_breathing(token)
return match(token, "([" .. ROUGH .. SMOOTH .. "])")
end
local function has_same_breathing_as(token1, token2)
return find_breathing(token1) == find_breathing(token2)
end
-- Make token have the length specified by the string "length".
local function change_length(length, token)
local dia
if length == "long" then
dia = MACRON
elseif length == "short" then
dia = BREVE
end
if dia then
return add(token, dia)
else
return token
end
end
--[[
Take two words, mark implied length on each, then harmonize any macrons and
breves that disagree.
]]
function export.harmonize_length(word1, word2)
word1 = toNFD(word1)
-- Do nothing if there are no vowel letters that could be ambiguous.
if not (find(word1, either_vowel) or find(word2, either_vowel)) then
return word1, word2
end
local tokens1, tokens2 = mark_implied_length(word1, true), mark_implied_length(word2, true)
local strip1, strip2 = shallow_copy(tokenize(export.strip_accent(word1))), shallow_copy(tokenize(export.strip_accent(word2)))
for i, token1 in pairs(tokens1) do
local token2 = tokens2[i]
if strip1[i] == strip2[i] then
if has_same_breathing_as(token1, token2) then
local length1, length2 = export.get_length(token1), export.get_length(token2)
if length1 and length2 and length1 ~= length2 then
if length1 == "either" then
tokens1[i] = change_length(length2, token1)
elseif length2 == "either" then
tokens2[i] = change_length(length1, token2)
end
end
else
break
end
else
break
end
end
local new_word1, new_word2 = concat(tokens1), concat(tokens2)
return new_word1, new_word2
end
--[[
Get weight of nth syllable from end of word. Position defaults to 1, the last
syllable. Returns "heavy" or "light", or nil if syllable is open with an
ambiguous vowel.
]]
function export.get_weight(word, position)
if not if_not_empty(word) then
return nil
end
local tokens = tokenize(word)
if not position then
position = 1
end
local vowel
local vowel_index = 0
-- Find nth vowel from end of word.
for i, token in m_table.reverseIpairs(tokens) do
local length = export.get_length(token)
if length then
vowel_index = vowel_index + 1
if vowel_index == position then
vowel = {index = i, length = length}
break
end
end
end
if not vowel then
return nil
end
if vowel.length == "long" then
return "heavy"
else
-- Count consonants after the vowel.
local consonant_count = 0
for i = vowel.index + 1, #tokens do
if not contains_vowel(tokens[i]) then
consonant_count = consonant_count + 1
else
break
end
end
if consonant_count > 1 then
return "heavy"
elseif vowel.length == "short" then
return "light"
else
return nil
end
end
end
--[[
Add accent mark at position. Position is a number that refers to the nth
vowel from the beginning of the word. Respects the rules of accent.
Examples:
- δημος, 1 => δῆμος
- προτερᾱ, 1 => προτέρᾱ (position changed to 2 because ultima is long)
- μοιρα, 1, true => μοῖρα (circumflex can be added because ultima is
ambiguous)
- χωρᾱ, 1, true => χώρᾱ (circumflex can't be added because ultima
is long)
- τοιουτος, 2 => τοιοῦτος (circumflex because ultima is short)
Arguments:
- word: string (hopefully an Ancient Greek word or stem)
- syllable_position: number (less than the number of monophthongs or diphthongs
in the word)
- options: table
- circumflex boolean (add a circumflex if allowed)
- synaeresis boolean (accent can fall before εω in penult
and ultima: πόλεως)
- short_diphthong boolean (word-final οι, αι count as short)
]]
function export.add_accent(word, syllable_position, options)
local check = _check("add_accent")
check(1, word, "string")
check(2, syllable_position, "number")
check(3, options, "table", true)
word = toNFD(word)
if find(word, tonal_diacritic) then
return word
end
options = options or {}
local tokens = shallow_copy(tokenize(word))
local vowels = export.get_vowel_info(tokens, options.short_diphthong)
local vowel_count = #vowels
-- Convert positions in relation to the beginning of the word
-- to positions in relation to the end of the word.
-- The farthest back that an accent can be placed is 3 (the antepenult),
-- so that is the greatest allowed position.
if syllable_position > 0 then
syllable_position = math.min(3, vowel_count - syllable_position + 1)
-- If the position is in relation to the end of the word and it is greater
-- than the length of the word, then reduce it to the length of the word.
-- This is for practical reasons. Positions in relation to the beginning of
-- the word do not need leeway.
elseif syllable_position < 0 then
syllable_position = math.min(-syllable_position, vowel_count)
end
if syllable_position == 0 then
error("Invalid position value " .. syllable_position .. ".")
elseif syllable_position > vowel_count then
error("The position " .. syllable_position .. " is invalid, because the word has only " .. vowel_count .. " vowels.")
end
-- Apply accent rules to change the accent's position or type.
local accent_mark = options.circumflex and CIRCUMFLEX or ACUTE
local ultima = vowels[1]
-- If synaeresis is selected, a final vowel sequence εω (optionally
-- separated by an undertie) counts as one syllable.
if syllable_position == 3 then
local penult = vowels[2]
if not options.force_antepenult and (ultima.length == "long"
and not (options.synaeresis
and ("Ωω"):find(tokens[ultima.index], 1, true)
and ("Εε"):find(tokens[penult.index], 1, true)
and (ultima.index == penult.index + 1
or ultima.index == penult.index + 2
and tokens[penult.index + 1] == UNDERTIE))) then
syllable_position = 2
else
accent_mark = ACUTE
end
end
if syllable_position == 2 then
if ultima.length == "short" and vowels[2].length == "long" then
accent_mark = CIRCUMFLEX
elseif ultima.length == "long" then
accent_mark = ACUTE
end
end
local vowel = vowels[syllable_position]
if not vowel then
error('No vowel at position ' .. syllable_position ..
' from the end of the word ' .. word .. '.')
end
if vowel.length == "short" then
accent_mark = ACUTE
end
local i = vowel.index
tokens[i] = add(tokens[i], accent_mark)
return concat(tokens)
end
function export.syllables(word, func, number)
local check = _check('syllables')
check(1, word, 'string')
check(2, func, 'string', true)
check(3, number, 'number', true)
if not func then
error('No function specified')
end
local functions = {
eq = function (word, number)
local vowels = 0
for _, token in ipairs(tokenize(word)) do
if contains_vowel(token) then
vowels = vowels + 1
if vowels > number then
return false
end
end
end
if vowels == number then
return true
end
return false
end
}
func = functions[func]
if func then
return func(word, number)
else
error('No function ' .. func)
end
end
return export
ctcoupikea2d7tjrva0fq2qy1leyqwg
Modul:grc-pronunciation/data
828
11665
379728
212943
2026-10-19T23:54:39Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/89302842|89302842]])
379728
Scribunto
text/plain
local ipairs = ipairs
local pairs = pairs
local toNFD = mw.ustring.toNFD
local U = require("Module:string utilities").char
local m_utils_data = require("Module:grc-utilities/data")
local diacritics = m_utils_data.diacritics
local nonsyllabic = U(0x32F)
local high = U(0x341) -- combining acute tone mark
local low = U(0x340) -- combining grave tone mark
local midHigh = U(0x1DC4) -- mid–high pitch
local midLow = U(0x1DC6) -- mid–low pitch
local highMid = U(0x1DC7) -- high–mid pitch
local rising = U(0x30C) -- combining caron
local falling = diacritics.Latin_circum -- combining circumflex
local voiceless = U(0x325) -- combining ring below
local aspirated = "ʰ"
local stress_mark = "ˈ"
local long = "ː"
local macron = diacritics.spacing_macron
local breve = diacritics.spacing_breve
local circumflex_on_long_vowel = falling
local acute_on_long_vowel = rising
local acute_on_short_vowel = high
local grave_pitch_mark = low
local pre_hasMacronBreve = { { "0~hasMacronBreve", 1 }, 0 }
local pre_hasSubI = { { "0~hasSubI", 1 }, 0 }
local pre_hasMacronBreve_hasSubI = { { "0~hasMacronBreve/0~hasSubI", 1 }, 0 }
local data = {}
-- Letters which can be treated as allographs of other letters.
local aliases = {}
for k, v in pairs{
["κͱ"] = "χ",
["ξ"] = "κσ",
["ϝͱ"] = "ϝ̔",
["λͱ"] = "λ̔",
["πͱ"] = "φ",
["ϻ"] = "σ",
["ϙ"] = "κ",
["ϙͱ"] = "χ",
["ς"] = "σ",
["τͱ"] = "θ",
["ψ"] = "πσ"
} do
aliases[toNFD(k)] = toNFD(v)
end
data.aliases = aliases
data[" "] = {
["p"] = {
["cla"] = " ",
["koi1"] = " ",
["koi2"] = " ",
["byz1"] = " ",
["byz2"] = " ",
},
}
data["β"] = {
["p"] = {
["cla"] = "b",
["koi1"] = "b",
["koi2"] = {
{ "-1=μ", "b" },
"β"
},
["byz1"] = {
{ "-1=μ", "b" },
"v"
},
["byz2"] = {
{ "1=β", "" },
{ "-1=μ", "b" },
"v",
},
},
}
data["γ"] = {
["p"] = {
["cla"] = {
{ "1.dorsal/1=μ", "ŋ" },
"ɡ",
},
["koi1"] = {
{ "1.dorsal", "ŋ" },
"ɡ",
},
["koi2"] = {
{ "1.dorsal",
{
{ "1~preFront", "ɲ" },
"ŋ",
},
},
{ "0~preFront",
{
{ "-1=γ/-1=ν", "ɟ" },
"ʝ",
},
},
{ "-1=γ/-1=ν", "ɡ" },
"ɣ", },
["byz1"] = {
{ "1.dorsal",
{
{ "1~preFront", "ɲ" },
"ŋ",
},
},
{ "0~preFront",
{
{ "-1=γ/-1=ν", "ɟ" },
"ʝ",
},
},
{ "-1=γ/-1=ν", "ɡ" },
"ɣ",
},
["byz2"] = {
{ "1.dorsal",
{
{ "1~preFront", "ɲ" },
"ŋ",
},
},
{ "0~preFront",
{
{ "-1=γ/-1=ν", "ɟ" },
"ʝ",
},
},
{ "-1=γ/-1=ν", "ɡ" },
"ɣ",
},
},
}
data["δ"] = {
["p"] = {
["cla"] = "d",
["koi1"] = "d",
["koi2"] = {
{ "-1=ν", "d" },
"ð",
},
["byz1"] = {
{ "-1=ν", "d" },
"ð",
},
["byz2"] = {
{ "1=δ", "" },
{ "-1=ν", "d" },
"ð",
},
},
}
data["ϝ"] = {
["pre"] = {
{ "1=̓/1=̔", 1 },
0,
},
["p"] = {
-- "w" in (early) Epic and Aeolic
["cla"] = "",
["koi1"] = "",
["koi2"] = "",
["byz1"] = "",
["byz2"] = "",
},
}
data["ϝ̓"] = {
["p"] = data["ϝ"]["p"]
}
data["ϝ̔"] = {
["p"] = {
-- "ʍ" in (early) Epic and Aeolic
["cla"] = "h",
["koi1"] = "(h)",
["koi2"] = data["ϝ̓"].p.koi2,
["byz1"] = data["ϝ̓"].p.byz1,
["byz2"] = data["ϝ̓"].p.byz2,
},
}
data["ζ"] = {
["p"] = {
["cla"] = "zd",
["koi1"] = "z",
["koi2"] = "z",
["byz1"] = "z",
["byz2"] = {
{ "1=ζ", "" },
"z",
},
},
}
data["ͱ"] = {
["p"] = {
["cla"] = "h",
["koi1"] = "(h)",
["koi2"] = "",
["byz1"] = "",
["byz2"] = "",
},
}
data["θ"] = {
["p"] = {
["cla"] = "tʰ",
["koi1"] = "tʰ",
["koi2"] = "θ",
["byz1"] = "θ",
["byz2"] = {
{ "1=θ", "" },
"θ",
},
},
}
data["ϳ"] = {
["p"] = {
["cla"] = "j",
["koi1"] = "j",
["koi2"] = "j",
["byz1"] = "j",
["byz2"] = "j",
},
}
data["κ"] = {
["p"] = {
["cla"] = {
{ "1.voiced+1.stop", "ɡ" },
{ "1.aspirated", "kʰ" },
"k",
},
["koi1"] = {
{ "1.voiced+1.stop", "ɡ" },
"k",
},
["koi2"] = {
{ "1=κ", "" },
{ "1.voiced+1.stop", "ɡ" },
{ "-1=γ/-1=ν",
{
{ "0~preFront", "ɟ" },
{ "1.unvoiced", "k" },
"ɡ",
},
},
{ "0~preFront", "c" },
"k",
},
["byz1"] = {
{ "1=κ", "" },
{ "1.voiced+1.stop", "ɡ" },
{ "-1=γ/-1=ν",
{
{ "0~preFront", "ɟ" },
{ "1.unvoiced", "k" },
"ɡ",
},
},
{ "0~preFront", "c" },
"k",
},
["byz2"] = {
{ "1=κ", "" },
{ "1.voiced+1.stop", "ɡ" },
{ "-1=γ/-1=ν",
{
{ "0~preFront", "ɟ" },
{ "1.unvoiced", "k" },
"ɡ",
},
},
{ "0~preFront", "c" },
"k",
},
},
}
data["λ"] = {
["pre"] = {
{ "1=̓/1=̔", 1 },
0,
},
["p"] = {
["cla"] = {
{ "1=λ",
{
{ "2=̔", "l̥" },
"l",
},
},
{ "-1=̔",
{
{ "-2=λ", "l̥" },
"l",
},
},
"l",
},
["koi1"] = "l",
["koi2"] = "l",
["byz1"] = "l",
["byz2"] = {
{ "1=λ", "" },
"l",
},
},
}
data["λ̓"] = {
["p"] = {
["cla"] = {
{ "2=λ",
{
{ "3=̔", "l̥" },
"l",
},
},
"l",
},
["koi1"] = data["λ"].p.koi1,
["koi2"] = data["λ"].p.koi2,
["byz1"] = data["λ"].p.byz1,
["byz2"] = {
{ "2=λ", "" },
"l",
},
},
}
data["λ̔"] = {
["p"] = {
["cla"] = "l̥",
["koi1"] = data["λ̓"].p.koi1,
["koi2"] = data["λ̓"].p.koi2,
["byz1"] = data["λ̓"].p.byz1,
["byz2"] = data["λ̓"].p.byz2,
},
}
data["μ"] = {
["p"] = {
["cla"] = "m",
["koi1"] = "m",
["koi2"] = "m",
["byz1"] = "m",
["byz2"] = {
{ "1=μ", "" },
"m",
},
},
}
data["ν"] = {
["p"] = {
["cla"] = "n",
["koi1"] = "n",
["koi2"] = "n",
["byz1"] = "n",
["byz2"] = {
{ "1=ν", "" },
"n",
},
},
}
data["π"] = {
["p"] = {
["cla"] = {
{ "1.aspirated", "pʰ" },
"p",
},
["koi1"] = "p",
["koi2"] = "p",
["byz1"] = "p",
["byz2"] = {
{ "-1=μ",
{
{ "1.unvoiced", "p" },
"b",
},
},
{ "1=π", "" },
"p",
},
},
}
data["ρ"] = {
["p"] = {
["cla"] = {
{ "1=ρ/1=ῥ/-1=ρ/-1=ῤ/-1=ῥ", "r̥" },
"r",
},
["koi1"] = "r",
["koi2"] = "r",
["byz1"] = "r",
["byz2"] = {
{ "1=ρ/1=ῤ/1=ῥ", "" },
"r",
},
},
}
data["ῤ"] = {
["p"] = {
["cla"] = {
{ "1=ρ/1=ῥ", "r̥" },
"r",
},
["koi1"] = data["ρ"].p.koi1,
["koi2"] = data["ρ"].p.koi2,
["byz1"] = data["ρ"].p.byz1,
["byz2"] = data["ρ"].p.byz2,
},
}
data["ῥ"] = {
["p"] = {
["cla"] = "r̥",
["koi1"] = data["ῤ"].p.koi1,
["koi2"] = data["ῤ"].p.koi2,
["byz1"] = data["ῤ"].p.byz1,
["byz2"] = data["ῤ"].p.byz2,
},
}
data["σ"] = {
["p"] = {
["cla"] = {
{ "1.voiced", "z" },
"s",
},
["koi1"] = {
{ "1.voiced", "z" },
"s",
},
["koi2"] = {
{ "1.voiced", "z" },
"s",
},
["byz1"] = {
{ "1.voiced", "z" },
"s",
},
["byz2"] = {
{ "1=σ", "" },
{ "1.voiced", "z" },
"s",
},
},
}
data["τ"] = {
["pre"] = {
{ "1=ζ/1=σ", 1 },
0,
},
["p"] = {
["cla"] = {
{ "1.aspirated", "tʰ" },
"t",
},
["koi1"] = "t",
["koi2"] = "t",
["byz1"] = {
{ "1=ζ", "d" },
"t",
},
["byz2"] = {
{ "-1=ν", "d" },
{ "1=τ", "" },
"t",
},
},
}
data["τζ"] = {
["p"] = {
["cla"] = {
{ "2.unvoiced", "st" },
{ "-1=ν/2.voiced", "zd" },
"st",
},
["koi1"] = {
{ "2.unvoiced", "ts" },
{ "-1=ν/2.voiced", "dz" },
"ts",
},
["koi2"] = {
{ "2.unvoiced", "ts" },
{ "-1=ν/2.voiced", "dz" },
"ts",
},
["byz1"] = {
{ "2.unvoiced", "ts" },
{ "-1=ν/2.voiced", "dz" },
"ts",
},
["byz2"] = {
{ "2=ζ", "d" },
{ "2.unvoiced", "ts" },
{ "-1=ν/2.voiced", "dz" },
"ts",
},
},
}
data["τσ"] = {
["p"] = {
["cla"] = {
{ "2.voiced", "z" },
"s",
},
["koi1"] = {
{ "2.voiced", "dz" },
"ts",
},
["koi2"] = {
{ "2.voiced", "dz" },
"ts",
},
["byz1"] = {
{ "2.voiced", "dz" },
"ts",
},
["byz2"] = {
{ "2=σ", "t" },
{ "2.voiced", "dz" },
"ts",
},
},
}
data["φ"] = {
["p"] = {
["cla"] = "pʰ",
["koi1"] = "pʰ",
["koi2"] = "ɸ",
["byz1"] = "f",
["byz2"] = {
{ "1=φ", "" },
"f",
},
},
}
data["χ"] = {
["p"] = {
["cla"] = "kʰ",
["koi1"] = "kʰ",
["koi2"] = {
{ "1=χ", "" },
{ "0~preFront", "ç" },
"x" },
["byz1"] = {
{ "1=χ", "" },
{ "0~preFront", "ç" },
"x" },
["byz2"] = {
{ "1=χ", "" },
{ "0~preFront", "ç" },
"x",
},
},
}
data["α"] = {
["pre"] = {
{ "0~hasMacronBreve",
{
{ "1~isIDiphth/1~isUDiphth", 2 },
1,
},
},
{ "0~isIDiphth/0~isUDiphth", 1 },
0,
},
["p"] = {
["cla"] = "a",
["koi1"] = "a",
["koi2"] = "a",
["byz1"] = "a",
["byz2"] = "a",
},
}
data["ε"] = {
["pre"] = {
{ "0~isIDiphth/0~isUDiphth", 1 },
0,
},
["p"] = {
["cla"] = "e",
["koi1"] = "e",
["koi2"] = "e",
["byz1"] = "e",
["byz2"] = "e",
},
}
data["η"] = {
["pre"] = {
{ "0~isIDiphth/0~isUDiphth", 1 },
0,
},
["p"] = {
["cla"] = "ɛ",
["koi1"] = "e̝",
["koi2"] = "i",
["byz1"] = "i",
["byz2"] = "i",
},
}
data["ι"] = {
["p"] = {
["cla"] = "i",
["koi1"] = "i",
["koi2"] = "i",
["byz1"] = "i",
["byz2"] = "i",
},
}
data["ο"] = {
["pre"] = {
{ "0~isIDiphth/0~isUDiphth", 1 },
0,
},
["p"] = {
["cla"] = "o",
["koi1"] = "o",
["koi2"] = "o",
["byz1"] = "o",
["byz2"] = "o",
},
}
data["υ"] = {
["pre"] = {
{ "0~hasMacronBreve",
{
{ "1~isIDiphth", 2 },
1,
},
},
{ "0~isIDiphth/0~hasSubI", 1 },
0,
},
["p"] = {
["cla"] = "y",
["koi1"] = "y",
["koi2"] = "y",
["byz1"] = "y",
["byz2"] = "i",
},
}
data["ω"] = {
["pre"] = {
{ "0~isIDiphth/0~isUDiphth", 1 },
0,
},
["p"] = {
["cla"] = "ɔ",
["koi1"] = "o",
["koi2"] = "o",
["byz1"] = "o",
["byz2"] = "o",
},
}
local categories = {
[1] = {
["stop"] = { "β", "γ", "δ", "θ", "κ", "π", "τ", "φ", "χ" },
["dorsal"] = { "γ", "κ", "χ" },
["voiced"] = { "β", "γ", "δ", "ζ", "λ", "λ̓", "μ", "ν", "ρ", "ῤ" },
["unvoiced"] = { "ͱ", "θ", "κ", "λ̔", "π", "ῥ", "σ", "τ", "φ", "χ" },
["aspirated"] = { "ͱ", "θ", "φ", "χ" },
["diaer"] = { "ϊ", "ΐ", "ῒ", "ῗ", "ϋ", "ΰ", "ῢ", "ῧ", "ϋͅ", "ΰͅ", "ῢͅ", "ῧͅ" },
["subi"] = { "ᾳ", "ᾴ", "ᾲ", "ᾷ", "ᾀ", "ᾄ", "ᾂ", "ᾆ", "ᾁ", "ᾅ", "ᾃ", "ᾇ", "ῃ", "ῄ", "ῂ", "ῇ", "ᾐ", "ᾔ", "ᾒ", "ᾖ", "ᾑ", "ᾕ", "ᾓ", "ᾗ", "υͅ", "ύͅ", "ὺͅ", "ῦͅ", "ὐͅ", "ὔͅ", "ὒͅ", "ὖͅ", "ὑͅ", "ὕͅ", "ὓͅ", "ὗͅ", "ϋͅ", "ΰͅ", "ῢͅ", "ῧͅ", "ῳ", "ῴ", "ῲ", "ῷ", "ᾠ", "ᾤ", "ᾢ", "ᾦ", "ᾡ", "ᾥ", "ᾣ", "ᾧ" },
},
["type"] = {
["vowel"] = { "α", "ε", "η", "ι", "ο", "υ", "ω" }, -- Not currently used; if it were, it might need to include all the accented vowel characters.
["consonant"] = { "β", "γ", "δ", "ζ", "ͱ", "θ", "ϳ", "κ", "λ", "μ", "ν", "π", "ρ", "σ", "τ", "φ", "χ" },
["long"] = { "ᾱ", "η", "ῑ", "ῡ", "ω" },
["short"] = { "ᾰ", "ε", "ῐ", "ο", "ῠ" },
["either"] = { "α", "ι", "υ" },
["diacritic"] = { diacritics.macron, diacritics.spacing_macron, diacritics.modifier_macron, diacritics.breve, diacritics.spacing_breve, diacritics.rough, diacritics.smooth, diacritics.diaeresis, diacritics.acute, diacritics.grave, diacritics.circum, diacritics.Latin_circum, diacritics.coronis, diacritics.subscript },
},
["accent"] = {
["acute"] = { "ά", "ᾴ", "ἄ", "ᾄ", "ἅ", "ᾅ", "έ", "ἔ", "ἕ", "ή", "ῄ", "ἤ", "ᾔ", "ἥ", "ᾕ", "ί", "ἴ", "ἵ", "ΐ", "ό", "ὄ", "ὅ", "ύ", "ύͅ", "ὔ", "ὔͅ", "ὕ", "ὕͅ", "ΰ", "ΰͅ", "ώ", "ῴ", "ὤ", "ᾤ", "ὥ", "ᾥ" },
["grave"] = { "ὰ", "ᾲ", "ἂ", "ᾂ", "ἃ", "ᾃ", "ὲ", "ἒ", "ἓ", "ὴ", "ῂ", "ἢ", "ᾒ", "ἣ", "ᾓ", "ὶ", "ἲ", "ἳ", "ῒ", "ὸ", "ὂ", "ὃ", "ὺ", "ὺͅ", "ὒ", "ὒͅ", "ὓ", "ὓͅ", "ῢ", "ῢͅ", "ὼ", "ῲ", "ὢ", "ᾢ", "ὣ", "ᾣ" },
["circum"] = { "ᾶ", "ᾷ", "ἆ", "ᾆ", "ἇ", "ᾇ", "ῆ", "ῇ", "ἦ", "ᾖ", "ἧ", "ᾗ", "ῖ", "ἶ", "ἷ", "ῗ", "ῦ", "ῦͅ", "ὖ", "ὖͅ", "ὗ", "ὗͅ", "ῧ", "ῧͅ", "ῶ", "ῷ", "ὦ", "ᾦ", "ὧ", "ᾧ" },
},
["breath"] = {
["rough"] = { "ἁ", "ᾁ", "ἅ", "ᾅ", "ἃ", "ᾃ", "ἇ", "ᾇ", "ἑ", "ἕ", "ἓ", "ϝ̔", "ἡ", "ᾑ", "ἥ", "ᾕ", "ἣ", "ᾓ", "ἧ", "ᾗ", "ἱ", "ἵ", "ἳ", "ἷ", "λ̔", "ὁ", "ὅ", "ὃ", "ῥ", "ὑ", "ὑͅ", "ὕ", "ὕͅ", "ὓ", "ὓͅ", "ὗ", "ὗͅ", "ὡ", "ᾡ", "ὥ", "ᾥ", "ὣ", "ᾣ", "ὧ", "ᾧ" },
["smooth"] = { "ἀ", "ᾀ", "ἄ", "ᾄ", "ἂ", "ᾂ", "ἆ", "ᾆ", "ἐ", "ἔ", "ἒ", "ϝ̓", "ἠ", "ᾐ", "ἤ", "ᾔ", "ἢ", "ᾒ", "ἦ", "ᾖ", "ἰ", "ἴ", "ἲ", "ἶ", "λ̓", "ὀ", "ὄ", "ὂ", "ῤ", "ὐ", "ὐͅ", "ὔ", "ὔͅ", "ὒ", "ὒͅ", "ὖ", "ὖͅ", "ὠ", "ᾠ", "ὤ", "ᾤ", "ὢ", "ᾢ", "ὦ", "ᾦ" },
},
}
for key1, list in pairs(categories) do
for key2, letters in pairs(list) do
if type(key1) == "number" then
for _, letter in ipairs(letters) do
if not data[letter] then
data[letter] = {}
end
data[letter][key2] = true
end
elseif type(key1) == "string" then
for _, letter in ipairs(letters) do
if not data[letter] then
data[letter] = {}
end
data[letter][key1] = key2
end
end
end
end
local rough_breathing = { "h", "(h)" }
local smooth_breathing = { "", "" }
local function get_breathing(breathing)
return breathing == "rough" and rough_breathing or smooth_breathing
end
local function get_pitch_marks(accent_type, long)
if accent_type == "acute" then
if long then
return acute_on_long_vowel
else
return acute_on_short_vowel
end
elseif accent_type == "grave" then
return grave_pitch_mark
elseif accent_type == "circum" then
return circumflex_on_long_vowel
end
return ""
end
local function epsilon(breathing, accent)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, false)
return {
["cla"] = breathing[1] .. "e" .. pitch,
["koi1"] = breathing[2] .. stress .. "e",
["koi2"] = stress .. "e",
["byz1"] = stress .. "e",
["byz2"] = stress .. "e"
}
end
for _, letter in ipairs{ "ε", "έ", "ὲ", "ἐ", "ἔ", "ἒ", "ἑ", "ἕ", "ἓ" } do
local l_data = data[letter]
l_data.p = epsilon(l_data.breath, l_data.accent)
end
local function omicron(breathing, accent)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, false)
return {
["cla"] = breathing[1] .. "o" .. pitch,
["koi1"] = breathing[2] .. stress .. "o",
["koi2"] = stress .. "o",
["byz1"] = stress .. "o",
["byz2"] = stress .. "o"
}
end
for _, letter in ipairs{ "ο", "ό", "ὸ", "ὀ", "ὄ", "ὂ", "ὁ", "ὅ", "ὃ" } do
local l_data = data[letter]
l_data.p = omicron(l_data.breath, l_data.accent)
end
local function eta(breathing, accent, iota)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, true)
local offglide = iota and ("i" .. nonsyllabic) or ""
return {
["cla"] = breathing[1] .. "ɛ" .. pitch .. long .. offglide,
["koi1"] = breathing[2] .. stress .. "e̝",
["koi2"] = stress .. "i",
["byz1"] = stress .. "i",
["byz2"] = stress .. "i"
}
end
for _, letter in ipairs{ "η", "ῃ", "ή", "ῄ", "ὴ", "ῂ", "ῆ", "ῇ", "ἠ", "ᾐ", "ἤ", "ᾔ", "ἢ", "ᾒ", "ἦ", "ᾖ", "ἡ", "ᾑ", "ἥ", "ᾕ", "ἣ", "ᾓ", "ἧ", "ᾗ" } do
local l_data = data[letter]
l_data.p = eta(l_data.breath, l_data.accent, l_data.subi)
end
local function omega(breathing, accent, iota)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, true)
local offglide = iota and ("i" .. nonsyllabic) or ""
return {
["cla"] = breathing[1] .. "ɔ" .. pitch .. long .. offglide,
["koi1"] = breathing[2] .. stress .. "o",
["koi2"] = stress .. "o",
["byz1"] = stress .. "o",
["byz2"] = stress .. "o"
}
end
for _, letter in ipairs{ "ω", "ῳ", "ώ", "ῴ", "ὼ", "ῲ", "ῶ", "ῷ", "ὠ", "ᾠ", "ὤ", "ᾤ", "ὢ", "ᾢ", "ὦ", "ᾦ", "ὡ", "ᾡ", "ὥ", "ᾥ", "ὣ", "ᾣ", "ὧ", "ᾧ" } do
local l_data = data[letter]
l_data.p = omega(l_data.breath, l_data.accent, l_data.subi)
end
local function alpha(breathing, accent, iota, isLong)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, isLong)
local length = (isLong or iota or accent == "circum") and long or ""
local offglide = iota and ("i" .. nonsyllabic) or ""
return {
["cla"] = breathing[1] .. "a" .. pitch .. length .. offglide,
["koi1"] = breathing[2] .. stress .. "a",
["koi2"] = stress .. "a",
["byz1"] = stress .. "a",
["byz2"] = stress .. "a"
}
end
for _, letter in ipairs{ "α", "ᾳ", "ά", "ᾴ", "ὰ", "ᾲ", "ᾶ", "ᾷ", "ἀ", "ᾀ", "ἄ", "ᾄ", "ἂ", "ᾂ", "ἆ", "ᾆ", "ἁ", "ᾁ", "ἅ", "ᾅ", "ἃ", "ᾃ", "ἇ", "ᾇ" } do
local l_data = data[letter]
l_data.p = alpha(l_data.breath, l_data.accent, l_data.subi)
if not (l_data.subi or l_data.accent == "circum") then
if not l_data.pre then
l_data.pre = pre_hasMacronBreve
end
data[letter .. breve] = { p = l_data.p }
data[letter .. macron] = { p = alpha(l_data.breath, l_data.accent, false, true) }
end
end
local function iota(breathing, accent, isLong)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, isLong)
local length = (isLong or accent == "circum") and long or ""
return {
["cla"] = breathing[1] .. "i" .. pitch .. length,
["koi1"] = breathing[2] .. stress .. "i",
["koi2"] = stress .. "i",
["byz1"] = stress .. "i",
["byz2"] = stress .. "i"
}
end
local function ai(breathing, accent)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, true)
return {
["cla"] = breathing[1] .. "a" .. pitch .. "i" .. nonsyllabic,
["koi1"] = breathing[2] .. stress .. "ɛ",
["koi2"] = stress .. "ɛ",
["byz1"] = stress .. "e",
["byz2"] = stress .. "e"
}
end
local function ei(breathing, accent)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, true)
return {
["cla"] = breathing[1] .. "e" .. pitch .. long,
["koi1"] = breathing[2] .. stress .. "i",
["koi2"] = stress .. "i",
["byz1"] = stress .. "i",
["byz2"] = stress .. "i"
}
end
local function oi(breathing, accent)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, true)
return {
["cla"] = breathing[1] .. "o" .. pitch .. "i" .. nonsyllabic,
["koi1"] = breathing[2] .. stress .. "y",
["koi2"] = stress .. "y",
["byz1"] = stress .. "y",
["byz2"] = stress .. "i"
}
end
local function ui(breathing, accent)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, true)
return {
["cla"] = breathing[1] .. "y" .. pitch .. long,
["koi1"] = breathing[2] .. stress .. "y",
["koi2"] = stress .. "y",
["byz1"] = stress .. "y",
["byz2"] = stress .. "i"
}
end
for _, letter in ipairs{ "ι", "ί", "ὶ", "ῖ", "ἰ", "ἴ", "ἲ", "ἶ", "ἱ", "ἵ", "ἳ", "ἷ", "ϊ", "ΐ", "ῒ", "ῗ" } do
local l_data = data[letter]
l_data.p = iota(l_data.breath, l_data.accent)
if l_data.accent ~= "circum" then
l_data.pre = pre_hasMacronBreve
data[letter .. breve] = { p = l_data.p }
data[letter .. macron] = { p = iota(l_data.breath, l_data.accent, true) }
end
if not l_data.diar then
local ai_data = { p = ai(l_data.breath, l_data.accent) }
data["α" .. letter] = ai_data
data["α" .. breve .. letter] = ai_data
data["α" .. macron .. letter] = { p = alpha(l_data.breath, l_data.accent, true, true) }
data["ε" .. letter] = { p = ei(l_data.breath, l_data.accent) }
data["η" .. letter] = { p = eta(l_data.breath, l_data.accent, true) }
data["ο" .. letter] = { p = oi(l_data.breath, l_data.accent) }
local ui_data = { p = ui(l_data.breath, l_data.accent) }
data["υ" .. letter] = ui_data
data["υ" .. breve .. letter] = ui_data
data["υ" .. macron .. letter] = ui_data
data["ϋ" .. letter] = ui_data
data["ϋ" .. breve .. letter] = ui_data
data["ϋ" .. macron .. letter] = ui_data
data["ω" .. letter] = { p = omega(l_data.breath, l_data.accent, true) }
end
end
local function upsilon(breathing, accent, iota, isLong)
if iota then
return ui(breathing, accent)
end
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, isLong)
local length = (isLong or accent == "circum") and long or ""
return {
["cla"] = breathing[1] .. "y" .. pitch .. length,
["koi1"] = breathing[2] .. stress .. "y",
["koi2"] = stress .. "y",
["byz1"] = stress .. "y",
["byz2"] = stress .. "i"
}
end
local function au(breathing, accent, marked)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, true)
if marked then
return {
["cla"] = breathing[1] .. "a" .. pitch .. "u" .. nonsyllabic,
["koi1"] = {
{ "3=σ+4.voiced", breathing[2] .. stress .. "a" .. "w" },
{ "3.unvoiced", breathing[2] .. stress .. "a" .. "ʍ" },
breathing[2] .. stress .. "a" .. "w",
},
["koi2"] = {
{ "3=σ+4.voiced", stress .. "aβ" },
{ "3.unvoiced", stress .. "aɸ" },
stress .. "aβ",
},
["byz1"] = {
{ "3=σ+4.voiced/3=τ+4=ζ", stress .. "av" },
{ "3.unvoiced", stress .. "af" },
stress .. "av",
},
["byz2"] = {
{ "3=σ+4.voiced/3=τ+4=ζ", stress .. "av" },
{ "3.unvoiced", stress .. "af" },
stress .. "av",
},
}
end
return {
["cla"] = breathing[1] .. "a" .. pitch .. "u" .. nonsyllabic,
["koi1"] = {
{ "2=σ+3.voiced", breathing[2] .. stress .. "a" .. "w" },
{ "2.unvoiced", breathing[2] .. stress .. "a" .. "ʍ" },
breathing[2] .. stress .. "a" .. "w",
},
["koi2"] = {
{ "2=σ+3.voiced", stress .. "aβ" },
{ "2.unvoiced", stress .. "aɸ" },
stress .. "aβ",
},
["byz1"] = {
{ "2=σ+3.voiced/2=τ+3=ζ", stress .. "av" },
{ "2.unvoiced", stress .. "af" },
stress .. "av",
},
["byz2"] = {
{ "2=σ+3.voiced/2=τ+3=ζ", stress .. "av" },
{ "2.unvoiced", stress .. "af" },
stress .. "av",
},
}
end
local function aau(breathing, accent)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, true)
return {
["cla"] = breathing[1] .. "a" .. pitch .. "ːu" .. nonsyllabic,
["koi1"] = {
{ "3=σ+4.voiced", breathing[2] .. stress .. "a" .. "w" },
{ "3.unvoiced", breathing[2] .. stress .. "a" .. "ʍ" },
breathing[2] .. stress .. "a" .. "w",
},
["koi2"] = {
{ "3=σ+4.voiced", stress .. "aβ" },
{ "3.unvoiced", stress .. "aɸ" },
stress .. "aβ",
},
["byz1"] = {
{ "3=σ+4.voiced/3=τ+4=ζ", stress .. "av" },
{ "3.unvoiced", stress .. "af" },
stress .. "av",
},
["byz2"] = {
{ "3=σ+4.voiced/3=τ+4=ζ", stress .. "av" },
{ "3.unvoiced", stress .. "af" },
stress .. "av",
},
}
end
local function eu(breathing, accent)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, true)
return {
["cla"] = breathing[1] .. "e" .. pitch .. "u" .. nonsyllabic,
["koi1"] = {
{ "2=σ+3.voiced", breathing[2] .. stress .. "e" .. "w" },
{ "2.unvoiced", breathing[2] .. stress .. "e" .. "ʍ" },
breathing[2] .. stress .. "e" .. "w",
},
["koi2"] = {
{ "2=σ+3.voiced", stress .. "eβ" },
{ "2.unvoiced", stress .. "eɸ" },
stress .. "eβ",
},
["byz1"] = {
{ "2=σ+3.voiced/2=τ+3=ζ", stress .. "ev" },
{ "2.unvoiced", stress .. "ef" },
stress .. "ev",
},
["byz2"] = {
{ "2=σ+3.voiced/2=τ+3=ζ", stress .. "ev" },
{ "2.unvoiced", stress .. "ef" },
stress .. "ev",
},
}
end
local function hu(breathing, accent)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, true)
return {
["cla"] = breathing[1] .. "ɛ" .. pitch .. "ːu" .. nonsyllabic,
["koi1"] = {
{ "2=σ+3.voiced", breathing[2] .. stress .. "e̝" .. "w" },
{ "2.unvoiced", breathing[2] .. stress .. "e̝" .. "ʍ" },
breathing[2] .. stress .. "e̝" .. "w",
},
["koi2"] = {
{ "2=σ+3.voiced", stress .. "iβ" },
{ "2.unvoiced", stress .. "iɸ" },
stress .. "iβ",
},
["byz1"] = {
{ "2=σ+3.voiced/2=τ+3=ζ", stress .. "iv" },
{ "2.unvoiced", stress .. "if" },
stress .. "iv",
},
["byz2"] = {
{ "2=σ+3.voiced/2=τ+3=ζ", stress .. "iv" },
{ "2.unvoiced", stress .. "if" },
stress .. "iv",
},
}
end
local function ou(breathing, accent)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, true)
return {
["cla"] = breathing[1] .. "u" .. pitch .. long,
["koi1"] = breathing[2] .. stress .. "u",
["koi2"] = stress .. "u",
["byz1"] = stress .. "u",
["byz2"] = stress .. "u"
}
end
local function wu(breathing, accent)
local breathing = get_breathing(breathing)
local stress = accent and stress_mark or ""
local pitch = get_pitch_marks(accent, true)
return {
["cla"] = breathing[1] .. "ɔ" .. pitch .. "ːu" .. nonsyllabic,
["koi1"] = {
{ "2=σ+3.voiced", breathing[2] .. stress .. "o" .. "w" },
{ "2.unvoiced", breathing[2] .. stress .. "o" .. "ʍ" },
breathing[2] .. stress .. "o" .. "w",
},
["koi2"] = {
{ "2=σ+3.voiced", stress .. "oβ" },
{ "2.unvoiced", stress .. "oɸ" },
stress .. "oβ",
},
["byz1"] = {
{ "2=σ+3.voiced/2=τ+3=ζ", stress .. "ov" },
{ "2.unvoiced", stress .. "of" },
stress .. "ov",
},
["byz2"] = {
{ "2=σ+3.voiced/2=τ+3=ζ", stress .. "ov" },
{ "2.unvoiced", stress .. "of" },
stress .. "ov",
},
}
end
for _, letter in ipairs{ "υ", "υͅ", "ύ", "ύͅ", "ὺ", "ὺͅ", "ῦ", "ῦͅ", "ὐ", "ὐͅ", "ὔ", "ὔͅ", "ὒ", "ὒͅ", "ὖ", "ὖͅ", "ὑ", "ὑͅ", "ὕ", "ὕͅ", "ὓ", "ὓͅ", "ὗ", "ὗͅ", "ϋ", "ϋͅ", "ΰ", "ΰͅ", "ῢ", "ῢͅ", "ῧ", "ῧͅ" } do
local l_data = data[letter]
l_data.p = upsilon(l_data.breath, l_data.accent, l_data.subi)
if not l_data.subi then
if l_data.accent == "circum" then
l_data.pre = pre_hasSubI
else
if letter == "ϋ" then
l_data.pre = data["υ"].pre
elseif letter ~= "υ" then
l_data.pre = pre_hasMacronBreve_hasSubI
end
data[letter .. breve] = { p = l_data.p }
data[letter .. macron] = { p = upsilon(l_data.breath, l_data.accent, false, true) }
end
if not l_data.diar then
data["α" .. letter] = { p = au(l_data.breath, l_data.accent) }
data["α" .. breve .. letter] = { p = au(l_data.breath, l_data.accent, true) }
data["α" .. macron .. letter] = { p = aau(l_data.breath, l_data.accent) }
data["ε" .. letter] = { p = eu(l_data.breath, l_data.accent) }
data["η" .. letter] = { p = hu(l_data.breath, l_data.accent) }
data["ο" .. letter] = { p = ou(l_data.breath, l_data.accent) }
data["ω" .. letter] = { p = wu(l_data.breath, l_data.accent) }
end
end
end
data["chars"] = {
["cons"] = "bɡŋdzklmnprstβðɣɸθxfvɟʝcçwʍj",
["vowel"] = "aeiouyɛɔ",
["diacritic"] = high .. low .. midHigh .. midLow .. highMid .. long .. aspirated .. voiceless .. nonsyllabic .. rising .. falling,
["liquid"] = "rln",
["obst"] = "bɡdkptβðɣɸθxfv",
["frontDiphth"] = "[αο]ι",
["frontVowel"] = "ιηευ",
["iDiaer"] = "ϊΐῒῗ",
["long"] = "ηω",
["short"] = "εο",
["ambig"] = "αιυ",
["uDiphth"] = "αᾰᾱεηοω", -- first members for diphthongs ending in "υ"
["iDiphth"] = "αᾰᾱεηουῠῡω", -- first members for diphthongs ending in "ι"
["Greekdiacritic"] = m_utils_data.all,
["Greekconsonant"] = m_utils_data.consonants
}
data.chars.frontDiphthong = data.chars.frontDiphth
return data
rld1xydw59s6kaigbuu34l3lbs6j8k7
Templat:R:grc:LSJ
10
11672
379737
105158
2026-10-20T00:10:53Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Templat:R:LSJ]] ke [[Templat:R:grc:LSJ]]: Penyelarasan nama seperti versi Wikikamus Bahasa Inggeris
105158
wikitext
text/x-wiki
{{#invoke:R:Perseus|create}}<!--
-->{{cite-book<!--
-->|author=Liddell & Scott<!--
-->|title=[[w:A Greek-English Lexicon|A Greek–English Lexicon]]<!--
-->|publisher=Clarendon Press<!--
-->|location=Oxford<!--
-->|year=1940<!--
-->}}<!--
--><noinclude>{{documentation}}</noinclude>
gatv5ib4ifyyqkv2ux804m0ns4klehs
Modul:ru-headword
828
12165
379768
184041
2026-10-20T08:10:28Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/93514783|93514783]])
379768
Scribunto
text/plain
--[=[
This module implements the headword templates {{ru-noun}}, {{ru-adj}}, {{ru-adv}}, {{ru-noun+}}, etc. The main entry
point is show(), which is meant to be called from one of the above templates. However, {{ru-noun+}} uses the entry
point noun_plus(). When calling show(), the first parameter of the #invoke call is the part of speech. Other
parameters are taken from the parent template call.
The implementations for different types of headwords (different parts of speech) are set in pos_functions[POS] for a
given POS (part of speech).
]=]--
local export = {}
local force_cat = false -- for testing; if true, categories appear in non-mainspace pages
local lang = require("Module:languages").getByCode("ru")
local langname = lang:getCanonicalName()
local com = require("Module:ru-common")
local m_links = require("Module:links")
local m_table_tools = require("Module:table tools")
local require_when_needed = require("Module:require when needed")
local en_utilities_module = "Module:en-utilities"
local json_module = "Module:JSON"
local headword_module = "Module:headword"
local headword_utilities_module = "Module:headword utilities"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local m_en_utilities = require_when_needed(en_utilities_module)
local m_headword_utilities = require_when_needed(headword_utilities_module)
local m_string_utilities = require_when_needed(string_utilities_module)
local m_table = require_when_needed(table_module)
local glossary_link = require_when_needed(headword_utilities_module, "glossary_link")
local shallowCopy = require_when_needed(table_module, "shallowCopy")
local insertIfNot = require_when_needed(table_module, "insertIfNot")
local boolean_param = {type = "boolean"}
local list_param = {list = true, disallow_holes = true}
local list_comp = {list = "comp", disallow_holes = true}
local list_sup = {list = "sup", disallow_holes = true}
local u = m_string_utilities.char
local rfind = m_string_utilities.find
local rsubn = m_string_utilities.gsub
local rmatch = m_string_utilities.match
local rsplit = m_string_utilities.split
local concat = table.concat
local insert = table.insert
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local IRREGMARKER = "△"
local HYPMARKER = "⟐"
-- Forward references
local do_noun
local AC = u(0x0301) -- acute = ́
local pos_functions = {}
-- version of rsubn() that discards all but the first return value
local function rsub(term, foo, bar)
local retval = rsubn(term, foo, bar)
return retval
end
-- version of rsubn() that returns a 2nd argument boolean indicating whether
-- a substitution was made.
local function rsubb(term, foo, bar)
local retval, nsubs = rsubn(term, foo, bar)
return retval, nsubs > 0
end
local function track(page)
require("Module:debug/track")("ru-headword/" .. page)
return true
end
-- Clone args while also assigning nil to empty strings.
local function clone_args(in_args)
local args = {}
for pname, param in pairs(in_args) do
if param == "" then args[pname] = nil
else args[pname] = param
end
end
return args
end
local function make_qualifier_text(text)
return require("Module:qualifier").format_qualifier(text)
end
-- Split a list of "RUSSIAN" or "RUSSIAN//TRANSLIT" strings into a list of {RUSSIAN, TRANSLIT} objects.
local function split_list_into_russian_tr(list)
local splitlist = {}
for _, item in ipairs(list) do
insert(splitlist, com.split_russian_tr(item, "dopair"))
end
return splitlist
end
local function check_if_accent_needed(termobj, data, is_head)
local val = termobj.term
local val_no_links = m_links.remove_links(val)
if com.needs_accents(val_no_links) then
if not termobj.unknown_stress and not termobj.nostress then
error("Stress must be supplied using an acute accent or ё: '" .. val_no_links .. "' (use inline modifier <unknown_stress:1> if stress is unknown and <nostress:1> if a multisyllable word really has no stress, as in ''до́ смерти'')")
end
if not termobj.nostress then
local pos = require(en_utilities_module).singularize(data.pos_category)
insert(data.categories, "Requests for accents in " .. langname .. " " .. pos .. " entries")
end
end
if com.is_multi_stressed(val_no_links) then
error("Multi-stressed form '" .. val_no_links .. "' not allowed")
end
end
-- Parse the forms of an inflection. The raw arguments are specified in `forms`, a list of forms which are parsed for
-- inline modifiers. Multiple comma-separated values are allowed.
local function parse_inflection_forms(data, forms, field, is_head)
local include_mods = is_head and {
"tr",
{"nostress", {type = "boolean"}},
{"unknown_stress", {type = "boolean"}},
} or {
"tr",
"t",
"pos",
"ng",
"g",
{"nostress", {type = "boolean"}},
{"unknown_stress", {type = "boolean"}},
}
local retobjs = m_headword_utilities.parse_term_list_with_modifiers {
forms = forms,
paramname = field,
splitchar = ",",
is_head = is_head,
include_mods = include_mods,
}
for _, retobj in ipairs(retobjs) do
if is_head and retobj.term == "+" then
retobj.term = data.pagename
end
check_if_accent_needed(retobj, data, is_head)
if is_head then
-- Catch errors in arguments where headword doesn't match page title; for the moment, do only with
-- tracking.
local head_no_links = m_links.remove_links(retobj.term)
local head_noaccent = com.remove_accents(head_no_links)
if head_noaccent ~= data.pagename then
track("bad-headword")
--error("Headword " .. term .. " doesn't match pagename " .. data.pagename)
end
end
end
return retobjs
end
-- Parse an inflection. The raw arguments come from `args[field]`, which is parsed for inline modifiers. Multiple
-- comma-separated values are allowed.
local function parse_inflection(data, args, field, is_head)
local argfield = field
if type(argfield) == "table" then
argfield = argfield[1]
end
return parse_inflection_forms(data, args[argfield], field, is_head)
end
-- Add a full inflection (e.g. genitive singular of nouns, abstract noun of adjectives) to `data.inflections`. `label`
-- is the label of the inflection (e.g. "abstract noun"), which can have <<...>> glossary references. `terms` is a list
-- of term objects. `accel_form` is the accelerator form (e.g. "gen|s" for genitive singular) of the inflection, or nil
-- to add no accelerator. `accel_pos` is the part of speech of the inflection, if different from the lemma.
--
-- This is a wrapper around insert_inflection() in [[Module:headword utilities]], but handles hypothetical markers in
-- the term (which are converted into the `hypothetical` face) as well as the fact that the accelerator spec may differ
-- from term to term if we need to specify a target (i.e. the value of |head= used in {{head}} or similar) that's
-- different from the term itself. This happens in particular in comparative forms, where the term reads e.g.
-- "([[покраснее|по]])[[краснее|красне́е]]" but we want the target to be just красне́е.
local function insert_inflection(data, terms, label, accel_form, accel_pos)
if not terms[1] then
return
end
for _, termobj in ipairs(terms) do
local term = termobj.term
local tr = termobj.tr
local sawhyp_term, sawhyp_tr
term, sawhyp_term = rsubb(term, HYPMARKER, "")
if tr then
tr, sawhyp_tr = rsubb(tr, HYPMARKER, "")
end
local accel
local target_term = termobj.target_term or term
-- If the target was given without a translit, don't fall back to the term translit because the target itself
-- may be different from the term.
local target_tr = termobj.target_tr or not termobj.target_term and tr or nil
if accel_form then
local lemmas = {}
local lemma_translits = {}
for i, headobj in ipairs(data.heads) do
lemmas[i] = headobj.term
lemma_translits[i] = headobj.tr
end
accel = {
form = accel_form, lemma = lemmas, lemma_translit = lemma_translits, pos = accel_pos,
target = target_term, translit = target_tr
}
end
termobj.term = term
-- Currently we don't display translits for inflections, so null out any manual translit. If we want to change
-- this, we need to set enable_auto_translit in the call to insert_inflection().
termobj.tr = nil
termobj.face = (sawhyp_term or sawhyp_tr) and "hypothetical" or nil
termobj.accel = accel
if com.needs_accents(m_links.remove_links(target_term)) then
local pos = require(en_utilities_module).singularize(data.pos_category)
insert(data.categories, "Requests for accents in " .. langname .. " " .. pos .. " entries")
end
end
m_headword_utilities.insert_inflection {
headdata = data,
terms = terms,
label = label,
}
end
-- Insert a fixed label `label` into the inflections for `data`. If `originating_term` is supplied, copy the decorations
-- from it into the fixed label.
local function insert_fixed_inflection(data, label, originating_term)
m_headword_utilities.insert_fixed_inflection {
headdata = data,
originating_term = originating_term,
label = label,
}
end
-- Parse and insert an inflection not requiring additional processing into `data.inflections`. The raw arguments come
-- from `args[field]`, which is parsed for inline modifiers. Multiple comma-separated values are allowed. `label` is the
-- label that the inflections are given; sections enclosed in <<...>> are linked to the glossary. `accel_form` is the
-- accelerator form (or nil), and `accel_pos` if specified overrides the accelerator part of speech. Note that
-- [[Module:headword utilities]] provides a parse_and_insert_inflection() that is usually sufficient, but in our case
-- we do a bunch of additional processing when inserting inflections, so we do the parsing and inserting separately
-- using functions defined above.
local function parse_and_insert_inflection(data, args, field, label, accel_form, accel_pos)
local terms = parse_inflection(data, args, field)
insert_inflection(data, terms, label, accel_form, accel_pos)
end
local function get_split_decomposed_heads(data)
if not data.split_decomposed_heads then
data.split_decomposed_heads = com.split_translit_of_duplicate_termobjs_and_decompose(data.heads)
end
return data.split_decomposed_heads
end
local function add_common_all_pos_params(params)
params["notrcat"] = boolean_param -- don't add 'irregular pronunciations' tracking category
params["pagename"] = true
params["id"] = true
return params
end
-- The main entry point.
function export.show(frame)
local iparams = {
[1] = {required = true, desc = "part of speech"},
}
local iargs = require("Module:parameters").process(frame.args, iparams)
local poscat = iargs[1]
local params = add_common_all_pos_params {
[1] = {list = "head", disallow_holes = true},
["tr"] = {list = true, allow_holes = true, replaced_by = false,
instead = "use <tr:...> inline modifier on the headword"},
}
if pos_functions[poscat] then
for key, val in pairs(pos_functions[poscat].params) do
params[key] = val
end
end
local parargs = frame:getParent().args
local args = require("Module:parameters").process(parargs, params)
local pagename = args.pagename or mw.loadData("Module:headword/data").pagename
local data = {
lang = lang,
no_redundant_head_cat = true,
pos_category = poscat,
categories = {},
genders = {},
inflections = {},
id = args.id,
pagename = pagename,
}
data.heads = parse_inflection(data, args, {1, "head"}, "is_head")
if not data.heads[1] then
data.heads = {{term = pagename}}
end
local unknown_stress = false
for i, headobj in ipairs(data.heads) do
unknown_stress = unknown_stress or headobj.unknown_stress
local tr = headobj.tr
if tr then
tr = com.decompose(tr)
local tr_gen = com.translit_no_links(headobj.term)
if tr == tr_gen then
track("redundant-translit")
elseif not args.notrcat then
insert(data.categories, langname .. " terms with irregular pronunciations")
end
end
end
if pos_functions[poscat] then
pos_functions[poscat].func(args, data)
end
if unknown_stress then
track("unknown-stress")
if not pos_functions[poscat] or not pos_functions[poscat].no_insert_unknown_stress_label or
not pos_functions[poscat].no_insert_unknown_stress_label(args) then
insert(data.inflections, {label = "unknown stress"})
end
end
return require(headword_module).full_headword(data)
end
-- Common params shared by {{ru-noun}} and {{ru-noun+}}.
local function add_common_noun_params(params)
params["unknown_decl"] = boolean_param -- declension unknown
params["unknown_pattern"] = boolean_param -- stress pattern (a, b, b', ...) unknown
params["unknown_gender"] = boolean_param -- gender unknown
params["unknown_animacy"] = boolean_param -- animacy unknown
params["unknown_stress"] = boolean_param -- stress unknown
params["f"] = list_param -- feminine equivalent(s)
params["m"] = list_param -- masculine equivalent(s)
params["adj"] = list_param -- relational adjective(s)
params["poss"] = list_param -- possessive adjective(s)
params["coll"] = list_param -- collective(s)
params["dim"] = list_param -- diminutive(s)
params["aug"] = list_param -- augmentative(s)
params["hypocor"] = list_param -- hypocorism(s)
params["pej"] = list_param -- pejorative(s)
params["pejcoll"] = list_param -- pejorative collective(s)
params["clip"] = list_param -- clipping(s)
params["expr"] = list_param -- expressive term(s)
params["dem"] = list_param -- demonym(s)
params["fdem"] = list_param -- female demonym(s)
params["sgl"] = list_param -- singulative(s)
return params
end
-- Implementation of {{ru-noun+}}. We should redo this implementation along the lines of {{uk-ndecl}}. For example,
-- instead of existing {{ru-noun-table|[[дви́гатель]]|m|_|[[внутренний|вну́треннего]]|+$|_|[[сгорание|сгора́ния]]|$}}, it
-- should look more like {{ru-ndecl|дви́гатель<M> [[внутренний|вну́треннего]] [[сгорание|сгора́ния]]}}.
function export.noun_plus(frame)
local iparams = {
[1] = {required = true, desc = "part of speech"},
["old"] = boolean_param,
["ndef"] = {},
}
local iargs = require("Module:parameters").process(frame.args, iparams)
local poscat = iargs[1]
local params = add_common_noun_params(add_common_all_pos_params {
["g"] = {list = true, disallow_holes = true, type = "genders", flatten = true}, -- genders
["notes"] = list_param, -- "footnotes" displayed after headword
})
local parargs = frame:getParent().args
local headword_args, args = require("Module:parameters").process(parargs, params, "return unknown")
args = clone_args(args)
-- default value of n=, used in ru-proper noun+ where ndef=sg is set
args.ndef = args.ndef or iargs.ndef
local m_noun = require("Module:ru-noun")
args = m_noun.do_generate_forms(args, iargs.old)
local pagename = args.pagename or mw.loadData("Module:headword/data").pagename
local data = {
lang = lang,
no_redundant_head_cat = true,
pos_category = poscat,
categories = {},
genders = {},
inflections = {},
id = args.id,
pagename = pagename,
}
-- Do explicit genders using g=, g2=, etc.
data.genders = headword_args.g
-- If none, do inferred or explicit genders taken from declension.
if not data.genders[1] then
local gender_specs = {}
for _, g in ipairs(args.genders) do
insert(gender_specs, {spec = g})
end
data.genders = gender_specs
end
local saw_note = false
-- Given a list of {RU, TR} pairs, where TR may be nil, separate off the footnote symbols from RU and TR; link the
-- remainder if it's not already linked; remove monosyllabic accents (but not from multiword expressions); and
-- return the result in termobj format, i.e. {term = RU, tr = TR}.
local function convert_paired_terms_to_termobjs(paired_list, ishead)
if not paired_list or not paired_list[1] then
return {{term = "-"}}
end
local termobjs = {}
for _, x in ipairs(paired_list) do
local ru, tr = x[1], x[2]
if not ishead and (rfind(ru, "[%[|%]]") or tr and rfind(tr, "[%[|%]]")) then
track("form-with-link")
end
local ruentry, runotes = m_table_tools.separate_notes(ru)
local sawhyp
runotes = rsub(runotes, IRREGMARKER, "") -- remove note of irregularity
runotes, sawhyp = rsubb(runotes, HYPMARKER, "")
if runotes ~= "" then
saw_note = true
end
runotes = m_table_tools.superscript_notes(runotes)
local trentry, trnotes
if tr then
trentry, trnotes = m_table_tools.separate_notes(tr)
trnotes = rsub(trnotes, IRREGMARKER, "") -- remove note of irregularity
trnotes = m_table_tools.superscript_notes(trnotes)
end
ruentry, trentry = com.remove_monosyllabic_accents(ruentry, trentry)
if sawhyp then
insert(termobjs,
{term = ruentry .. runotes .. HYPMARKER, tr = trentry and trentry .. trnotes .. HYPMARKER or nil}
)
elseif ishead then
insert(termobjs,
{term = ruentry .. runotes, tr = trentry and trentry .. trnotes or nil})
else
local ruspan, trspan
if ruentry == "-" then
ruspan = "-"
elseif rfind(ruentry, "[%[|%]]") then
-- don't add links around a form that's already linked
ruspan = ruentry .. runotes
else
ruspan = "[[" .. ruentry .. "]]" .. runotes
end
if trentry then
trspan = trentry .. trnotes
end
insert(termobjs, {term = ruspan, tr = trspan})
end
end
return com.combine_translit_of_duplicate_termobjs_and_recompose(termobjs)
end
local argsn = args.n or args.ndef
local heads, genitives, plurals, genpls
if argsn == "p" then
heads = convert_paired_terms_to_termobjs(args.nom_pl_linked, "ishead")
genitives = convert_paired_terms_to_termobjs(args.gen_pl)
plurals = {{term = "-"}}
genpls = {{term = "-"}}
else
heads = convert_paired_terms_to_termobjs(args.nom_sg_linked, "ishead")
genitives = convert_paired_terms_to_termobjs(args.gen_sg)
plurals = argsn == "s" and {{term = "-"}} or convert_paired_terms_to_termobjs(args.nom_pl)
genpls = argsn == "s" and {{term = "-"}} or convert_paired_terms_to_termobjs(args.gen_pl)
end
data.heads = heads
if com.any_termobjs_have_translit(data.heads) and not args.notrcat then
insert(data.categories, langname .. " terms with irregular pronunciations")
end
do_noun(data, headword_args, argsn == "s", genitives, plurals, genpls, poscat)
local notes = headword_args.notes
local notes_segments = {}
if saw_note then
for _, note in ipairs(notes) do
insert(notes_segments, " " .. make_qualifier_text(note))
end
end
local notes_text = concat(notes_segments, "")
return require(headword_module).full_headword(data) .. notes_text
end
-- Implementation of {{ru-noun}} and {{ru-proper noun}}.
local function get_noun_pos(pos)
return {
params = add_common_noun_params({
[2] = {list = "g", disallow_holes = true, required = true, default = "?", type = "genders",
flatten = true}, -- genders
[3] = {list = "gen", disallow_holes = true}, -- genitive singulars, or - for indeclinable
[4] = {list = "pl", disallow_holes = true}, -- nominative plurals
[5] = {list = "genpl", disallow_holes = true}, -- genitive plurals
["altyo"] = boolean_param, -- called from {{ru-noun-alt-ё}} or variants
["manual"] = boolean_param, -- allow manual specification of principal parts
}),
func = function(args, data)
data.genders = args[2]
local genitives = parse_inflection(data, args, {3, "gen"})
local plurals = parse_inflection(data, args, {4, "pl"})
local genpls = parse_inflection(data, args, {5, "genpl"})
local unknown_stress = false
for _, headobj in ipairs(data.heads) do
unknown_stress = unknown_stress or headobj.unknown_stress
end
if not args.altyo and not args.manual and (not genitives[1] or genitives[1].term ~= "-") and
mw.title.getCurrentTitle().nsText == "" and
not args.unknown_decl and not unknown_stress and not args.unknown_stress and
not args.unknown_pattern and not args.unknown_gender and
not args.unknown_animacy then
error("[[Template:ru-noun]] can now only be used with indeclinable and manually-declined nouns; use [[Template:ru-noun+]] instead")
end
do_noun(data, args, pos == "proper nouns", genitives, plurals, genpls, pos)
end,
no_insert_unknown_stress_label = function(args)
return true -- we do it ourselves
end,
}
end
pos_functions["proper nouns"] = get_noun_pos("proper nouns")
pos_functions["pronouns"] = get_noun_pos("pronouns")
-- Display additional inflection information for a noun.
pos_functions["nouns"] = get_noun_pos("nouns")
-- Guts of {{ru-noun}} and {{ru-noun+}}.
do_noun = function(data, args, no_plural, genitives, plurals, genitive_plurals, pos)
local recognized_genders = {
"", -- not allowed when singular; this is needed because some invariant plural-only words have no gender to speak of
"m",
"f",
"n",
"mf",
"mfbysense",
}
local recognized_animacies = {
"",
"?",
"an",
"in",
}
local recognized_numbers = {
"",
"p",
}
local function insert_if_not_blank(seq, part)
if part ~= "" then
insert(seq, part)
end
end
local singular_genders = {} -- a set
local plural_genders = {} -- a set
-- Generate the allowed gender/number/animacy specs.
for _, number in ipairs(recognized_numbers) do
for _, gender in ipairs(recognized_genders) do
for _, animacy in ipairs(recognized_animacies) do
local set = number == "" and singular_genders or plural_genders
if gender ~= "" or number == "p" then -- disallow blank gender unless plural
local gender_number = {}
insert_if_not_blank(gender_number, gender)
insert_if_not_blank(gender_number, animacy)
insert_if_not_blank(gender_number, number)
local spec = concat(gender_number, "-")
set[spec] = true
end
end
end
end
for _, gspec in ipairs(data.genders) do
local g = gspec.spec
if g == "m" then
g = "m-?"
elseif g == "m-p" then
g = "m-?-p"
elseif g == "f" and plurals[1] ~= "-" and not no_plural then
g = "f-?"
elseif g == "f-p" then
g = "f-?-p"
elseif g == "p" then
g = "?-p"
end
if not singular_genders[g] and not plural_genders[g] and g ~= "?" and g ~= "?-in" and g ~= "?-an" then
error("Unrecognized gender: " .. g)
end
gspec.spec = g
-- Categorize by number
if plural_genders[g] then
if g == "?-p" or g == "an-p" or g == "in-p" then
insert(data.categories, langname .. " pluralia tantum with incomplete gender")
end
end
end
-- Add the genitive forms
if genitives[1] and genitives[1].term == "-" then
insert(data.inflections, {label = glossary_link("indeclinable")})
insert(data.categories, langname .. " indeclinable nouns")
else
insert_inflection(data, genitives, "genitive")
end
local plural_only = not not plural_genders[data.genders[1].spec]
-- Add the plural forms.
if genitives[1] and genitives[1].term == "-" then
if plurals[1] or genitive_plurals[1] then
error("Can't specify nominative or genitive plurals of a plural-only term")
end
elseif plural_genders[data.genders[1].spec] then
insert(data.inflections, {label = glossary_link("plural only")})
elseif plurals[1] and plurals[1].term == "-" then
if pos ~= "proper nouns" then
insert(data.inflections, {label = glossary_link("uncountable")})
insert(data.categories, langname .. " uncountable nouns")
end
else
insert_inflection(data, plurals, "nominative plural")
insert_inflection(data, genitive_plurals, "genitive plural")
end
-- Parse and insert an inflection not requiring additional processing into `data.inflections`. The raw arguments
-- come from `args[field]`, which is parsed for inline modifiers. `label` is the label that the inflections are
-- given; <<..>> in the label is linked to the glossary). `accel_form` is the accelerator form, or nil.
local function handle_infl(field, label, accel_form)
parse_and_insert_inflection(data, args, field, label, accel_form)
end
-- Add the masculine forms; intentionally no accelerator as the masculine forms are lemmas and need manual handling
handle_infl("m", "male equivalent")
-- Add the feminine forms
handle_infl("f", "female equivalent", "f")
-- Add the relational adjective forms; intentionally no accelerator, need manual handling
handle_infl("adj", "<<relational adjective>>")
-- Add the possessive adjective forms; intentionally no accelerator, need manual handling
handle_infl("poss", "<<possessive adjective>>")
-- Add the collective forms; intentionally no accelerator, need manual handling
handle_infl("coll", "<<collective>>")
-- Add the diminutive forms
handle_infl("dim", "<<diminutive>>", "diminutive")
-- Add the augmentative forms
handle_infl("aug", "<<augmentative>>", "augmentative")
-- Add the hypocorisms
handle_infl("hypocor", "<<hypocorism>>", "hypocorism")
-- Add the pejorative forms
handle_infl("pej", "<<pejorative>>", "pejorative")
-- Add the pejorative collective forms; intentionally no accelerator, need manual handling
handle_infl("pejcoll", "<<pejorative>> <<collective>>")
-- Add the clippings
handle_infl("clip", "<<clipping>>", "clipping")
-- Add the expressive terms
handle_infl("expr", "<<expressive>> term", "expressive term")
-- Add the demonyms
handle_infl("dem", "<<demonym>>", "demonym")
-- Add the female demonyms
handle_infl("fdem", "female <<demonym>>", "female demonym")
-- Add the singulatives
handle_infl("sgl", "<<singulative>>", "singulative")
if args.unknown_decl then
track("unknown-decl")
insert(data.inflections, {label = "unknown declension"})
end
local unknown_stress = false
for _, headobj in ipairs(data.heads) do
unknown_stress = unknown_stress or headobj.unknown_stress
end
if unknown_stress or args.unknown_stress then
track("unknown-stress")
insert(data.inflections, {label = "unknown stress"})
end
if args.unknown_pattern then
track("unknown-pattern")
insert(data.inflections, {label = "unknown accent pattern"})
end
if args.unknown_gender then
track("unknown-gender")
insert(data.inflections, {label = "unknown gender"})
end
if args.unknown_animacy then
track("unknown-animacy")
insert(data.inflections, {label = "unknown animacy"})
end
end
local function generate_informal_comp(compobj)
local ru, tr = compobj.term, compobj.tr
if rfind(ru, "е́?е$") then
ru, tr = com.strip_ending(ru, tr, "е") -- Cyrillic е
compobj = shallowCopy(compobj)
compobj.term, compobj.tr = com.concat_russian_tr(ru, tr, "й", nil)
return compobj
else
return nil
end
end
local function convert_to_po_variant(compobj)
local ru, tr = compobj.term, compobj.tr
if rfind(ru, "е$") or rfind(ru, "е́?й$") then
ru = "[[по" .. ru .. "|(по)]][[" .. ru .. "]]"
tr = tr and "(po)" .. tr or nil
compobj.term, compobj.tr = ru, tr
end
end
local function convert_to_nai_variant(compobj)
local ru, tr = compobj.term, compobj.tr
if ru:find("^наи") then
-- e.g. наибо́льший, наиме́ньший
return
end
ru = "[[наи" .. ru .. "|(наи)]][[" .. ru .. "]]"
tr = tr and "(nai)" .. tr or nil
compobj.term, compobj.tr = ru, tr
end
local allowed_endings = {
"ый",
"ий",
"о́й",
--old
"ій",
-- last two for adverbs
"о",
"о́",
}
local velar_to_translit = {
["к"] = "k",
["г"] = "g",
["х"] = "x"
}
local velar_to_palatal = {
["к"] = "ч",
["г"] = "ж",
["х"] = "ш",
["k"] = "č",
["g"] = "ž",
["x"] = "š"
}
-- Generate the comparative(s) given the positive(s). `positives` is a list of term objects, with the translit already
-- split and decomposed. `compspecobj` is the term object containing the comparative spec (either + or a spec giving an
-- adjectival accent pattern, such as +c'). If + is given, the default is +a unless the positive is ending-stressed, in
-- which case the default is +b. Return value is a list of term objects.
local function generate_comparative(positives, compspecobj)
local comps = {}
local compspec = compspecobj.term
if compspecobj.tr then
error("Can't specify manual translit with a '+...' comparative spec")
end
if not rfind(compspec, "^%+") then
error("Compspec '" .. compspec .. "' must begin with + in this function")
end
if compspec ~= "+" and not rfind(compspec, "^%+[abc]'*$") then
error("Compspec '" .. compspec .. "' has an illegal format, should be e.g. + or +c''")
end
compspec = rsub(compspec, "^%+", "")
for _, positive in ipairs(positives) do
local ru, tr = positive.term, positive.tr
ru = m_links.remove_links(ru)
local removed_ending = false
for _, allowed_ending in ipairs(allowed_endings) do
if rfind(ru, allowed_ending .. "$") then
if allowed_ending == "о́й" or allowed_ending == "о́" then
if compspec == "a" then
error("Short stress pattern a not allowed with ending-stressed adjectives/adverbs")
elseif compspec == "" then
compspec = "b"
end
end
ru, tr = com.strip_ending(ru, tr, allowed_ending)
removed_ending = true
break
end
end
if not removed_ending then
error("Russian '" .. ru .. "' doesn't end with expected ending")
end
local comp, comptr
if rfind(ru, "[кгх]$") then
local stemru, lastruchar = rmatch(ru, "^(.*)(.)$")
local stemtr, lasttrchar
if tr then
stemtr, lasttrchar = rmatch(tr, "^(.*)(.)$")
if velar_to_translit[lastruchar] ~= lasttrchar then
error("Translit '" .. tr .. "' doesn't end with transliterated equivalent of last char '" ..
lastruchar .. "' of Russian '" .. ru .. "'")
end
end
comp, comptr = com.make_ending_stressed(stemru, stemtr)
comp = comp .. velar_to_palatal[lastruchar] .. "е" -- Cyrillic е
if comptr then
comptr = comptr .. velar_to_palatal[lasttrchar] .. "e" -- Latin e
end
elseif compspec == "" or compspec == "a" then
comp = ru .. "ее" -- Cyrillic ее
if comptr then
comptr = tr .. "ee" -- Latin ee
end
else -- end-stressed comparative, including pattern a'
comp, comptr = com.make_unstressed_once(ru, tr)
comp = comp .. "е́е" -- Cyrillic е́е
if comptr then
comptr = comptr .. "e" .. AC .. "e" -- Latin decomposed ée
end
end
local compobj = shallowCopy(positive)
compobj.term = comp
compobj.tr = comptr
m_headword_utilities.combine_termobj_decorations(compobj, compspecobj)
insert(comps, compobj)
end
return comps
end
-- Meant to be called from a bot
function export.generate_comparative(frame)
local iparams = {
[1] = {required = true, desc = "comparative"},
[2] = {default = "+"},
}
local iargs = require("Module:parameters").process(frame.args, iparams)
local comps = iargs[1]
local compspec = iargs[2]
comps = rsplit(comps, ",")
for i, comp in ipairs(comps) do
local ru, tr = com.split_russian_tr(comp)
comps[i] = {term = ru, tr = tr}
end
comps = generate_comparative(comps, {term = compspec})
comps = com.combine_translit_of_duplicate_termobjs_and_recompose(comps)
return require(json_module).toJSON(comps)
end
-- Handle comparative and superlative inflections. If an explicit form is given such as коро́че or красне́е, we add
-- it in a "hacked" format that notes that e.g. покоро́че or покрасне́е is a possible variant. We also generate an
-- informal form in -ей if possible, e.g. красне́й, with по-hacking applied (but no such variant is possible for коро́че).
-- We also handle autogenerating comparatives when specified as + or +b, +c'', etc. (All specifications with an accent
-- pattern are equivalent other than +a.) Finally, we allow and handle periphrastic comparatives noted using "peri".
local function handle_comparatives_and_superlatives(data, args)
local noinf = args.noinf
local comps = parse_inflection(data, args, {2, "comp"})
comps = com.split_translit_of_duplicate_termobjs_and_decompose(comps)
if comps[1] and comps[1].term == "-" then
local nocomp = table.remove(comps, 1)
if comps[1] then
insert_fixed_inflection(data, "not generally <<comparable>>", nocomp)
else
insert_fixed_inflection(data, "no <<comparative>>", nocomp)
end
track("nocomp")
end
if comps[1] then
local comp_parts = {}
local function insert_comp(compobj)
local informal
if not noinf then
informal = generate_informal_comp(compobj)
if informal then
convert_to_po_variant(informal)
end
end
convert_to_po_variant(compobj)
m_headword_utilities.insert_termobj_combining_duplicates(comp_parts, compobj)
if informal then
m_headword_utilities.insert_termobj_combining_duplicates(comp_parts, informal)
end
end
for _, compobj in ipairs(comps) do
local term = compobj.term
if term == "peri" then
if compobj.tr then
error("Can't specify manual translit with 'peri' comparative spec")
end
for _, positive in ipairs(get_split_decomposed_heads(data)) do
local pericomp = shallowCopy(positive)
pericomp.term, pericomp.tr = com.concat_russian_tr("[[бо́лее]] ", nil, pericomp.term, pericomp.tr)
m_headword_utilities.combine_termobj_decorations(pericomp, compobj)
m_headword_utilities.insert_termobj_combining_duplicates(comp_parts, pericomp)
end
track("pericomp")
elseif rfind(term, "^+") then
local autocomps = generate_comparative(get_split_decomposed_heads(data), compobj)
for _, autocomp in ipairs(autocomps) do
insert_comp(autocomp)
end
else
insert_comp(compobj)
end
end
comp_parts = com.combine_translit_of_duplicate_termobjs_and_recompose(comp_parts)
for _, compobj in ipairs(comp_parts) do
local ru, tr = compobj.term, compobj.tr
-- WARNING: This has intimate knowledge of how convert_to_po_variant() works. To avoid this, we could
-- maintain the un-po-hacked target in each form in comp_parts, but then we'd have to modify
-- com.combine_translit_of_duplicate_termobjs_and_recompose() to preserve the extra target info when
-- combining duplicate forms, or use a map from hacked Russian form to target.
local un_po_hacked_ru = m_links.remove_links(rsub(ru, "^%[%[.-%]%]", ""))
local un_po_hacked_tr = tr and rsub(tr, rsub(tr, "^%(po%)", ""), ", %(po%)", ", ") or nil
compobj.target_term = un_po_hacked_ru
compobj.target_tr = un_po_hacked_tr
end
insert_inflection(data, comp_parts, "<<comparative>>", "comparative")
end
-- Add the superlatives
local sups = parse_inflection(data, args, {3, "sup"})
sups = com.split_translit_of_duplicate_termobjs_and_decompose(sups)
if sups[1] then
local sup_parts = {}
for _, supobj in ipairs(sups) do
local term = supobj.term
if term == "peri" then
if supobj.tr then
error("Can't specify manual translit with 'peri' superlative spec")
end
for _, positive in ipairs(get_split_decomposed_heads(data)) do
local perisup = shallowCopy(positive)
perisup.term, perisup.tr = com.concat_russian_tr("[[са́мый]] ", nil, perisup.term, perisup.tr)
m_headword_utilities.combine_termobj_decorations(perisup, supobj)
m_headword_utilities.insert_termobj_combining_duplicates(sup_parts, perisup)
end
track("perisup")
else
convert_to_nai_variant(supobj)
m_headword_utilities.insert_termobj_combining_duplicates(sup_parts, supobj)
end
end
sup_parts = com.combine_translit_of_duplicate_termobjs_and_recompose(sup_parts)
for _, supobj in ipairs(sup_parts) do
local ru, tr = supobj.term, supobj.tr
-- WARNING: This has intimate knowledge of how convert_to_nai_variant() works. To avoid this, we could
-- maintain the un-nai-hacked target in each form in sup_parts, but then we'd have to modify
-- com.combine_translit_of_duplicate_termobjs_and_recompose() to preserve the extra target info when
-- combining duplicate forms, or use a map from hacked Russian form to target.
local un_nai_hacked_ru = m_links.remove_links(rsub(ru, "^%[%[.-%]%]", ""))
local un_nai_hacked_tr = tr and rsub(tr, rsub(tr, "^%(nai%)", ""), ", %(nai%)", ", ") or nil
supobj.target_term = un_nai_hacked_ru
supobj.target_tr = un_nai_hacked_tr
end
insert_inflection(data, sup_parts, "<<superlative>>", "superlative")
end
end
-- Display additional inflection information for an adjective
pos_functions["adjectives"] = {
params = {
["indecl"] = boolean_param, --indeclinable
["noinf"] = boolean_param, --suppress informal comparatives
[2] = list_comp, --comparative(s)
[3] = list_sup, --superlative(s)
["adv"] = list_param, --corresponding adverb(s)
["absn"] = list_param, --corresponding abstract noun(s)
["dim"] = list_param, --corresponding diminutive(s)
["aug"] = list_param, --corresponding augmentative(s)
["intens"] = list_param, --corresponding intensive(s)
["attenu"] = list_param, --corresponding attenuative(s)
["pej"] = list_param, --corresponding pejorative(s)
["expr"] = list_param, --corresponding expressive term(s)
},
func = function(args, data)
if args.indecl then
insert(data.inflections, {label = "indeclinable"})
insert(data.categories, langname .. " indeclinable adjectives")
end
handle_comparatives_and_superlatives(data, args)
local function parse_and_insert_adj_inflection(field, label, accel_form, accel_pos)
parse_and_insert_inflection(data, args, field, label, accel_form, accel_pos)
end
-- Add the adverbs
parse_and_insert_adj_inflection("adv", "adverb")
-- Add the abstract nouns
local absn_objs = parse_inflection(data, args, "absn")
local saw_plus = false
for _, absnobj in ipairs(absn_objs) do
if absnobj.term == "+" then
saw_plus = true
break
end
end
if saw_plus then
local normalized_absn_objs = {}
absn_objs = com.split_translit_of_duplicate_termobjs_and_decompose(absn_objs)
for _, absnobj in ipairs(absn_objs) do
if absnobj.term == "+" then
if absnobj.tr then
error("Can't specify manual translit with '+' default abstract noun spec")
end
local lemmas = get_split_decomposed_heads(data)
for _, lemma in ipairs(lemmas) do
local ru, tr = lemma.term, lemma.tr
if rfind(ru, "о́?й$") then
error("Can't form default abstract noun of ending-stressed adjective " .. ru)
end
if rfind(ru, "ий$") then
ru, tr = com.strip_ending(ru, tr, "ий")
elseif rfind(ru, "ій$") then
ru, tr = com.strip_ending(ru, tr, "ій")
else
ru, tr = com.strip_ending(ru, tr, "ый")
end
ru, tr = com.concat_russian_tr(ru, tr, "ость", nil)
local lemma_absn = shallowCopy(lemma)
lemma_absn.term, lemma_absn.tr = ru, tr
m_headword_utilities.combine_termobj_decorations(lemma_absn, absnobj)
m_headword_utilities.insert_termobj_combining_duplicates(normalized_absn_objs, lemma_absn)
end
else
m_headword_utilities.insert_termobj_combining_duplicates(normalized_absn_objs, absnobj)
end
end
normalized_absn_objs = com.combine_translit_of_duplicate_termobjs_and_recompose(normalized_absn_objs)
insert_inflection(data, normalized_absn_objs, "abstract noun", "abstract noun", "noun")
else
insert_inflection(data, absn_objs, "abstract noun", "abstract noun", "noun")
end
-- Add the diminutives
parse_and_insert_adj_inflection("dim", "<<diminutive>>", "diminutive")
-- Add the augmentatives
parse_and_insert_adj_inflection("aug", "<<augmentative>>", "augmentative")
-- Add the intensives
parse_and_insert_adj_inflection("intens", "<<intensive>>", "intensive")
-- Add the attenuatives
parse_and_insert_adj_inflection("attenu", "<<attenuative>>", "attenuative")
-- Add the pejoratives
parse_and_insert_adj_inflection("pej", "<<pejorative>>", "pejorative")
-- Add the expressive terms
parse_and_insert_adj_inflection("expr", "<<expressive>> term", "expressive")
end
}
-- Display additional inflection information for an adverb
pos_functions["adverbs"] = {
params = {
["noinf"] = boolean_param, --suppress informal comparatives
[2] = list_comp, --comparative(s)
[3] = list_sup, --superlative(s)
["adj"] = list_param, --corresponding adjective(s)
["dim"] = list_param, --corresponding diminutive(s)
["aug"] = list_param, --corresponding augmentative(s)
["intens"] = list_param, --corresponding intensive(s)
["attenu"] = list_param, --corresponding attenuative(s)
["pej"] = list_param, --corresponding pejorative(s)
["expr"] = list_param, --corresponding expressive term(s)
},
func = function(args, data)
local comps = args[2]
handle_comparatives_and_superlatives(data, args)
local function parse_and_insert_adv_inflection(field, label, accel_form)
parse_and_insert_inflection(data, args, field, label, accel_form)
end
-- Add the adjectives
parse_and_insert_adv_inflection("adj", "<<adjective>>", "adjective")
-- Add the diminutives
parse_and_insert_adv_inflection("dim", "<<diminutive>>", "diminutive")
-- Add the augmentatives
parse_and_insert_adv_inflection("aug", "<<augmentative>>", "augmentative")
-- Add the intensives
parse_and_insert_adv_inflection("intens", "<<intensive>>", "intensive")
-- Add the attenuatives
parse_and_insert_adv_inflection("attenu", "<<attenuative>>", "attenuative")
-- Add the pejoratives
parse_and_insert_adv_inflection("pej", "<<pejorative>>", "pejorative")
-- Add the expressive terms
parse_and_insert_adv_inflection("expr", "<<expressive>> term", "expressive")
end
}
-- Display additional inflection information for a verb and verbal combining form
local function get_verb_pos(pos)
return {
params = {
[2] = {required = true, default = "?"}, --aspect
["impf"] = list_param, -- imperfective(s),
["pf"] = list_param, -- perfective(s),
["vn"] = list_param, -- verbal noun(s),
["vadj"] = list_param, -- verbal adjective(s),
["vadv"] = list_param, -- verbal adverb(s),
},
func = function(args, data)
local cform = pos == "verbal combining forms"
if cform then
insert(data.categories, "Russian verbs")
end
-- Aspect
local aspect = args[2]
if aspect == "both" then
insert(data.genders, "biasp")
elseif aspect == "pf" or aspect == "impf" or aspect == "biasp" or aspect == "?" then
insert(data.genders, aspect)
else
error("Invalid Russian verb aspect '" .. aspect .. "', should be 'pf', 'impf', 'both', 'biasp' or '?'")
end
local function add_verb_forms(field, label, accel_form, accel_pos)
parse_and_insert_inflection(data, args, field, label, accel_form, accel_pos)
end
-- Add the imperfective forms; intentionally no accelerator, need manual handling
if args.impf[1] and aspect == "impf" then
error("Can't specify imperfective counterparts for an imperfective verb")
end
add_verb_forms("impf", "imperfective")
-- Add the perfective forms; intentionally no accelerator, need manual handling
if args.pf[1] and aspect == "pf" then
error("Can't specify perfective counterparts for a perfective verb")
end
add_verb_forms("pf", "perfective")
-- Add the verbal nouns
add_verb_forms("vn", "verbal noun", "verbal noun", "noun")
-- Add the verbal adjectives
add_verb_forms("vadj", "verbal adjective", "verbal adjective", "adjective")
-- Add the verbal adverbs
add_verb_forms("vadv", "verbal adverb", "verbal adverb", "adverb")
end,
}
end
pos_functions["verbs"] = get_verb_pos("verbs")
pos_functions["verbal combining forms"] = get_verb_pos("verbal combining forms")
return export
pv622rid8nsuzljziolbeht5zhjngux
Modul:ru-noun
828
12166
379770
184042
2026-10-20T08:13:07Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/92933401|92933401]])
379770
Scribunto
text/plain
--[=[
This module contains functions for creating inflection tables for Russian
nouns.
Author: Benwing, rewritten from early version by Wikitiki89
Form of arguments: One of the following:
1. LEMMA|DECL|PLSTEM (all arguments optional)
2. ACCENT|LEMMA|DECL|PLSTEM (all arguments optional)
3. multiple sets of arguments separated by the literal word "or"
Arguments:
ACCENT: Accent pattern (a b c d e f b' d' f' f''). Multiple values can
be specified, separated by commas. If omitted, defaults to a or b
depending on the position of stress on the lemma or
explicitly-specified declension.
LEMMA: Lemma form (i.e. nom sg or nom pl), with appropriately-placed
stress; or the stem, if an explicit declension is specified
(in this case, the declension usually looks like an ending, and
the stem is the portion of the lemma minus the ending). In the
first argument set (i.e. first set of arguments separated
by "or"), defaults to page name; in later sets, defaults to lemma
of previous set. A plural form can be given, and causes argument
n= to default to n=p (plural only). Normally, an accent is
required if multisyllabic, and unaccented monosyllables with
automatically be stressed; prefix with * to override both behaviors.
DECL: Declension field. Normally omitted to autodetect based on the
lemma form; see below.
PLSTEM: special plural stem (defaults to stem of lemma)
Additional named arguments:
a: animacy (a/an/anim = animate, i/in/inan = inanimate, b/bi/both/ai = both
(listing animate first in the headword), ia = both (listing inanimate
first in the headword), otherwise inanimate)
n: number restriction (p = plural only, s = singular only, b = both;
defaults to both unless the lemma is plural, in which case it
defaults to plural only)
CASE_NUM or acc_NUM_ANIM or par/loc/voc: override (or multiple
values separated by commas) for case/number combination;
forms auto-linked; can have raw links in it, can have an
ending "note" (*, +, 1, 2, 3, etc.)
pltail: Specify something (usually a * or similar) to attach to
the end of the last plural form when there's more than one. Used in
conjunction with notes= to indicate that alternative plural forms
are obsolete, poetic, etc.
pltailall: Similar pltail= but attaches to all plural forms.
Typically used in conjunction with notes= to make a comment about
the plural as a whole (e.g. it's mostly hypothetical, rare and
awkward, etc.).
sgtail, sgtailall: Same as pltail=, pltailall= but for the singular.
obltail, obltailall: Same as pltail=, pltailall= but for oblique cases
(not the nominative or accusative).
CASE_NUM_tail: Attach the argument to the end of the last form
(whether there's one or more than one) for the particular
case/number combination. Note that this doesn't work quite like
pltail= or sgtail= in that it doesn't skip adding the argument
when there's only one form.
CASE_NUM_tailall: Attach the argument to the end of all forms
specified for the particular case/number combination. Similar to
pltailall= or sgtailall=.
suffix: Add a suffix such as ся to all forms.
prefix: Add a prefix to all forms.
plhyp, plhypall, CASE_NUM_hyp, etc.: Same as pltail, pltailall,
CASE_NUM_tal, etc. but specify that the marked forms are
mostly hypothetical or rare/awkard. Generally you will want
plhypall=y to mark the plural as hypothetical.
Per word named arguments:
All of the above named arguments have per-word variants, e.g.
a1, a2, ...; n1, n2, ...; CASE_NUM1, CASE_NUM2, ...;
pltail1, pltail2, ...; etc. These apply to the individual words of a
form.
Case abbreviations:
nom: nominative
gen: genitive
dat: dative
acc: accusative
ins: instrumental
pre: prepositional
par: partitive
loc: locative
voc: vocative
Number abbreviations:
sg: singular
pl: plural
Animacy abbreviations:
an: animate
in: inanimate
Declension field:
One of the following for regular nouns:
(blank)
GENDER
-VARIANT
GENDER-VARIANT
$
DECLTYPE
DECLTYPE/DECLTYPE
(also, can append various special-case markers to any of the above)
Or one of the following for adjectival nouns:
+
+ь
$
+short, +mixed or +proper
+DECLTYPE
GENDER if present is m, f, n or 3f; for regular nouns, required if the
lemma ends in -ь or is plural, ignored otherwise. 3f is the same
as f but in the case of a plural lemma in -и, detects a
third-declension feminine with singular in -ь rather than a
first-declension feminine with singular in -а or -я.
VARIANT is one way of requesting variant declensions (see also special
case (1) for variant nom pls, and special case (2) for variant
gen pls). The currently allowed values are -ья (will select a
mixed declension that has some normal declension in its singular
and the -ья plural declension); -ин (for animate masculine nouns
in -ин with plural in -е -- note that this is autodetected in the
majority of cases where the ending in -янин or -анин); -ишко
(used for inanimate neuter-form diminutive masculine nouns in
-ишко [also сараю́шко] with nom pl -и and colloquial feminine-ending
alternants in some singular cases); -ище (similar to -ишко but
used for *animate* augmentative masculine neuter-form nouns in
-ище). Variants -ишко and -ище must be given with with special
case (1).
$ is indeclinable words. It is principally useful in multiword
expressions where some of the words are indeclinable.
DECLTYPE is an explicit declension type. Normally you shouldn't use
this, and should instead let the declension type be autodetected
based on the ending, supplying the appropriate hint if needed
(gender for regular nouns, +ь for adjectives). If provided, the
declension type is usually the same as the ending, and if present,
the lemma field should be just the stem, without the ending.
Possibilities for regular nouns are (blank) or # for hard-consonant
declension, а, я, о, е or ё, е́, й, ья, ье or ьё, ь-m, ь-f,
ин, ёнок or онок or енок, ёночек or оночек or еночек, мя,
-а or #-а, ь-я, й-я, о-и or о-ы, -ья or #-ья, $ (indeclinable).
Old-style (pre-reform) declensions use ъ instead of (blank), ъ-а
instead of -а, ъ-ья instead of -ья, and инъ, ёнокъ/онокъ/енокъ,
ёночекъ/оночекъ/еночекъ instead of the same without terminating ъ.
The declensions can also be written with an accent on them; this
chooses the same declension (except for е vs. е́), but causes
ACCENT to default to pattern b instead of a.
For adjectival nouns, you should normally supply just + and let
the ending determine the declension; supply +ь in the case of a
possessive adjectival noun in -ий, which have an extra -ь- in
most endings compared with normal adjectival nouns in -ий, but
which can't be distinguished based on the nominative singular.
You can also supply +short, +mixed or +proper, which constrains
the declension appropriately but still autodetects the
gender-specific and stress-specific variant. If you do supply
a specific declension type, as with regular nouns you need to
omit the ending from the lemma field and supply just the stem.
Possibilities are +ый, +ое, +ая, +ій, +ее, +яя, +ой, +о́е, +а́я,
+ьій, +ье, +ья, +-short or +#-short (masc), +о-short,
+о-stressed-short or +о́-short, +а-short, +а-stressed-short or
+а́-short, and similar for -mixed and -proper (except there aren't
any stressed mixed declensions).
DECLTYPE/DECLTYPE is used for nouns with one declension in the
singular and a different one in the plural, for cases that
PLVARIANT and special case (1) below don't cover.
Special-case markers:
(1) for Zaliznyak-style alternative nominative plural ending:
-а or -я for masculine, -и or -ы for neuter
(2) for Zaliznyak-style alternative genitive plural ending:
-ъ/none for masculine, -ей for feminine, -ов(ъ) for neuter,
-ей for plural variant -ья
* for reducibles (nom sg or gen pl has an extra vowel before the
final consonant as compared with the stem found in other cases)
;ё for Zaliznyak-style alternation between last е in stem and ё
TODO:
1. Multi-word issues:
-- FIXME: Make sure internal_notes handled correctly; we may run into issues
with multiple internal notes from different words, if we're not careful
to use different footnote symbols for each type of footnote (which we
don't do currently). [NOT DONE, MAY NOT DO]
-- Handling default lemma: With multiple words, we should probably split
the page name on spaces and default each word in turn [NOT DONE, MAY
NOT DO]
2a. FIXME: For -ишко diminutives and -ище augmentatives, should add an
appropriate category of some sort (currently marked by colloqfem= in
category).
2b. FIXME: Adding a note to dat_sg also adds it to loc_sg when it exists;
seems wrong. See луг.
2c. FIXME: When you have both d' and f in feminines and you use sgtail=*,
you get two *'s. See User:Benwing2/test-ru-noun-debug.
3. ADJECTIVE FIXMES:
3a. FIXME: Change calls to ru-adj11 to use the new proper name support in
ru-adjective.
3b. FIXME: Test that omitting a manual form in ru-adjective leaves the form as
a big dash.
3c. FIXME: какой-либо and какой-то display genitives with translit -go
instead of -vo. To fix this properly requires implementing real
manual translit for adjectives.
3d. FIXME: Implement real manual translit for adjectives.
5. [FIXME: Consider adding an indicator in the header line when the ё/e
alternation occurs. This is a bit tricky to calculate: If special case
;ё is given, but also if ё occurs in the stem and the accent pattern is
as follows -- for sg-only, b' d' f' f'', also b d f if the noun is masc
or 3rd-decl fem (i.e. nom-sg ending is non-syllabic); for pl-only,
e f f' f'', also b b' c if the gen pl is non-syllabic; for sg/pl,
any but a or b, also b if either nom sg or gen pl is non-syllabic. But
it gets more complicated due to overrides. An alternative is to check
all forms to see if ё is present in some but not all; but this is
tricky also because e.g. reducibles frequently have ё/null alternation,
which doesn't count, and some endings have е or ё in them, which also
doesn't count. If we were to do it this way, we'd have to (a) count
the number of е's in the form(s) with ё and verify that there's at least
one form without ё and with one more е than in the form(s) with ё
(and it gets trickier if different forms with ё have different numbers of
е in them, although that is probably rare); and (b) ignore the appropriate
endings (the best way to do this would probably be to look at the actual
suffixes that were generated in args.suffixes and chop off any matching
ending in the actual form(s), but also chop off final -е/ё, as well as
-ев(ъ)/-ёв(ъ)/-ей/-ёй in the gen pl, which is frequently overridden,
unless perhaps the stem ends in the same way). It'd probably not
possible to do this in a 100% foolproof way but can be "good enough" for
nearly all circumstances.] [MIGHT BE TOO MUCH WORK]
6. HEADWORD FIXMES:
6a. FIXME: In ru-headword, create a category for words whose gender doesn't
match the form. (This is easy to do for ru-noun+ but harder for ru-noun.
We would need to do limited autodetection of the ending: for singulars,
-а/я should be feminine, -е/о/ё should be neuter, -ь should be masculine
or feminine, anything else should be masculine; for plurals, -и/ы should
be masculine or feminine, -а/я should be neuter except that -ія can be
feminine or neuter due to old-style adjectival pluralia tantum nouns,
anything else can be any gender.)
6b. FIXME: Recognize indeclinable nouns and indicate as indeclinable. Probably
should work by checking the case forms to see if they're the same.
9. FIXME: Change stress-pattern detection and overriding to happen inside of
looping over the two parts of a slash decl. Requires that the loop over
the two parts happen outside of the loop over stress patterns. Requires
that the category code get split into two parts, one to handle combined
singular/plural categories that goes outside the two loops, and one to
handle everything else that goes inside the two loops.
10. FIXME: override_matches_suffix() had a free variable reference to ARGS
in it, which should have triggered an error whenever there was a nom_sg or
nom_pl override but didn't. Is there an error causing this never to be
called? Check.
11a. FIXME: In a multiword lemma, using loc2=+ causes only the second word
to get linked instead of the whole expression. Same for par2=+, voc2=+.
11b. FIXME: Using loc=+ with a multiword lemma should do the right thing, same
as if locN=+ is specified for each individual word. Instead it generates
the locative as a whole from the dative, which fails e.g. if some of the
words are adjectival. Same for par=+, voc=+.
13. Multi-word issues:
-- Setting n=pl when auto-detecting a plural lemma. How does that interact
with multi-word stuff? (DONE)
-- compute_heading() -- what to do with multiple words? I assume we should
display info on the first noun (non-indeclinable, non-adjectival), and
on the first adjectival word otherwise, and finally on an indeclinable word (DONE)
-- args.genders -- it should presumably come from the same word as is used
in compute_heading(); but we should allow the overall gender to be
overridden, at least in ru-noun+ (DONE)
-- Bug in args.suffix: Gets added to every word in attach_with() and then
again at the end, after pltail and such. Needs to be added to the
last word only, before pltail. Need also suffixN for individual words.
(DONE, NEEDS TESTING)
-- Should have ..N versions of pltail and variants. (DONE, NEEDS TESTING)
-- Need to handle overrides of acc_sg, acc_pl (DONE)
-- Overrides of nom_sg/nom_pl should also override acc_sg/acc_pl if it
was originally empty and the animacy is inanimate; similarly for
gen_sg/gen_pl and animates; this needs to work both for per-word and
overall overrides. (DONE)
-- do_generate_forms(_multi) need to run part of make_table(), enough to
combine all per_word_info into single lists of forms and store back
into args[case]. (DONE, NEEDS TESTING)
-- In generate_forms, should probably check if a=="i" and only return
acc_sg_in as acc_sg=; or if a=="a" and only return acc_sg_an as acc_sg=;
in old/new comparison code, do something similar, also when a=="b"
check if acc_sg_in==acc_sg_an and make it acc_sg; when a=="b" and the
_in and _an variants are different, might need to ignore them or check
that acc_sg_in==nom_sg and acc_sg_an==gen_sg; similarly for _pl
(DONE, NEEDS TESTING)
-- Need to test with multiple words! [DONE]
-- Current handling of <adj> won't work properly with multiple words;
will need to translate word-by-word in that case (should be solved by
manual-translit branch) [DONE]
14. In multiple-words branch, fix ru-decl-noun-multi so it recognizes
things like *, (1), (2) and ; without the need for a separator. Consider
using semicolon as a separator, since we already use it to separate ё
from a previous declension. Maybe use $ or ~ for an indeclinable word; don't
use semicolon. [IMPLEMENTED. NEED TO TEST.]
16. [Consider having ru-noun+ treat par= as a second genitive in
the headword, as is done with край] [WON'T DO]
17. [FIXME: Consider removing slash patterns and instead handling them by
allowing additional declension flags 'sg' and 'pl'. This simplifies the
various special cases caused by slash declensions. It would also be
possible to remove the special plural stem, which would get rid of more
special cases. On the other hand, it makes it more complicated to support
plural variant -ья with all singular types, and the category code that
displays things like "Russian nouns with singular -X and plural -Y"
also gets more complicated, and there's something convenient and intuitive
about plural stems, and slash declensions are also convenient and at least
somewhat intuitive. One possibility is to externally allow slash
declensions and special plural stems and rewrite them internally to
separate stems with 'sg' and 'pl' declension flags; but there are still
the two coding issues mentioned above.]
18. [FIXME: Consider redoing slash patterns so they operate at the outer
level, i.e. things like special cases apply separately in the singular
and plural part of the slash pattern.]
19. In ru-noun, don't recognize -а with m as plural unless (1)
or n=pl is also given, because there are masculine words with the
feminine ending. Check using the test code whether this changes
anything. Also check if there are other similar cases (neuter with
-и isn't parallel because -и is always plural). [IMPLEMENTED. NEED TO TEST.]
19a. Internal notes weren't propagated properly from adjectives.
[IMPLEMENTED. NEED TO TEST.]
19b. Add support for -ишко and -ище variants (p. 74 of Z),
which conversationally and/or colloquially have feminine endings in
certain cases. [IMPLEMENTED. NEED TO TEST. MAKE SURE THE INTERNAL NOTES
APPEAR.]
19d. For masculine animate neuter-form nouns, the accusative singular
ends in -а (-я soft) instead of -о. [IMPLEMENTED. NEED TO TEST.
NOTE: Currently this variant only can be selected using new-style
arguments where the gender can be given. Perhaps we should consider
allowing gender to be specified with old-style explicit declensions.]
21. Put back gender hints for pl adjectival nouns; used by ru-noun+.
[IMPLEMENTED. NEED TO TEST.]
23. Mixed and proper-noun adjectives have built-in notes. We need to
handle those notes with an "internal_notes" section similar to what is used
in the adjective module. [IMPLEMENTED. NEED TO TEST.]
24. Adjective detection code here needs to work the same as for the
adjective module, in particular in the handling of short, stressed-short,
mixed, proper, stressed-proper. [IMPLEMENTED. NEED TO TEST.]
25. Consider simplifying plural-variant code to only allow -ья as a
plural variant [and maybe even change that to be something like (1')].
[IMPLEMENTED REDUCTION OF PLURAL VARIANTS TO -ья; PLURAL-VARIANT CODE
STILL COMPLEX, THOUGH. NEED TO TEST.]
26. Automatically superscript *, numbers and similar things at the
beginning of a note. Also do this in adjective module. [IMPLEMENTED.
NEED TO TEST.]
28. Make the check for multiple stress patterns (categorizing/tracking)
smarter, to keep a list of them and check at the end, so we handle
multiple stress patterns specified through different arg sets.
[IMPLEMENTED; NEED TO TEST.]
29. More sophisticated handling of user-requested plural variant vs. special
case (1) vs. plural-detected variant. [IMPLEMENTED. NEED TO TEST FURTHER.]
30. Solution to ambiguous plural involving gender spec "3f". [IMPLEMENTED;
NEED TO TEST. Use запчасти, новости.]
33. With pluralia tantum adjectival nouns, we don't know the gender.
By default we assume masculine (or feminine for old-style -ія nouns) and
currently this goes into the category, but shouldn't. [IMPLEMENTED.]
39. [Eventually: Even with decl type explicitly given, the full stem with
ending should be included.] [MAY NEVER IMPLEMENT]
40. [Get error "Unable to dereduce" with strange noun ва́йя, what should
happen?] [WILL NOT FIX; USE AN OVERRIDE]
41. In творог, module generates partitive творогу́ when it should copy the
dative творогу́,тво́рогу. (DONE)
42. [[груз 200]] doesn't work. Interprets 200 as a footnote symbol.
43. When converting е -> ё not after cons and with translit, we should
convert e -> o to avoid double j. (DONE)
44. FIXME: In ро́вня/ровня́, similarly with неровня, marks genitive plural
ровня́ as irregular even though it isn't. (NOT OUR ERROR; THE DECLENSIONS
OF THESE NOUNS MARKED THE ENDING-STRESSED VARIANTS WITH (2).)
]=]--
local m_utilities = require("Module:utilities")
local m_table = require("Module:table")
local m_links = require("Module:links")
local com = require("Module:ru-common")
local nom = require("Module:ru-nominal")
local m_ru_adj = require("Module:ru-adjective")
local m_str_utils = require("Module:string utilities")
local scriptutils = require("Module:script utilities")
local m_table_tools = require("Module:table tools")
local m_debug = require("Module:debug")
local export = {}
local lang = require("Module:languages").getByCode("ru")
local Latn = require("Module:scripts").getByCode("Latn")
local format = m_str_utils.format
local rfind = m_str_utils.find
local rsubn = m_str_utils.gsub
local rmatch = m_str_utils.match
local rsplit = m_str_utils.split
local u = m_str_utils.char
local ulower = m_str_utils.lower
local usub = m_str_utils.sub
local ulen = m_str_utils.len
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
-- If enabled, compare this module with new version of module to make
-- sure all declensions are the same. Eventually consider removing this;
-- but useful as new code is created.
local test_new_ru_noun_module = false
local AC = u(0x0301) -- acute = ́
local GR = u(0x0300) -- grave = ̀
local CFLEX = u(0x0302) -- circumflex = ̂
local DIA = u(0x0308) -- diaeresis = ̈
local PSEUDOCONS = u(0xFFF2) -- pseudoconsonant placeholder, matching ru-common
local IRREGMARKER = "△"
local HYPMARKER = "⟐"
local paucal_marker = "*"
local paucal_internal_note = "* Used with the numbers 1.5, 2, 3, 4 and higher numbers after 20 ending in 2, 3, and 4."
-- text class to check lowercase arg against to see if Latin text embedded in it
local latin_text_class = "[a-zščžěáéíóúýàèìòùỳâêîôûŷạẹịọụỵȧėȯẏ]"
-- Forward functions
local generate_forms_1
local determine_decl
local handle_forms_and_overrides
local handle_overall_forms_and_overrides
local concat_word_forms
local make_table
local detect_adj_type
local detect_stress_pattern
local override_stress_pattern
local determine_stress_variant
local determine_stem_variant
local is_reducible
local is_dereducible
local add_bare_suffix
local attach_stressed
local do_stress_pattern
local canonicalize_override
-- version of rsubn() that discards all but the first return value
local function rsub(term, foo, bar)
local retval = rsubn(term, foo, bar)
return retval
end
-- version of rsubn() that returns a 2nd argument boolean indicating whether
-- a substitution was made.
local function rsubb(term, foo, bar)
local retval, nsubs = rsubn(term, foo, bar)
return retval, nsubs > 0
end
-- version of rfind() that lowercases its string first, for case-insensitive matching
local function rlfind(term, foo)
return rfind(ulower(term), foo)
end
local function track(page)
m_debug.track("ru-noun/" .. page)
return true
end
-- version of m_table.insertIfNot() that makes sure 'false' doesn't get inserted by mistake.
local function insert_if_not(foo, bar)
assert(bar ~= false)
m_table.insertIfNot(foo, bar)
end
-- Fancy version of ine() (if-not-empty). Converts empty string to nil,
-- but also strips leading/trailing space and then single or double quotes,
-- to allow for embedded spaces.
local function ine(arg)
if not arg then return nil end
arg = rsub(arg, "^%s*(.-)%s*$", "%1")
if arg == "" then return nil end
local inside_quotes = rmatch(arg, '^"(.*)"$')
if inside_quotes then
return inside_quotes
end
inside_quotes = rmatch(arg, "^'(.*)'$")
if inside_quotes then
return inside_quotes
end
return arg
end
-- FIXME: Move to utils
-- Iterate over a chain of parameters, FIRST then PREF2, PREF3, ...,
-- inserting into LIST (newly created if omitted). Return LIST.
local function get_arg_chain(args, first, pref, list)
if not list then
list = {}
end
local val = args[first]
local i = 2
while val do
table.insert(list, val)
val = args[pref .. i]
i = i + 1
end
return list
end
-- synthesize a frame so that exported functions meant to be called from
-- templates can be called from the debug console.
local function debug_frame(parargs, args)
return {args = args, getParent = function() return {args = parargs} end}
end
local function rutr_pairs_equal(term1, term2)
local ru1, tr1 = term1[1], term1[2]
local ru2, tr2 = term2[1], term2[2]
local ru1entry, ru1notes = m_table_tools.separate_notes(m_links.remove_links(ru1))
local ru2entry, ru2notes = m_table_tools.separate_notes(m_links.remove_links(ru2))
if ru1entry ~= ru2entry then
return false
end
local tr1entry, tr1notes
local tr2entry, tr2notes
if tr1 then
tr1entry, tr1notes = m_table_tools.separate_notes(tr1)
end
if tr2 then
tr2entry, tr2notes = m_table_tools.separate_notes(tr2)
end
if tr1entry == tr2entry then
return true
elseif type(tr1entry) == type(tr2entry) then
return false
else
tr1entry = tr1entry or com.translit_no_links(ru1entry)
tr2entry = tr2entry or com.translit_no_links(ru2entry)
return tr1entry == tr2entry
end
end
local function contains_rutr_pair(list, pair)
for _, item in ipairs(list) do
if rutr_pairs_equal(item, pair) then
return true
end
end
return false
end
-- Clone parent's args while also assigning nil to empty strings.
local function clone_args(frame)
local args = {}
for pname, param in pairs(frame:getParent().args) do
args[pname] = ine(param)
end
return args
end
-- Old-style declensions.
local declensions_old = {}
-- New-style declensions; computed automatically from the old-style ones,
-- for the most part.
local declensions = {}
-- Internal notes for old-style declensions.
local internal_notes_table_old = {}
-- Same for new-style declensions.
local internal_notes_table = {}
-- Category and type information corresponding to declensions: These may
-- contain the following fields: 'singular', 'plural', 'decl', 'hard', 'g',
-- 'suffix', 'gensg', 'irregpl', 'alt_nom_pl', 'cant_reduce', 'ignore_reduce',
-- 'stem_suffix'.
--
-- 'singular' is used to construct a category of the form
-- "Russian nouns SINGULAR". If omitted, a category is constructed of the
-- form "Russian nouns ending in -ENDING", where ENDING is the actual
-- nom sg ending shorn of its acute accents; or "Russian nouns ending
-- in suffix -ENDING", if 'suffix' is true. The value of SINGULAR can be
-- one of the following: a single string, a list of strings, or a function,
-- which is passed one argument (the value of ENDING that would be used to
-- auto-initialize the category), and should return a single string or list
-- of strings. Such a category is only constructed if 'gensg' is true.
--
-- 'plural' is analogous but used to construct a category of the form
-- "Russian nouns with PLURAL", and if omitted, a category is constructed
-- of the form "Russian nouns with plural -ENDING", based on the actual
-- nom pl ending shorn of its acute accents. Currently no plural category
-- is actually constructed.
--
-- In addition, a category may normally constructed from the combination of
-- 'singular' and 'plural', appropriately defaulted; e.g. if both are present,
-- the combined category will be "Russian nouns SINGULAR with PLURAL" and
-- if both are missing, the combined category will be
-- "Russian nouns ending in -SGENDING with plural -PLENDING" (or
-- "Russian nouns ending in suffix -SGENDING with plural -PLENDING" if
-- 'suffix' is true). Note that if either singular or plural or both
-- specifies a list, looping will occur over all combinations. Such a
-- category is constructed only if 'irregpl' or 'alt_nom_pl' or 'suffix'
-- is true or if the declension class is a slash class.
--
-- 'decl' is "1st", "2nd", "3rd" or "indeclinable"; 'hard' is "hard", "soft"
-- or "none"; 'g' is "m", "f", "n" or "none"; these are all traditional
-- declension categories.
--
-- If 'suffix' is true, the declension type includes a long suffix
-- added to the string that itself undergoes reducibility and such, and so
-- reducibility cannot occur in the stem minus the suffix. Categories will
-- be created for the suffix.
--
-- 'alt_nom_pl' indicates that the declension has an alternative nominative
-- plural (corresponding to Zaliznyak's special case 1; compare special case 2
-- for alternative genitive plural). 'irregpl' indicates that the entire
-- plural is irregular.
--
-- In addition to the above categories, additional more specific categories
-- are constructed based on the final letter of the stem, e.g.
-- "Russian velar-stem 1st-declension hard nouns". See calls to
-- com.get_stem_trailing_letter_type(). 'stem_suffix', if present, is added to
-- the end of the stem when get_stem_trailing_letter_type() is called.
-- This is the only place that 'stem_suffix' is used. This is for use with
-- the '-ья' and '-ье' declension types, so that the trailing letter is
-- 'ь' and not whatever precedes it.
--
-- 'enable_categories' is a special hack for testing, which disables all
-- category insertion if false. Delete this as soon as we've verified the
-- working of the category code and created all the necessary categories.
local enable_categories = true
-- Category/type info corresponding to old-style declensions; see above.
local declensions_old_cat = {}
-- Category/type info corresponding to new-style declensions. Computed
-- automatically from the old-style ones, for the most part. Same format
-- as the old-style ones.
local declensions_cat = {}
-- Table listing aliases of old-style declension classes.
local declensions_old_aliases = {}
-- Table listing aliases of new-style declension classes; computed
-- automatically from the old-style ones.
local declensions_aliases = {}
local stress_patterns = {}
-- Set of patterns with ending-stressed genitive plural.
local ending_stressed_gen_pl_patterns = {}
-- Set of patterns with ending-stressed prepositional singular.
local ending_stressed_pre_sg_patterns = {}
-- Set of patterns with ending-stressed dative singular.
local ending_stressed_dat_sg_patterns = {}
-- Set of patterns with all singular forms ending-stressed.
local ending_stressed_sg_patterns = {}
-- Set of patterns with all plural forms ending-stressed.
local ending_stressed_pl_patterns = {}
local declinable_cases_except_accusative = {
"nom_sg", "gen_sg", "dat_sg", "ins_sg", "pre_sg",
"nom_pl", "gen_pl", "dat_pl", "ins_pl", "pre_pl",
}
local accusative_cases_unsplit_animacy = {
"acc_sg", "acc_pl",
}
local accusative_cases_split_animacy = {
"acc_sg_an", "acc_sg_in", "acc_pl_an", "acc_pl_in",
}
local lemma_linked_cases = {
"nom_sg_linked", "nom_pl_linked",
}
local overridable_only_cases = {
"par", "loc", "voc", "par_pl", "loc_pl", "voc_pl", "count", "pauc",
}
local overridable_only_cases_set = m_table.listToSet(overridable_only_cases)
-- List of all cases that are declined normally.
local decl_cases = m_table.append(declinable_cases_except_accusative, accusative_cases_unsplit_animacy)
-- List of all cases that can be overridden (includes all cases except the "linked" lemma case variants). Also
-- currently the same as the cases returned by export.generate_forms().
local overridable_cases = m_table.append(decl_cases, accusative_cases_split_animacy, overridable_only_cases)
-- List of all cases that can be displayed (includes all cases except plain accusatives).
local displayable_cases = m_table.append(declinable_cases_except_accusative, lemma_linked_cases,
accusative_cases_split_animacy, overridable_only_cases)
-- List of all cases, including those that are declined normally (nom/gen/dat/acc/ins/pre sg and pl), plus
-- animate/inanimate accusative variants (computed automatically as appropriate from the previous cases), plus
-- additional overridable cases (loc/par/voc and plural), plus the "linked" lemma case variants used in ru-noun+
-- headwords (nom_sg_linked, nom_pl_linked, whose values come from nom_sg and nom_pl but may have additional embedded
-- links if they were given in the lemma).
local all_cases = m_table.append(overridable_cases, lemma_linked_cases)
local function english_case_description(case)
if case == "par" or case == "loc" or case == "voc" then
-- For historical reasons, the singular of these cases doens't include "_sg" in their code.
case = case .. "_sg"
end
local engcase = rsub(case, "^([a-z]*)", {
nom="nominative", gen="genitive", dat="dative",
acc="accusative", ins="instrumental", pre="prepositional",
par="partitive", loc="locative", voc="vocative",
count="count form", pauc="paucal form",
})
engcase = rsub(engcase, "(_[a-z]*)", {
_sg=" singular", _pl=" plural",
_an="", _in="",
--_an=" animate", _in=" inanimate"
})
return engcase
end
--------------------------------------------------------------------------
-- Tracking and categorization --
--------------------------------------------------------------------------
-- FIXME! Move below the main code
-- FIXME!! Consider deleting most of this tracking code once we've enabled
-- all the categories. Note that some of the tracking categories aren't
-- completely redundant; e.g. we have tracking pages that combine decl and
-- stress classes, such as "а/a" or "о-и/d'", which are more or less
-- equivalent to stem/gender/stress categories, but we also have the same
-- prefixed by "reducible-stem/" for reducible stems.
local function tracking_code(stress, orig_decl, decl, args, n, islast)
assert(orig_decl)
assert(decl)
local hint_types = com.get_stem_trailing_letter_type(args.stem)
if orig_decl == decl then
orig_decl = nil
end
if args.notes then
track("notes")
end
local function all_pl_irreg()
track("irreg")
for _, case in ipairs(overridable_cases) do
if rfind(case, "_pl") then
track("irreg/" .. case)
end
end
end
local function track_prefix(prefix)
local function dotrack(suf)
track(prefix .. suf)
end
dotrack(stress)
dotrack(decl)
dotrack(decl .. "/" .. stress)
if orig_decl then
dotrack(orig_decl)
dotrack(orig_decl .. "/" .. stress)
end
for _, hint_type in ipairs(hint_types) do
dotrack(hint_type)
dotrack(decl .. "/" .. hint_type)
if orig_decl then
dotrack(orig_decl .. "/" .. hint_type)
end
end
end
track_prefix("")
if args.reducible then
track("reducible-stem")
track_prefix("reducible-stem/")
end
if rlfind(args.stem, "и́?н$") and (decl == "" or decl == "#") then
track("irregular-in")
end
if args.pltail then
track("pltail")
end
if args.sgtail then
track("sgtail")
end
if args.pltailall then
track("pltailall")
end
if args.sgtailall then
track("sgtailall")
end
if args.pl ~= args.stem then
track("irreg-pl-stem")
track("irreg")
end
if args.alt_gen_pl then
track("alt-gen-pl")
track("irreg")
track("irreg/gen_pl")
end
if args.want_sc1 then
track("want-sc1")
track("irreg")
track("irreg/nom_pl")
end
if rfind(decl, "-и$") or rfind(decl, "-а$") or rfind(decl, "-я$") then
track("irreg")
track("irreg/nom_pl")
end
if rfind(decl, "-ья$") then
track("variant-ья")
all_pl_irreg()
end
if rfind(decl, "%(ишк%)$") then
track("variant-ишко")
end
if rfind(decl, "%(ищ%)$") then
track("variant-ище")
end
if args.jo_special then
track("jo-special")
end
if args.manual then
track("manual")
end
if args.explicit_gender then
track("explicit-gender")
track("explicit-gender/" .. args.explicit_gender)
end
for _, case in ipairs(overridable_cases) do
if args[case .. n] and not args.manual then
track("override")
track("override/" .. case .. n)
track("irreg")
track("irreg/" .. case .. n)
-- questionable use: track_prefix("irreg/" .. case .. "/")
-- questionable use: track_prefix("irreg/" .. case .. n .. "/")
end
if islast and args[case] and not args.manual then
track("override")
track("override/" .. case)
track("irreg")
track("irreg/" .. case)
-- questionable use: track_prefix("irreg/" .. case .. "/")
end
if args[case .. "_tail"] then
track("casenum-tail")
track("casenum-tail/" .. case)
end
if args[case .. "_tailall"] then
track("casenum-tailall")
track("casenum-tailall/" .. case)
end
end
end
local gender_to_full = {m="masculine", f="feminine", n="neuter"}
local gender_to_short = {m="masc", f="fem", n="neut"}
-- Insert the category CAT (a string) into list CATEGORIES. String will
-- have "Russian " prepended and ~ substituted for the plural part of speech.
local function insert_category(categories, cat, pos, atbeg)
if enable_categories then
local fullcat = "Russian " .. rsub(cat, "~", pos .. "s")
if atbeg then
table.insert(categories, 1, fullcat)
else
table.insert(categories, fullcat)
end
end
end
-- Insert categories into ARGS.CATEGORIES corresponding to the specified
-- stress and declension classes and to the form of the stem (e.g. velar,
-- sibilant, etc.). Also initialize values used to compute the declension
-- heading that describes similar information. N is the number of the
-- word being processed; ISLAST is true if this is the last word.
local function categorize_and_init_heading(stress, decl, args, n, islast)
local function cat_to_list(cat)
if not cat then
return {}
elseif type(cat) == "string" then
return {cat}
else
assert(type(cat) == "table")
return cat
end
end
-- Insert category CAT into the list of categories in ARGS.
-- CAT may be nil, a single string or a list of strings. We call
-- insert_category() on each string. The strings will have "Russian "
-- prepended and "~" replaced with the plural part of speech.
local function insert_cat(cat)
for _, c in ipairs(cat_to_list(cat)) do
insert_category(args.categories, c, args.pos)
end
end
-- "Resolve" the category spec CATSPEC into the sort of category spec
-- accepted by insert_cat(), i.e. nil, a single string or a list of
-- strings. CATSPEC may be any of these or a function, which takes one
-- argument (SUFFIX) and returns another CATSPEC.
local function resolve_cat(catspec, suffix)
if type(catspec) == "function" then
return resolve_cat(catspec(suffix), suffix)
else
return catspec
end
end
-- Check whether an override for nom_sg or nom_pl still contains the
-- normal suffix (which should already have accents removed) in at least
-- one of its entries. If no override then of course we return true.
local function override_matches_suffix(args, case, n, suffix)
assert(suffix == com.remove_accents(suffix))
-- NOTE: It might not be completely correct to pass in args.forms
-- here when n == ""; this is different behavior from
-- handle_overall_forms_and_overrides(). But args.forms is only used
-- to retrieve the dat_sg for handling loc/par, and we're never
-- called with loc or par as the value of CASE, so it doesn't matter.
-- We add an assert to make sure of this.
assert(case ~= "loc" and case ~= "par")
local override = canonicalize_override(args, case, args.forms, n)
if not override then
return true
end
for _, x in ipairs(override) do
local ru, tr = x[1], x[2]
local entry, notes = m_table_tools.separate_notes(ru)
entry = com.remove_accents(m_links.remove_links(entry))
if rlfind(entry, suffix .. "$") then
return true
end
end
return false
end
if args.manual then
return
end
local h = args.heading_info
assert(decl)
local decl_cats = args.old and declensions_old_cat or declensions_cat
local sgdecl, pldecl
local is_slash_decl = rfind(decl, "/")
if is_slash_decl then
local indiv_decls = rsplit(decl, "/")
sgdecl, pldecl = indiv_decls[1], indiv_decls[2]
else
sgdecl, pldecl = decl, decl
end
local sgdc = decl_cats[sgdecl]
local pldc = decl_cats[pldecl]
assert(sgdc)
assert(pldc)
local sghint_types = com.get_stem_trailing_letter_type(
args.stem .. (sgdc.stem_suffix or ""))
-- insert English version of Zaliznyak stem type
if sgdc.decl == "indeclinable" then
insert_cat("indeclinable ~")
insert_if_not(h.stemetc, "indecl")
else
local stem_type =
sgdc.decl == "3rd" and "3rd-declension" or
m_table.contains(sghint_types, "velar") and "velar-stem" or
m_table.contains(sghint_types, "sibilant") and "sibilant-stem" or
m_table.contains(sghint_types, "c") and "ц-stem" or
m_table.contains(sghint_types, "i") and "i-stem" or
m_table.contains(sghint_types, "vowel") and "vowel-stem" or
m_table.contains(sghint_types, "soft-cons") and "vowel-stem" or
m_table.contains(sghint_types, "palatal") and "vowel-stem" or
sgdc.hard == "soft" and "soft-stem" or
"hard-stem"
local short_stem_type = stem_type == "3rd-declension" and "3rd-decl" or stem_type
if sgdc.adj then
-- Don't include gender for pluralia tantum because it's mostly
-- indeterminate (certainly when specified using a plural lemma,
-- which will be usually; technically it's partly or completely
-- determinate in certain old-style adjectives that distinguish
-- masculine from feminine/neuter, but this is too rare a case
-- to worry about)
local gendertext = args.thisn == "p" and "plural-only" or gender_to_full[sgdc.g]
insert_if_not(h.adjectival, "yes")
if args.thisn ~= "p" then
insert_if_not(h.gender, gender_to_short[sgdc.g])
end
if sgdc.possadj then
insert_cat(sgdc.decl .. " possessive " .. gendertext .. " accent-" .. (stress:gsub("''", "″"):gsub("'", "′")) .. " adjectival ~")
insert_if_not(h.stemetc, sgdc.decl .. " poss")
insert_if_not(h.stress, stress)
elseif stem_type == "soft-stem" or stem_type == "vowel-stem" then
insert_cat(stem_type .. " " .. gendertext .. " adjectival ~")
insert_if_not(h.stemetc, short_stem_type)
else
insert_cat(stem_type .. " " .. gendertext .. " accent-" .. (stress:gsub("''", "″"):gsub("'", "′")) .. " adjectival ~")
insert_if_not(h.stemetc, short_stem_type)
insert_if_not(h.stress, stress)
end
else
-- NOTE: There are 8 Zaliznyak-style stem types and 3 genders, but
-- we don't create a category for masculine-form 3rd-declension
-- nouns (there is such a noun, путь, but it mostly behaves
-- like a feminine noun), so there are 23.
insert_cat(stem_type .. " " .. gender_to_full[sgdc.g] .. "-form ~")
-- NOTE: Here we are creating categories for the combination of
-- stem, gender and accent. There are 10 accent patterns and 23
-- combinations of stem and gender, which potentially makes for
-- 10*23 = 230 such categories, which is a lot. Not all such
-- categories should actually exist; there were maybe 75 former
-- declension templates, each of which was essentially categorized
-- by the same three variables, but some of which dealt with
-- ancillary issues like irregular plurals; this amounts to 67
-- actual stem/gender/accent categories, although there are more
-- of them in Zaliznyak (FIXME, how many? See generate_cats.py).
insert_cat(stem_type .. " " .. gender_to_full[sgdc.g] .. "-form accent-" .. (stress:gsub("''", "″"):gsub("'", "′")) .. " ~")
insert_if_not(h.adjectival, "no")
insert_if_not(h.gender, gender_to_short[sgdc.g])
insert_if_not(h.stemetc, short_stem_type)
insert_if_not(h.stress, stress)
end
insert_cat("~ with accent pattern " .. (stress:gsub("''", "″"):gsub("'", "′")))
end
local sgsuffix = args.suffixes.nom_sg
if sgsuffix then
assert(#sgsuffix == 1) -- If this ever fails, then implement a loop
sgsuffix = com.remove_accents(sgsuffix[1])
-- If we are plural only or if nom_sg is overridden and has
-- an unusual suffix, then don't create category for sg suffix
if args.thisn == "p" or not override_matches_suffix(args, "nom_sg", n, sgsuffix) or islast and not override_matches_suffix(args, "nom_sg", "", sgsuffix) then
sgsuffix = nil
end
end
local plsuffix = args.suffixes.nom_pl
if plsuffix then
assert(#plsuffix == 1) -- If this ever fails, then implement a loop
plsuffix = com.remove_accents(plsuffix[1])
-- If we are a singulare tantum or if nom_pl is overridden and has
-- an unusual suffix, then don't create category for pl suffix
if args.thisn == "s" or not override_matches_suffix(args, "nom_pl", n, plsuffix) or islast and not override_matches_suffix(args, "nom_pl", "", plsuffix) then
plsuffix = nil
end
end
sgsuffix = sgsuffix and rsub(sgsuffix, "ъ$", "")
plsuffix = plsuffix and rsub(plsuffix, "ъ$", "")
local sgcat = sgsuffix and (resolve_cat(sgdc.singular, sgsuffix) or "ending in " .. (sgsuffix == "" and "a consonant" or (sgdc.suffix and "suffix " or "") .. "-" .. sgsuffix))
local plcat = plsuffix and (resolve_cat(pldc.plural, plsuffix) or "plural -" .. plsuffix)
if sgcat and sgdc.gensg then
for _, cat in ipairs(cat_to_list(sgcat)) do
insert_cat("~ " .. cat)
end
end
if sgcat and plcat and (sgdc.suffix or sgdc.alt_nom_pl or sgdc.irregpl or
is_slash_decl and plsuffix == "-ья") then
for _, scat in ipairs(cat_to_list(sgcat)) do
for _, pcat in ipairs(cat_to_list(plcat)) do
insert_cat("~ " .. scat .. " with " .. pcat)
end
end
end
if args.pl ~= args.stem then
insert_cat("~ with irregular plural stem")
end
if args.reducible and not sgdc.ignore_reduce then
insert_cat("~ with reducible stem")
if args.soft_n then
insert_cat("~ with soft final н in reduced stem")
end
insert_if_not(h.reducible, "yes")
else
insert_if_not(h.reducible, "no")
end
if args.alt_gen_pl then
insert_cat("~ with alternative genitive plural")
end
if sgdc.adj then
insert_cat("adjectival ~")
end
end
local function compute_heading(args)
local headings = {}
local h = args.heading_info
table.insert(headings, args.a == "a" and "anim" or args.a == "i" and
"inan" or "bian")
table.insert(headings, args.nonumber and "uncountable" or
args.n == "s" and "sg-only" or args.n == "p" and "pl-only" or nil)
if #h.gender > 0 then
table.insert(headings, table.concat(h.gender, "/") .. "-form")
end
if #h.stemetc > 0 then
table.insert(headings, table.concat(h.stemetc, "/"))
end
if #h.stress > 0 then
local stresses = {}
for _, stress in ipairs(h.stress) do
table.insert(stresses, (stress:gsub("''", "″"):gsub("'", "′")))
end
table.insert(headings, "accent-" .. table.concat(stresses, "/"))
end
local function handle_bool(boolvals, text, into)
into = into or headings
if m_table.contains(boolvals, "yes") and m_table.contains(boolvals, "no") then
table.insert(into, "[" .. text .. "]")
elseif m_table.contains(boolvals, "yes") then
table.insert(into, text)
end
end
handle_bool(h.adjectival, "adj")
handle_bool(h.reducible, "reduc")
return headings
end
local function compute_overall_heading_categories_and_genders(args)
local hinfo = args.per_word_heading_info
local index = 0
-- First try for non-adjectival, non-indeclinable
for i=1,#hinfo do
if not m_table.contains(hinfo[i].stemetc, "indecl") and not m_table.contains(hinfo[i].adjectival, "yes") then
index = i
break
end
end
if index == 0 then
-- Then just non-indeclinable
for i=1,#hinfo do
if not m_table.contains(hinfo[i].stemetc, "indecl") then
index = i
break
end
end
end
-- Finally, do anything
if index == 0 then
index = 1
end
-- Compute final heading
local headings = args.per_word_headings[index]
local categories = args.per_word_categories[index]
if args.any_irreg then
table.insert(headings, "irreg")
insert_category(categories, "irregular ~", args.pos)
end
for _, case in ipairs(overridable_cases) do
local is_pl = rfind(case, "_pl")
if args.n == "s" and is_pl or args.n == "p" and not is_pl then
-- Don't create singular categories when plural-only or vice-versa
elseif overridable_only_cases_set[case] then
if args.any_overridden[case] then
insert_category(categories, "~ with " .. english_case_description(case), args.pos)
end
elseif args.any_irreg_case[case] then
insert_category(categories, "~ with irregular " .. english_case_description(case), args.pos)
end
end
local heading = args.manual and "" or "(<span style=\"font-size: smaller;\">[[Appendix:Russian nouns#Declension tables|" .. table.concat(headings, " ") .. "]]</span>)"
args.heading = heading
args.categories = categories
args.genders = args.per_word_genders[index]
end
--------------------------------------------------------------------------
-- Main code --
--------------------------------------------------------------------------
-- Used by do_generate_forms().
local function arg1_is_stress(arg1)
if not arg1 then return false end
for _, arg in ipairs(rsplit(arg1, ",")) do
if not rfind(arg, "^[a-f]'?'?$") then
return false
end
end
return true
end
-- Used by do_generate_forms(), handling a word joiner argument
-- of the form 'join:JOINER'.
local function extract_word_joiner(spec)
word_joiner = rmatch(spec, "^join:(.*)$")
assert(word_joiner)
return com.split_russian_tr(word_joiner, "dopair")
end
local function determine_headword_gender(args, sgdc, gender)
-- If gender unspecified, use normal gender of declension, except when
-- adjectival nouns that are pluralia tantum, where the gender is
-- mostly indeterminate (FIXME, not completely with old-style declensions
-- but we don't handle that currently).
if not gender then
if sgdc.adj and args.thisn == "p" then
gender = nil
else
gender = sgdc.g
end
end
-- Determine headword genders
gender = gender and gender ~= "none" and gender .. "-" or ""
local plsuffix = args.n == "p" and "-p" or ""
local hgens
if args.a == "a" then
hgens = {gender .. "an" .. plsuffix}
elseif args.a == "i" then
hgens = {gender .. "in" .. plsuffix}
elseif args.a == "ai" then
hgens = {gender .. "an" .. plsuffix, gender .. "in" .. plsuffix}
else
hgens = {gender .. "in" .. plsuffix, gender .. "an" .. plsuffix}
end
-- Insert into list of genders
for _, hgen in ipairs(hgens) do
insert_if_not(args.genders, hgen)
end
end
function export.do_generate_forms(args, old)
old = old or args.old
args.old = old
args.pos = args.pos or "noun"
-- This is a list with each element corresponding to a word and
-- consisting of a two-element list, ARG_SETS and JOINER, where ARG_SETS
-- is a list of ARG_SET objects, one per alternative stem, and JOINER
-- is a string indicating how to join the word to the next one.
local per_word_info = {}
-- Gather arguments into a list of ARG_SET objects, containing (potentially)
-- elements 1, 2, 3, 4, corresponding to accent pattern, stem, declension
-- type, pl stem and coming from consecutive numbered parameters. Sets of
-- declension parameters are separated by the word "or".
local arg_sets = {}
-- Find maximum-numbered arg, allowing for holes
local max_arg = 0
for k, v in pairs(args) do
if type(k) == "number" and k > max_arg then
max_arg = k
end
end
-- Now gather the arguments.
local offset = 0
local arg_set = {}
for i=1,(max_arg + 1) do
local end_arg_set = false
local end_word = false -- FIXME, is this correct?
local word_joiner
if i == max_arg + 1 then
end_arg_set = true
end_word = true
word_joiner = {""}
elseif args[i] == "_" then
end_arg_set = true
end_word = true
word_joiner = {" "}
elseif args[i] == "-" then
end_arg_set = true
end_word = true
word_joiner = {"-"}
elseif args[i] and rfind(args[i], "^join:") then
end_arg_set = true
end_word = true
word_joiner = extract_word_joiner(args[i])
elseif args[i] == "or" then
end_arg_set = true
end
if end_arg_set then
table.insert(arg_sets, arg_set)
arg_set = {}
offset = i
if end_word then
table.insert(per_word_info, {arg_sets, word_joiner})
arg_sets = {}
end
else
-- If the first argument isn't stress, that means all arguments
-- have been shifted to the left one. We want to shift them
-- back to the right one, so we change the offset so that we
-- get the same effect of skipping a slot in the arg set.
if i - offset == 1 and not arg1_is_stress(args[i]) then
offset = offset - 1
end
if i - offset > 4 then
error("Too many arguments for argument set: arg " .. i .. " = " .. (args[i] or "(blank)"))
end
arg_set[i - offset] = args[i]
end
end
return generate_forms_1(args, per_word_info)
end
function export.do_generate_forms_multi(args, old)
old = old or args.old
args.old = old
args.pos = args.pos or "noun"
-- This is a list with each element corresponding to a word and
-- consisting of a two-element list, ARG_SET and JOINER, where ARG_SET
-- is a list of ARG_SET objects, one per alternative stem, and JOINER
-- is a string indicating how to join the word to the next one.
local per_word_info = {}
-- Find maximum-numbered arg, allowing for holes (FIXME: Is this needed
-- here? Will there be holes?)
local max_arg = 0
for k, v in pairs(args) do
if type(k) == "number" and k > max_arg then
max_arg = k
end
end
-- Gather arguments into a list of ARG_SET objects, containing
-- (potentially) elements 1, 2, 3, corresponding to accent pattern,
-- lemma+declension spec, pl stem, exactly as with do_generate_forms()
-- and {{ru-noun-table}} except that the values come from a single argument
-- of the form ACCENTPATTERN:LEMMADECL:PL where all but LEMMADECL may (and
-- probably will be) omitted and LEMMADECL may be of the following forms:
-- LEMMA (for a noun with empty decl spec),
-- LEMMADECL (for a noun with non-empty decl spec beginning with a
-- *, left paren or semicolon),
-- LEMMA^DECL (for a noun with non-empty decl spec),
-- LEMMA$ (for an indeclinable word)
-- LEMMA+ (for an adjective with auto-detected decl class)
-- LEMMA+DECL (for an adjective with explicit decl class)
-- Sets of parameters for the same word are separated by the word "or".
local arg_sets = {}
local continue_arg_sets = true
for i=1,(max_arg + 1) do
local end_word = false
local word_joiner
local process_arg = false
if i == max_arg + 1 then
end_word = true
word_joiner = {""}
elseif args[i] == "-" then
end_word = true
word_joiner = {"-"}
continue_arg_sets = true
elseif rfind(args[i], "^join:") then
end_word = true
word_joiner = extract_word_joiner(args[i])
continue_arg_sets = true
elseif args[i] == "or" then
continue_arg_sets = true
else
if continue_arg_sets then
continue_arg_sets = false
else
end_word = true
word_joiner = {" "}
end
process_arg = true
end
if end_word then
table.insert(per_word_info, {arg_sets, word_joiner})
arg_sets = {}
end
if process_arg then
local vals = rsplit(args[i], ":")
if #vals > 3 then
error("Can't specify more than 3 colon-separated params of param set: " .. args[i])
end
local arg_set = {}
if arg1_is_stress(vals[1]) then
arg_set[1] = vals[1]
arg_set[2] = vals[2]
arg_set[4] = vals[3]
else
arg_set[2] = vals[1]
arg_set[4] = vals[2]
end
-- recognize indeclinable
local indecl_stem = rmatch(arg_set[2], "^(.-)%$$")
if indecl_stem then
arg_set[2] = indecl_stem
arg_set[3] = "$"
else
-- recognize adjective
local adj_stem, adj_type = rmatch(arg_set[2], "^(.*)(%+.*)$")
if adj_stem then
arg_set[2] = adj_stem
arg_set[3] = adj_type
else
-- recognize noun with ^
local noun_stem, noun_type = rmatch(arg_set[2], "^(.*)%^(.*)$")
if noun_stem then
arg_set[2] = noun_stem
arg_set[3] = noun_type
else
-- recognize noun without ^ but with decl spec
noun_stem, noun_type = rmatch(arg_set[2], "^(..-)([;*(].*)$")
if noun_stem then
arg_set[2] = noun_stem
arg_set[3] = noun_type
else
-- noun without ^ or decl spec; nothing to do
end
end
end
end
table.insert(arg_sets, arg_set)
end
end
return generate_forms_1(args, per_word_info)
end
-- Implementation of do_generate_forms() and do_generate_forms_multi(),
-- which have equivalent functionality but different calling sequence.
-- Implementation of do_generate_forms() and do_generate_forms_multi(),
-- which have equivalent functionality but different calling sequence,
-- as well as show_z() for template {{ru-decl-noun-z}}, which has a
-- subset of the functionality of the other two.
generate_forms_1 = function(args, per_word_info)
local orig_args
if test_new_ru_noun_module then
orig_args = mw.clone(args)
end
local pagename = args.pagename or mw.loadData("Module:headword/data").pagename
local old = args.old
local function verify_animacy_value(val)
if not val then return nil end
if val == "a" or val == "an" or val == "anim" then
return "a"
elseif val == "i" or val == "in" or val == "inan" then
return "i"
elseif val == "b" or val == "bi" or val == "both" or val == "ai" then
return "ai"
elseif val == "ia" then
return "ia"
end
error("Animacy value " .. val .. " should be empty or a/an/anim (animate), i/in/inan (inanimate), b/bi/both/ai (bianimate, listing animate first), or ia (bianimate, listing inanimate first)")
return nil
end
local function verify_number_value(val, allow_none)
if not val then return nil end
local short = usub(val, 1, 1)
if short == "s" or short == "p" or short == "b" or (allow_none and short == "n" or false) then
return short
end
if allow_none then
error("Number value " .. val .. " should be empty or start with 's' (singular), 'p' (plural), 'b' (both) or 'n' (none)")
else
error("Number value " .. val .. " should be empty or start with 's' (singular), 'p' (plural), or 'b' (both)")
end
return nil
end
-- Verify and canonicalize animacy, number, prefix, suffix
assert(#per_word_info >= 1)
for i=1,#per_word_info do
args["a" .. i] = verify_animacy_value(args["a" .. i])
args["n" .. i] = verify_number_value(args["n" .. i])
args["prefix" .. i] = com.split_russian_tr(args["prefix" .. i] or "", "dopair")
args["suffix" .. i] = com.split_russian_tr(args["suffix" .. i] or "", "dopair")
end
args.a = verify_animacy_value(args.a) or "i"
-- args.ndef, if set, is the default value for args.n; if unset, it defaults
-- to "both". It is set to "singular" in ru-proper noun+. We store the value
-- of args.n in args.orign before defaulting to args.ndef because we may
-- change it to plural-only later on if it was unspecified (this happens if
-- an individual word's lemma is plural), and to determine whether it was
-- unspecified, we need the original value before defaulting.
args.n = verify_number_value(args.n, "allow none")
-- treat n=none like n=sg but set a flag so "singular" isn't displayed
if args.n == "n" then
args.n = "s"
args.nonumber = true
end
args.ndef = verify_number_value(args.ndef)
args.orign = args.n
args.n = args.n or args.ndef
args.prefix = com.split_russian_tr(args.prefix or "", "dopair")
args.suffix = com.split_russian_tr(args.suffix or "", "dopair")
-- Attach overall prefix to first per-word prefix, similarly for suffix
args.prefix1 = com.concat_paired_russian_tr(args.prefix, args.prefix1)
args["suffix" .. #per_word_info] = com.concat_paired_russian_tr(
args["suffix" .. #per_word_info], args.suffix)
-- Initialize non-word-specific arguments.
--
-- The following is a list of WORD_INFO items, one per word, each of
-- which is a two element list of WORD_FORMS (a table listing the forms for
-- each case) and JOINER (a string, indicating how to join the word with
-- the next one).
args.per_word_info = {}
-- List of HEADING_INFO items, one per word, containing the raw material
-- used to generate the header. Initialized from 'args.heading_info',
-- which is initialized in categorize_and_init_heading().
args.per_word_heading_info = {}
-- List of CATEGORIES items, one per word, containing the categories
-- used to initialize the page categories. Initialized from
-- 'args.categories', which is initialized in categorize_and_init_heading().
args.per_word_categories = {}
-- List of HEADINGS items, one per word, containing the actual words that
-- go into the header if we were to use that word to construct the header.
-- Comes from compute_heading(). We use this to generate the actual header
-- string, which goes into 'args.heading' at the end (done in
-- compute_overall_heading_categories_and_genders()).
args.per_word_headings = {}
-- List of GENDERS items, one per word, containing the headword genders
-- for each word (where "headword gender" is in the format used in
-- headwords and actually includes animacy and number as well, e.g.
-- 'm-in' or 'f-an-p'). We end up selecting one such item and putting
-- it into 'args.genders' at the end
-- (in compute_overall_heading_categories_and_genders()).
args.per_word_genders = {}
args.any_overridden = {}
args.any_non_nil = {}
args.any_irreg = false
args.any_irreg_case = {}
local function insert_cat(cat)
insert_category(args.categories, cat, args.pos)
end
args.internal_notes = {}
local decl_sufs = old and declensions_old or declensions
local decl_cats = old and declensions_old_cat or declensions_cat
local intable = old and internal_notes_table_old or internal_notes_table
-- Default lemma defaults to previous lemma, first one to page name.
-- FIXME: With multiple words, we should probably split the page name
-- on spaces and default each word in turn
local default_lemma
local all_stresses_seen
-- Made into a function to avoid having to indent a lot of code.
-- Process a single arg set of a single word. This inserts the forms
-- for the word into args.forms and sets categories and tracking pages.
local function do_arg_set(arg_set, n, islast)
local stress_arg = arg_set[1]
local decl = arg_set[3] or ""
local pl, pltr
if arg_set[4] then
pl, pltr = com.split_russian_tr(arg_set[4])
end
-- Extract special markers from declension class.
if decl == "manual" then
decl = "$"
args.manual = true
if #per_word_info > 1 or #per_word_info[1][1] > 1 then
error("Can't specify multiple words or argument sets when manual")
end
if pl then
error("Can't specify optional stem parameters when manual")
end
end
decl, args.jo_special = rsubb(decl, "([^/%a])ё$", "%1")
if not args.jo_special then
decl, args.jo_special = rsubb(decl, "([^/%a])ё([^/%a])", "%1%2")
end
decl, args.want_sc1 = rsubb(decl, "%(1%)", "")
decl, args.alt_gen_pl = rsubb(decl, "%(2%)", "")
decl, args.reducible = rsubb(decl, "%*", "")
decl, args.soft_n = rsubb(decl, "%(нь%)", "")
decl = rsub(decl, ";", "")
-- Get the lemma.
local lemma = args.manual and "-" or arg_set[2] or default_lemma
if not lemma then
error("Lemma in first argument set must be specified")
end
default_lemma = lemma
local lemmatr
lemma, lemmatr = com.split_russian_tr(lemma)
-- If we're conjugating a suffix, insert a pseudoconsonant at the beginning
-- of all forms, so they get conjugated as if ending in a consonant.
-- We remove the pseudoconsonant later.
local is_suffix = lemma ~= "-" and rfind(lemma, "^%-")
args.any_suffix = args.any_suffix or is_suffix
local asif_prefix = args["asif_prefix" .. n] or args.asif_prefix or is_suffix and PSEUDOCONS
if asif_prefix then
lemma = rsub(lemma, "^%-", "-" .. asif_prefix)
if lemmatr then
lemmatr = rsub(lemmatr, "^%-", "-" .. com.translit(asif_prefix))
end
end
args.thisa = args["a" .. n] or args.a
args.thisn = args["n" .. n] or args.n
-- Check for explicit allow-unaccented indication.
local allow_unaccented
lemma, allow_unaccented = rsubb(lemma, "^%*", "")
args.allow_unaccented = args.allow_unaccented or allow_unaccented
if args.allow_unaccented then
track("allow-unaccented")
end
args.orig_lemma = lemma
lemma = m_links.remove_links(lemma)
args.lemma_no_links = lemma
args.lemmatr = lemmatr
if args.lemma then
-- Explicit lemma given.
args.explicit_lemma, args.explicit_lemmatr = com.split_russian_tr(args.lemma)
end
-- Treat suffixes without an accent, and suffixes with an accent on the
-- initial hyphen, as if they were preceded with a *, which overrides
-- all the logic that normally (a) normalizes the accent, and (b)
-- complains about multisyllabic words without an accent. Don't do this
-- if lemma is just -, which is used specially in manual declension
-- tables (e.g. сто, три).
if lemma ~= "-" and (rfind(lemma, "^%-" .. AC) or (com.is_unstressed(lemma) and rfind(lemma, "^%-"))) then
args.allow_unaccented = true
end
-- Convert lemma and decl arg into stem and canonicalized decl.
-- This will autodetect the declension from the lemma if an explicit
-- decl isn't given.
local stem, tr, gender, was_accented, was_plural, was_autodetected
if rfind(decl, "^%+") then
stem, tr, decl, gender, was_accented, was_plural, was_autodetected =
detect_adj_type(lemma, lemmatr, decl, old)
else
stem, tr, decl, gender, was_accented, was_plural, was_autodetected =
determine_decl(lemma, lemmatr, decl, args)
end
if was_plural then
args.n = args.orign or "p"
args.thisn = args["n" .. n] or args.n
elseif decl ~= "$" then
args.thisn = args.thisn or "b"
end
args.explicit_gender = gender
-- If allow-unaccented not given, maybe check for missing accents.
if not args.allow_unaccented and not stress_arg and was_autodetected and com.needs_accents(lemma) then
-- If user gave the full word and expects us to determine the
-- declension and stress, the word should have an accent on the
-- stem or ending. We have a separate check farther below for
-- an accent on a multisyllabic stem, after stripping off any
-- ending; but this way we get an error if the user e.g. writes
-- "гора" without an accent rather than assuming it's stem
-- stressed, as would otherwise happen.
error("Lemma must have an accent in it: " .. lemma)
end
-- If stress not given, auto-determine; else validate/canonicalize
-- stress arg, override in certain cases and convert to list.
if not stress_arg then
stress_arg = {detect_stress_pattern(stem, decl, decl_cats, args.reducible, was_plural, was_accented)}
else
stress_arg = rsplit(stress_arg, ",")
for i=1,#stress_arg do
local stress = stress_arg[i]
stress = override_stress_pattern(decl, stress)
if not stress_patterns[stress] then
error("Unrecognized accent pattern " .. stress)
end
stress_arg[i] = stress
end
end
-- parse slash decl to list
local sub_decls
if rfind(decl, "/") then
track("mixed-decl")
insert_cat("~ with mixed declension")
local indiv_decls = rsplit(decl, "/")
-- Should have been caught in canonicalize_decl()
assert(#indiv_decls == 2)
sub_decls = {{indiv_decls[1], "sg"}, {indiv_decls[2], "pl"}}
else
sub_decls = {{decl}}
end
-- Get singular declension and corresponding category.
local sgdecl = sub_decls[1][1]
local sgdc = decl_cats[sgdecl]
assert(sgdc)
-- Compute headword gender(s). We base it off the singular declension
-- if we have a slash declension -- it's the best we can do.
determine_headword_gender(args, sgdc, gender)
local original_stem, original_tr = stem, tr
local original_pl, original_pltr = pl, pltr
-- Loop over accent patterns in case more than one given.
for _, stress in ipairs(stress_arg) do
args.suffixes = {}
stem, tr = original_stem, original_tr
local bare, baretr
local stem_for_bare, tr_for_bare
pl, pltr = original_pl, original_pltr
insert_if_not(all_stresses_seen, stress)
local stem_was_unstressed = com.is_unstressed(stem)
-- If special case ;ё was given and stem is unstressed,
-- add ё to the stem now; but don't let this interfere with
-- restressing, to handle cases like железа́ with gen pl желёз
-- but nom pl же́лезы.
if stem_was_unstressed and args.jo_special then
-- Beware, Cyrillic еЕ in first rsub, Latin eE in second
local new_stem = rsub(stem, "([еЕ])([^еЕ]*)$",
function(e, rest)
return (e == "Е" and "Ё" or "ё") .. rest
end
)
if stem == new_stem then
error("No е in stem to replace with ё")
end
stem = new_stem
if tr then
local subbed
-- e after j -> o, e not after j -> jo; don't just convert e -> jo
-- and then map jjo -> jo because we want to preserve double j
tr, subbed = rsubb(tr, "([jJ])([eE])([^eE]*)$",
function(j, e, rest)
return j .. (e == "E" and "O" or "o") .. AC .. rest
end
)
if not subbed then
tr = rsub(tr, "([eE])([^eE]*)$",
function(e, rest)
return (e == "E" and "Jo" or "jo") .. AC .. rest
end
)
end
tr = com.j_correction(tr)
end
-- This is used to handle железа́ with gen pl желёз and nom pl
-- же́лезы. We have two stressed stems, one for the gen pl and
-- one for the remaining pl cases, and the variable 'stem' can
-- handle only one, so we put the second (gen pl) stem in
-- stem_for_bare, which goes into the value of 'bare' (used
-- only for gen pl here).
stem_for_bare, tr_for_bare = stem, tr
end
-- Maybe add stress to the stem, depending on whether the
-- stem was unstressed and the stress pattern. Stem pattern f
-- and variants call for initial stress (голова́ -> го́ловы);
-- stem pattern d and variants call for stem-final stress
-- (сапожо́к -> сапо́жки). Stem patterns b and b' apparently
-- call for stem-final stress as well but it's unlikely to
-- make much of a difference (pattern b' only occurs in 3rd-decl
-- feminines, which should already have stress in the stem,
-- and the only place pattern b gets stem stress is in bare
-- forms, i.e. nom sg and/or gen pl depending on the decl type,
-- and nom sg stress should already be in the lemma while
-- gen pl stress is handled by a different ending-stressing
-- mechanism in attach_unstressed(); however, the user is
-- free to leave a masc or 3rd-decl fem lemma completely
-- unstressed with pattern b, and then the stem-final stress
-- *will* make a difference).
local function restress_stem(stem, tr, stress, stem_unstressed)
-- If the user has indicated they purposely are leaving the
-- word unstressed by putting a * at the beginning of the main
-- stem, leave it unstressed. This might indicate lack of
-- knowledge of the stress or a truly unaccented word
-- (e.g. an unaccented suffix).
if args.allow_unaccented then
return stem, tr
end
if tr and com.is_unstressed(stem) ~= com.is_unstressed(tr) then
error("Stem " .. stem .. " and translit " .. tr .. " must have same accent pattern")
end
-- it's safe to accent monosyllabic stems
if com.is_monosyllabic(stem) then
stem, tr = com.make_ending_stressed(stem, tr)
-- For those patterns that are ending-stressed in the singular
-- nominative (and hence are likely to be expressed without an
-- accent on the stem) it's safe to put a particular accent on
-- the stem depending on the stress type. Otherwise, give an
-- error if no accent.
elseif stem_unstressed then
if rfind(stress, "^f") then
stem, tr = com.make_beginning_stressed(stem, tr)
elseif (rfind(stress, "^[bd]") or
args.thisn == "p" and ending_stressed_pl_patterns[stress]) then
stem, tr = com.make_ending_stressed(stem, tr)
elseif com.needs_accents(stem) then
error("Stem " .. stem .. " requires an accent")
end
end
return stem, tr
end
stem, tr = restress_stem(stem, tr, stress, stem_was_unstressed)
-- Leave pl unaccented if user wants this; see restress_stem().
if pl and not args.allow_unaccented then
if pltr and com.is_unstressed(pl) ~= com.is_unstressed(pltr) then
error("Plural stem " .. pl .. " and translit " .. pltr .. " must have same accent pattern")
end
if com.is_monosyllabic(pl) then
pl, pltr = com.make_ending_stressed(pl, pltr)
end
-- I think this is safe.
if com.needs_accents(pl) then
if ending_stressed_pl_patterns[stress] then
pl, pltr = com.make_ending_stressed(pl, pltr)
elseif not args.allow_unaccented then
error("Plural stem " .. pl .. " requires an accent")
end
end
end
local resolved_bare, resolved_baretr
-- Handle (de)reducibles
-- FIXME! We are dereducing based on the singular declension.
-- In a slash declension things can get weird and we don't
-- handle that. We are also computing the bare value from the
-- singular stem, and again things can get weird with a plural
-- stem. Note that we don't compute a bare value unless we have
-- to (either (de)reducible or stress pattern f/f'/f'' combined
-- with ё special case); the remaining times we generate the bare
-- value directly from the plural stem.
if args.reducible and not sgdc.ignore_reduce then
-- Zaliznyak treats all nouns in -ье and -ья as being
-- reducible. We handle this automatically and don't require
-- the user to specify this, but ignore it if so for
-- compatibility.
if is_reducible(sgdc) then
-- If we derived the stem from a nom pl form, then
-- it's already reduced, and we need to dereduce it to
-- get a bare form; otherwise the stem comes from the
-- nom sg and we need to reduce it to get the real stem.
if was_plural then
resolved_bare, resolved_baretr =
export.dereduce_nom_sg_stem(stem, tr, sgdc,
stress, old, "error")
else
resolved_bare, resolved_baretr = stem, tr
stem, tr = export.reduce_nom_sg_stem(stem, tr,
sgdecl, args.soft_n, "error")
-- Stem will be unstressed if stress was on elided
-- vowel; restress stem the way we did above. (This is
-- needed in at least one word, сапожо́к 3*d(2), with
-- plural stem probably сапо́жк- and gen pl probably
-- сапо́жек.)
stem, tr = restress_stem(stem, tr, stress,
com.is_unstressed(stem))
if stress ~= "a" and stress ~= "b" and args.alt_gen_pl and not pl then
-- Nouns like рожо́к, глазо́к of type 3*d(2) have
-- gen pl's ро́жек, гла́зок; to handle this,
-- dereduce the reduced stem and store in a
-- special place.
args.gen_pl_bare, args.gen_pl_baretr =
export.dereduce_nom_sg_stem(stem, tr,
sgdc, stress, old, "error")
end
end
elseif is_dereducible(sgdc) then
resolved_bare, resolved_baretr =
export.dereduce_nom_sg_stem(stem, tr, sgdc,
stress, old, "error")
else
error("Declension class " .. sgdecl .. " not (de)reducible")
end
elseif stem_for_bare and stem ~= stem_for_bare then
resolved_bare, resolved_baretr =
add_bare_suffix(stem_for_bare, tr_for_bare, old, sgdc, false)
end
-- Leave unaccented if user wants this; see restress_stem().
-- FIXME, we no longer allow the user to specify the bare value
-- so it's unclear if this is needed any more.
if resolved_bare and not args.allow_unaccented then
if resolved_baretr and com.is_unstressed(resolved_bare) ~= com.is_unstressed(resolved_baretr) then
error("Resolved bare stem " .. resolved_bare .. " and translit " .. resolved_baretr .. " must have same accent pattern")
end
if com.is_monosyllabic(resolved_bare) then
resolved_bare, resolved_baretr =
com.make_ending_stressed(resolved_bare, resolved_baretr)
else
if com.needs_accents(resolved_bare) then
error("Resolved bare stem " .. resolved_bare .. " requires an accent")
end
end
end
args.stem, args.stemtr = stem, tr
args.bare, args.baretr = resolved_bare, resolved_baretr
args.ustem, args.ustemtr = com.make_unstressed_once(stem, tr)
if pl then
args.pl, args.pltr = pl, pltr
else
args.pl, args.pltr = stem, tr
end
args.upl, args.upltr = com.make_unstressed_once(args.pl, args.pltr)
-- Special hack for любо́вь and other reducible 3rd-fem nouns,
-- which have the full stem in the ins sg
args.ins_sg_stem = sgdecl == "ь-f" and args.reducible and resolved_bare
args.ins_sg_tr = sgdecl == "ь-f" and args.reducible and resolved_baretr
-- Loop over declension classes (we may have two of them, one for
-- singular and one for plural, in the case of a mixed declension
-- class of the form SGDECL/PLDECL).
for _,decl_spec in ipairs(sub_decls) do
local orig_decl = decl_spec[1]
local number = decl_spec[2]
local real_decl =
determine_stress_variant(orig_decl, stress)
real_decl = determine_stem_variant(real_decl,
number == "pl" and args.pl or args.stem)
-- sanity checking; errors should have been caught in
-- canonicalize_decl()
assert(decl_cats[real_decl], "real_decl " .. real_decl .. " nonexistent")
assert(decl_sufs[real_decl], "real_decl " .. real_decl .. " nonexistent")
tracking_code(stress, orig_decl, real_decl, args, n, islast)
do_stress_pattern(stress, args, real_decl, number, n, islast)
-- handle internal notes
local internal_note = intable[real_decl]
if internal_note then
insert_if_not(args.internal_notes, internal_note)
end
end
categorize_and_init_heading(stress, decl, args, n, islast)
end
end
local n = 0
for _, word_info in ipairs(per_word_info) do
n = n + 1
local islast = n == #per_word_info
local arg_sets, joiner = word_info[1], word_info[2]
args.forms = {}
args.heading_info = {animacy={}, number={}, gender={}, stress={},
stemetc={}, adjectival={}, reducible={}}
args.categories = {}
args.genders = {}
args.this_any_non_nil = {}
args.any_suffix = false
if #arg_sets > 1 then
track("multiple-arg-sets")
insert_cat("~ with multiple argument sets")
track("multiple-declensions")
insert_cat("~ with multiple declensions")
end
default_lemma = pagename
all_stresses_seen = {}
-- Loop over all arg sets.
for _, arg_set in ipairs(arg_sets) do
do_arg_set(arg_set, n, islast)
end
if #all_stresses_seen > 1 then
track("multiple-accent-patterns")
insert_cat("~ with multiple accent patterns")
track("multiple-declensions")
insert_cat("~ with multiple declensions")
end
table.insert(args.per_word_heading_info, args.heading_info)
table.insert(args.per_word_categories, args.categories)
local headings = compute_heading(args)
table.insert(args.per_word_headings, headings)
table.insert(args.per_word_genders, args.genders)
handle_forms_and_overrides(args, n, islast)
if args.any_suffix then
-- If we're conjugating a suffix, remove the pseudoconsonant or asif_prefix
-- that we previously inserted at the beginning.
local asif_prefix = args["asif_prefix" .. n] or args.asif_prefix or PSEUDOCONS
local asif_prefix_tr = com.translit(asif_prefix)
for _, case in ipairs(all_cases) do
if args.forms[case] then
local newforms = {}
for _, form in ipairs(args.forms[case]) do
local formru = form[1]
local formtr = form[2]
formru = rsub(formru, "^%-" .. asif_prefix, "-")
if formtr then
formtr = rsub(formtr, "^%-" .. asif_prefix_tr, "-")
end
if formru == "-" then
-- if no ending, insert "(no suffix)".
table.insert(newforms, {"(no suffix)"})
else
table.insert(newforms, {formru, formtr})
end
end
args.forms[case] = newforms
end
end
end
table.insert(args.per_word_info, {args.forms, joiner})
end
handle_overall_forms_and_overrides(args)
compute_overall_heading_categories_and_genders(args)
for _, case in ipairs(all_cases) do
if args[case] then
for _, form in ipairs(args[case]) do
local ru, tr = form[1], form[2]
local ruentry, runotes = m_table_tools.separate_notes(ru)
ruentry = m_links.remove_links(ruentry)
if rfind(ulower(ruentry), latin_text_class) then
--error("Found Latin text " .. ruentry .. " in case " .. case)
track("latin-text")
track("latin-text/" .. case)
end
end
end
end
-- Test code to compare existing module to new one.
if test_new_ru_noun_module then
local m_new_ru_noun = require("Module:User:Benwing2/ru-noun")
local newargs = m_new_ru_noun.do_generate_forms(orig_args, old)
local difdecl = false
for _, case in ipairs(all_cases) do
local arg = args[case]
local newarg = newargs[case]
local is_pl = rfind(case, "_pl")
if args.thisn == "s" and is_pl or args.thisn == "p" and not is_pl then
-- Don't need to check cases that won't be displayed.
elseif not m_table.deepEquals(arg, newarg) then
local monosyl_accent_diff = false
-- Differences only in monosyllabic accents. Enable if we
-- change the algorithm for these.
--if arg and newarg and #arg == 1 and #newarg == 1 then
-- local ru1, tr1 = arg[1][1], arg[1][2]
-- local ru2, tr2 = newarg[1][1], newarg[1][2]
-- if com.is_monosyllabic(ru1) and com.is_monosyllabic(ru2) then
-- ru1, tr1 = com.remove_accents(ru1, tr1)
-- ru2, tr2 = com.remove_accents(ru2, tr2)
-- if ru1 == ru2 and tr1 == tr2 then
-- monosyl_accent_diff = true
-- end
-- end
--end
if monosyl_accent_diff then
track("monosyl-accent-diff")
difdecl = true
else
-- Uncomment this to display the particular case and
-- differing forms.
--error(case .. " " .. (arg and com.concat_forms(arg) or "nil") .. " || " .. (newarg and com.concat_forms(newarg) or "nil"))
track("different-decl")
difdecl = true
end
break
end
end
if not difdecl then track("same-decl") end
end
return args
end
-- Implementation of main entry point
local function do_show(frame, old)
local args = clone_args(frame)
local args = export.do_generate_forms(args, old)
return make_table(args) .. m_utilities.format_categories(args.categories, lang)
end
-- The main entry point for modern declension tables.
function export.show(frame)
return do_show(frame, false)
end
-- The main entry point for old declension tables.
function export.show_old(frame)
return do_show(frame, true)
end
-- Implementation of new entry point, esp. for multiple words
local function do_show_multi(frame)
local args = clone_args(frame)
local args = export.do_generate_forms_multi(args)
return make_table(args) .. m_utilities.format_categories(args.categories, lang)
end
-- The new entry point, esp. for multiple words (but works fine for
-- single words).
function export.show_multi(frame)
return do_show_multi(frame)
end
local function get_form(forms, preserve_links, raw)
local canon_forms = {}
for _, form in ipairs(forms) do
if raw then
local ru, tr = form[1], form[2]
ru = rsub(ru, "|", "<!>")
if tr then
tr = rsub(tr, "|", "<!>")
end
insert_if_not(canon_forms, {ru, tr})
else
local ru, tr = form[1], form[2]
local ruentry, runotes = m_table_tools.separate_notes(ru)
-- Skip hypothetical forms (but include in the raw versions)
if not rfind(runotes, HYPMARKER) then
local trentry, trnotes
if tr then
trentry, trnotes = m_table_tools.separate_notes(tr)
end
if not preserve_links then
ruentry = m_links.remove_links(ruentry)
end
ruentry = rsub(ruentry, "|", "<!>")
if trentry then
trentry = rsub(trentry, "|", "<!>")
end
insert_if_not(canon_forms, {ruentry, trentry})
end
end
end
return com.concat_forms(canon_forms)
end
local function case_will_be_displayed(args, case)
local ispl = rfind(case, "_pl")
local caseok = true
if args.n == "p" then
caseok = ispl
elseif args.n == "s" then
caseok = not ispl
end
for _, override_case in ipairs(overridable_only_cases) do
if case == override_case and not args.any_overridden[override_case] then
caseok = false
break
end
end
if args.a == "a" or args.a == "i" then
if rfind(case, "_[ai]n") then
caseok = false
end
else -- bianimate
-- don't include inanimate/animate variants if combined variant exists
-- (typically because inanimate/animate variants are the same);
-- FIXME: This could conceivably be different from how the display
-- code works, which just checks that the inanimate/animate variants
-- are the same when deciding whether to display them, in particular
-- if there is an override. Here we are following the algorithm of
-- handle_overall_forms_and_overrides().
if (case == "acc_sg_in" or case == "acc_sg_an") and args.acc_sg or
(case == "acc_pl_in" or case == "acc_pl_an") and args.acc_pl then
caseok = false
end
end
if not args[case] then
caseok = false
end
return caseok
end
local function concat_case_args(args, do_all, raw)
local ins_text = {}
for _, case in ipairs(do_all and all_cases or overridable_cases) do
if case_will_be_displayed(args, case) then
local forms = get_form(args[case], rfind(case, "_linked"), raw)
if forms ~= "" then
table.insert(ins_text, case .. (raw and "_raw" or "") .. "=" ..
forms)
end
end
end
return table.concat(ins_text, "|")
end
-- The entry point for 'ru-noun-forms' to generate all noun forms.
-- This returns a single string, with | separating arguments and named
-- arguments of the form NAME=VALUE.
function export.generate_forms(frame)
local args = clone_args(frame)
args = export.do_generate_forms(args, false)
return concat_case_args(args)
end
-- The entry point to generate multiple sets of noun forms. This is a hack
-- to speed up calling from a bot, where we often want to compare old and new
-- argument results to make sure they're the same. Each set of arguments is
-- jammed together into a single argument with individual values separated by
-- <!>; named arguments are of the form NAME<->VALUE. The return value for
-- each set of arguments is as in export.generate_forms(), and the return
-- values are concatenated with <!> separating them. NOTE: This will fail if
-- the exact sequences <!> or <-> happen to occur in values (which is unlikely,
-- esp. as we don't even use the characters <, ! or > for anything) and aren't
-- HTML-escaped.
function export.generate_multi_forms(frame)
local retvals = {}
for _, argset in ipairs(frame.args) do
local args = {}
local i = 0
local argvals = rsplit(argset, "<!>")
for _, argval in ipairs(argvals) do
local split_arg = rsplit(argval, "<%->")
if #split_arg == 1 then
i = i + 1
args[i] = ine(split_arg[1])
else
assert(#split_arg == 2)
args[split_arg[1]] = ine(split_arg[2])
end
end
args = export.do_generate_forms(args, false)
table.insert(retvals, concat_case_args(args))
end
return table.concat(retvals, "<!>")
end
-- The entry point for 'ru-noun-form' to generate a particular noun form.
function export.generate_form(frame)
local args = clone_args(frame)
if not args.form then
error("Must specify desired form using form=")
end
local form = args.form
if not m_table.contains(all_cases, form) then
error("Unrecognized form " .. form)
end
local args = export.do_generate_forms(args, false)
if not args[form] then
return ""
else
return get_form(args[form])
end
end
-- The entry point for generating arguments of various sorts, including
-- the case forms, gender, number and animacy.
function export.generate_args(frame)
local args = clone_args(frame)
args = export.do_generate_forms(args, false)
local retargs = {}
table.insert(retargs, concat_case_args(args, "doall"))
table.insert(retargs, concat_case_args(args, "doall", "raw"))
table.insert(retargs, "g=" .. table.concat(args.genders, ","))
-- The following is correct even with ndef because if ndef is
-- set we will set it in args.n.
table.insert(retargs, "n=" .. (args.n or "b"))
table.insert(retargs, "a=" .. (args.a or "i"))
return table.concat(retargs, "|")
end
-- The entry point for compatibility with {{ru-decl-noun-z}}.
function export.show_z(frame)
local args = clone_args(frame)
local stem = args[1]
local stress = args[2]
local specific = args[4] or ""
-- Parse gender/animacy spec
local gender, anim = rmatch(args[3], "^([mfn])-([a-z]+)")
if not gender then
error("Unrecognized gender/anim spec " .. args[3])
end
if anim ~= "an" and anim ~= "in" then anim = "both" end
args.a = anim
-- Handle specific
specific = rsub(specific, "ё", ";ё")
-- Compute decl; special case for семьянин (perhaps not necessary)
local decl = com.make_unstressed_once(stem) == "семьянин" and "#" .. specific or gender .. specific
-- Handle overrides
args.pre_sg = args.prp_sg
args.pre_pl = args.prp_pl
args.notes = args.note
if args.par then
args.par = "+"
end
if args.loc then
if args.loc == "в" then
args.loc = "в +"
elseif args.loc == "на" then
args.loc = "на +"
else
args.loc = "в +,на +"
end
end
local arg_set = {}
table.insert(arg_set, stress)
table.insert(arg_set, stem)
table.insert(arg_set, decl)
local per_word_info = {{{arg_set}, ""}}
return generate_forms_1(args, per_word_info)
end
--------------------------------------------------------------------------
-- Autodetection and lemma munging --
--------------------------------------------------------------------------
-- Attempt to detect the type of the lemma based on its ending, separating
-- off the stem and the ending. GENDER must be present with -ь and plural
-- stems, and is otherwise ignored. Return up to three values: The stem
-- (lemma minus ending), the singular lemma ending, and if the lemma was
-- plural, the plural lemma ending. If the lemma was singular, the singular
-- lemma ending will contain any user-given accents; likewise, if the
-- lemma was plural, the plural ending will contain such accents.
-- VARIANT comes from the declension spec and controls certain declension
-- variants.
local function detect_lemma_type(lemma, tr, gender, args, variant)
local base, ending = rmatch(lemma, "^(.*)([еЕ]" .. AC .. ")$") -- accented
if base then
return base, com.strip_tr_ending(tr, ending), ulower(ending)
end
base, ending = rmatch(lemma, "^(.*[" .. com.sib_c .. "])([еЕ])$") -- unaccented
if base then
if variant == "-ище" and not rfind(lemma, "[щЩ][еЕ]$") then
error("With declension variant -ище, lemma should end in -ще: " .. lemma)
end
return base, com.strip_tr_ending(tr, ending), variant == "-ище" and "(ищ)е-и" or "о"
end
if variant == "-ишко" then
base, ending = rmatch(lemma, "^(.*[шШ][кК])([оО])$") -- unaccented
if not base then
error("With declension variant -ишко, lemma should end in -шко: " .. lemma)
end
return base, com.strip_tr_ending(tr, ending), "(ишк)о-и"
end
if variant == "-ин" then
base, ending = rmatch(lemma, "^(.*)([иИ][" .. AC .. GR .. "]?[нН][ъЪ]?)$") -- maybe accented
if not base then
error("With declension variant -ин, lemma should end in -ин(ъ): " .. lemma)
end
return base, com.strip_tr_ending(tr, ending), ulower(ending)
end
-- Now autodetect -ин; only animate and in -анин/-янин
base, ending = rmatch(lemma, "^(.*[аАяЯ][" .. AC .. GR .. "]?[нН])([иИ][" .. AC .. GR .. "]?[нН][ъЪ]?)$")
-- Need to check the animacy to avoid nouns like маиганин, цианин,
-- меланин, соланин, etc.
if base and args.thisa == "a" then
return base, com.strip_tr_ending(tr, ending), ulower(ending)
end
base, ending = rmatch(lemma, "^(.*)([ёЁ]" .. AC .. "?[нН][оО][кК][ъЪ]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), ulower(ending)
end
base, ending = rmatch(lemma, "^(.*[" .. com.sib_c .. "])([оО]" .. AC .. "[нН][оО][кК][ъЪ]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), ulower(ending)
end
base, ending = rmatch(lemma, "^(.*)([ёЁ]" .. AC .. "?[нН][оО][чЧ][еЕ][кК][ъЪ]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), ulower(ending)
end
base, ending = rmatch(lemma, "^(.*[" .. com.sib_c .. "])([оО]" .. AC .. "[нН][оО][чЧ][еЕ][кК][ъЪ]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), ulower(ending)
end
base, ending = rmatch(lemma, "^(.*)([мМ][яЯ][" .. AC .. GR .. "]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), ulower(ending)
end
--recognize plural endings
if gender == "n" then
base, ending = rmatch(lemma, "^(.*)([ьЬ][яЯ][" .. AC .. GR .. "]?)$")
if base then
-- Don't do this; о/-ья is too rare
-- error("Ambiguous plural lemma " .. lemma .. " in -ья, singular could be -о or -ье/-ьё; specify the singular")
return base, com.strip_tr_ending(tr, ending), "ье", ending
end
base, ending = rmatch(lemma, "^(.*)([аяАЯ][" .. AC .. GR .. "]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), rfind(ending, "[аА]") and "о" or "е", ending
end
base, ending = rmatch(lemma, "^(.*)([ыиЫИ][" .. AC .. GR .. "]?)$")
if base then
if rfind(ending, "[ыЫ]") or rfind(base, "[" .. com.sib .. com.velar .. "]$") then
return base, com.strip_tr_ending(tr, ending), "о-и", ending
else
-- FIXME, should we return a slash declension?
error("No neuter declension е-и available; use a slash declension")
end
end
end
if gender == "f" then
base, ending = rmatch(lemma, "^(.*)([ьЬ][иИ][" .. AC .. GR .. "]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), "ья", ending
end
end
-- Recognize masculines with irregular plurals, but only if the user
-- either explicitly specified that this noun is plural (n=p) or
-- specifically requested the irregular plural. This is necessary
-- because some masculine nouns have feminine endings, which look
-- like irregular plurals.
if gender == "m" then
if args.thisn == "p" or variant == "-ья" then
base, ending = rmatch(lemma, "^(.*)([ьЬ][яЯ][" .. AC .. GR .. "]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), (args.old and "ъ-ья" or "-ья"), ending
end
end
if args.thisn == "p" or args.want_sc1 then
base, ending = rmatch(lemma, "^(.*)([аА][" .. AC .. GR .. "]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), (args.old and "ъ-а" or "-а"), ending
end
base, ending = rmatch(lemma, "^(.*)([яЯ][" .. AC .. GR .. "]?)$")
if base then
if rfind(base, "[" .. com.vowel .. "][" .. AC .. GR .. "]?$") then
return base, com.strip_tr_ending(tr, ending), "й-я", ending
else
return base, com.strip_tr_ending(tr, ending), "ь-я", ending
end
end
end
end
if gender == "m" or gender == "f" then
base, ending = rmatch(lemma, "^(.*[" .. com.sib .. com.velar .. "])([иИ][" .. AC .. GR .. "]?)$")
if not base then
base, ending = rmatch(lemma, "^(.*)([ыЫ][" .. AC .. GR .. "]?)$")
end
if base then
return base, com.strip_tr_ending(tr, ending), gender == "m" and (args.old and "ъ" or "") or "а", ending
end
base, ending = rmatch(lemma, "^(.*[" .. com.vowel .. "й][" .. AC .. GR .. "]?)([иИ][" .. AC .. GR .. "]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), gender == "m" and "й" or "я", ending
end
base, ending = rmatch(lemma, "^(.*)([иИ][" .. AC .. GR .. "]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), gender == "m" and "ь-m" or "я", ending
end
end
if gender == "3f" then
base, ending = rmatch(lemma, "^(.*)([иИ][" .. AC .. GR .. "]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), "ь-f", ending
end
end
-- end of recognize-plurals code
base, ending = rmatch(lemma, "^(.*)([ьЬ][яеёЯЕЁ][" .. AC .. GR .. "]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), ulower(ending)
end
base, ending = rmatch(lemma, "^(.*)([йаяеоёъЙАЯЕОЁЪ][" .. AC .. GR .. "]?)$")
if base then
return base, com.strip_tr_ending(tr, ending), ulower(ending)
end
base, ending = rmatch(lemma, "^(.*)([ьЬ])$")
if base then
if gender == "m" or gender == "f" then
return base, com.strip_tr_ending(tr, ending), "ь-" .. gender
elseif gender == "3f" then
return base, com.strip_tr_ending(tr, ending), "ь-f"
else
error("Need to specify gender m or f with lemma in -ь: ".. lemma)
end
end
if rfind(lemma, "[ыиЫИ][" .. AC .. GR .. "]?$") then
error("If this is a plural lemma, gender must be specified: " .. lemma)
elseif rfind(lemma, "[иИіІуУыЫѣѢэЭюЮѵѴ]" .. DIA .. "?[" .. AC .. GR .. "]?$") then
error("Don't know how to decline lemma ending in this type of vowel: " .. lemma)
end
return lemma, tr, ""
end
local plural_variant_detection_map = {
[""] = {["-ья"]="-ья"},
["ъ"] = {["-ья"]="ъ-ья"},
}
local special_case_1_to_plural_variant = {
[""] = "-а",
["ъ"] = "ъ-а",
["й"] = "й-я",
["ь-m"] = "ь-я",
["о"] = "о-и",
-- these last two are here to avoid getting errors in that checks for
-- compatibility with special case 1; the basic variants of these decls
-- don't actually exist
["(ишк)о"] = "(ишк)о-и",
["(ищ)е"] = "(ищ)е-и",
}
local function map_decl(decl, fun)
if rfind(decl, "/") then
local split_decl = rsplit(decl, "/")
if #split_decl ~= 2 then
error("Mixed declensional class " .. decl
.. "needs exactly two classes, singular and plural")
end
return fun(split_decl[1]) .. "/" .. fun(split_decl[2])
else
return fun(decl)
end
end
-- Canonicalize decl class into non-accented and alias-resolved form;
-- but note that some canonical decl class names with an accent in them
-- (e.g. е́, not the same as е, whose accented version is ё; and various
-- adjective declensions).
local function canonicalize_decl(decl, old)
local function do_canon(decl)
-- remove accents, but not from е́ (for adj decls, accent matters
-- as well but we handle that by mapping the accent to a stress pattern
-- and then to the accented version in determine_stress_variant())
if decl ~= "е́" then
decl = com.remove_accents(decl)
end
local decl_aliases = old and declensions_old_aliases or declensions_aliases
local decl_cats = old and declensions_old_cat or declensions_cat
if decl_aliases[decl] then
-- If we find an alias, map it.
decl = decl_aliases[decl]
elseif not decl_cats[decl] then
error("Unrecognized declension class " .. decl)
end
return decl
end
return map_decl(decl, do_canon)
end
-- Attempt to determine the actual declension (including plural variants)
-- based on a combination of the declension the user specified, what can be
-- detected from the lemma, and special case (1), if given in the declension.
-- DECL is the value the user passed for the declension field, after
-- extraneous annotations (special cases (1) and (2), * for reducible,
-- ё for ё/ё alternation and a ; that may precede ё) have been stripped off.
-- What's left is one of the following:
--
-- 1. Blank, meaning to autodetect the declension from the lemma
-- 2. A hyphen followed by a declension variant (-ья, -ин, -ишко, -ище; see
-- long comment at top of file)
-- 3. A gender (m, f, n, 3f)
-- 4. A gender plus declension variant (e.g. f-ья)
-- 5. An actual declension, possibly including a plural variant (e.g. о-и) or
-- a slash declension (e.g. я/-ья, used for the noun дядя).
--
-- Return seven args: stem (lemma minus ending), translit, canonicalized
-- declension, explicitly specified gender if any (m, f, n or nil), whether
-- the specified declension or detected ending was accented, whether the
-- detected ending was pl, and whether the declension was autodetected
-- (corresponds to cases where a full word with ending attached is required
-- in the lemma field). "Canonicalized" means after autodetection, with
-- accents removed, with any aliases mapped to their canonical versions
-- and with any requested declension variants applied. The result is either a
-- declension that will have a categorization entry (in declensions_cat[] or
-- declensions_old_cat[]) or a slash declension where each part similarly has
-- a categorization entry.
--
-- Note that gender is never required when an explicit declension is given,
-- and in connection with stem autodetection is required only when the lemma
-- either ends in -ь or is plural.
determine_decl = function(lemma, tr, decl, args)
-- Assume we're passed a value for DECL of types 1-4 above, and
-- fetch gender and requested declension variant.
local stem
local want_ya_plural, orig_pl_ending, variant
local was_autodetected
local gender = rmatch(decl, "^(3?[mfn]?)$")
if not gender then
gender, variant = rmatch(decl, "^(3?[mfn]?)(%-[^%-]+)$")
-- But be careful with explicit declensions like -а that look like
-- variants without gender (FIXME, eventually we should maybe do
-- something about the potential ambiguity).
if gender == "" and not m_table.contains({"-ья", "-ин", "-ишко", "-ище"}, variant) then
gender, variant = nil, nil
end
end
-- If DECL is of type 1-4, handle declension variants and detect
-- the actual declension from the lemma.
if gender then
-- Check for declension variants
if variant then
if variant == "-ья" then
want_ya_plural = "-ья"
else
-- Sanity-check remaining declension variants, which need
-- specific values of animacy, gender and special-case (1)
local sc1_needed
local animate_needed
if variant == "-ишко" then
animate_needed = false
sc1_needed = true
elseif variant == "-ище" then
animate_needed = true
sc1_needed = true
elseif variant == "-ин" then
animate_needed = true
sc1_needed = false
else
-- WARNING: If adding another variant, you need to also
-- add to the list farther above.
error("Unrecognized declension variant " .. variant .. ", should be -ья, -ин, -ишко or -ище")
end
if sc1_needed and not args.want_sc1 then
error("Declension variant " .. variant .. " must be used with special case (1)")
elseif sc1_needed == false and args.want_sc1 then
error("Declension variant " .. variant .. " must not be used with special case (1)")
end
if animate_needed and args.thisa ~= "a" then
error("Declension variant " .. variant .. " must be specified as animate")
elseif animate_needed == false and args.thisa == "a" then
error("Declension variant " .. variant .. " must not be specified as animate")
end
if gender ~= "" and gender ~= "m" then
error("Declension variant " .. variant .. " should be used with the masculine gender")
end
end
end
stem, tr, decl, orig_pl_ending = detect_lemma_type(lemma, tr, gender,
args, variant)
was_autodetected = true
else
stem, tr = lemma, tr
end
-- Now canonicalize gender
if gender == "3f" then
gender = "f"
elseif gender == "" then
gender = nil
end
-- The ending should be treated as accented if either the original singular
-- or plural ending was accented, or if the stem is non-syllabic.
local was_accented = com.is_stressed(decl) or
orig_pl_ending and com.is_stressed(orig_pl_ending) or
com.is_nonsyllabic(stem)
local was_plural = not not orig_pl_ending
decl = canonicalize_decl(decl, args.old)
-- The rest of this code concerns plural variants. It's somewhat
-- complicated because there are potentially four sources of plural
-- variants (not to mention plural variants constructed using slash
-- notation):
--
-- 1. A user-requested plural variant in declension types 2 or 4 above
-- (currently only -ья)
-- 2. An explicit plural variant encoded in an explicit declension of
-- type 5 above
-- 3. An autodetected plural variant (which will happen in some cases
-- when autodetection is performed on a nominative plural)
-- 4. A plural variant derived using special case (1).
--
-- Up to three actual plural variants might exist (e.g. if the user
-- specifies a DECL value or 'm-ья(1)' and a STEM ending in -а,
-- although not all three can ever be compatible because -ья and (1)
-- are never compatible). We can't have all four because if there's
-- an explicit plural variant, there won't be a user-requested or
-- autodetected plural variant.
--
-- The goal below is to do two things: Check that all available plural
-- variants are the same, and generate the actual declension.
-- If we have a type-2 or type-3 variant, we already have the actual
-- declension; else we need to use a table to map the basic declension
-- to the one with the plural variant encoded in it.
--
-- NOTE: The code below was written with a more general plural-variant
-- system. It probably can be simplified a lot now.
-- 1: Handle explicit decl with slash variant
if rfind(decl, "/") then
if want_ya_plural then
-- Don't think this can happen
error("Plural variant " .. want_ya_plural .. " not compatible with slash declension " .. decl)
end
if args.want_sc1 then
error("Special case (1) not compatible with slash declension" .. decl)
end
return stem, tr, decl, gender, was_accented, was_plural, was_autodetected
end
-- 2: Retrieve explicitly specified or autodetected decl and pl. variant
local basic_decl, detected_or_explicit_plural = rmatch(decl, "^(.*)(%-[^mf]+)$")
if basic_decl == "ь" then
basic_decl = "ь-m"
end
basic_decl = basic_decl or decl
-- 3: Any user-requested plural variant must agree with explicit or
-- autodetected variant.
if want_ya_plural and detected_or_explicit_plural and want_ya_plural ~= detected_or_explicit_plural then
error("Plural variant " .. want_ya_plural .. " requested but plural variant " .. detected_or_explicit_plural .. " detected from plural stem")
end
-- 4: Handle special case (1). Derive the full declension, make sure its
-- plural variant matches any other available plural variants, and
-- return the declension.
if args.want_sc1 then
local sc1_decl = special_case_1_to_plural_variant[basic_decl] or
error("Special case (1) not compatible with declension " .. basic_decl)
local sc1_plural = rsub(sc1_decl, "^.*%-", "-")
local other_plural = want_ya_plural or detected_or_explicit_plural
if other_plural and sc1_plural ~= other_plural then
error("Plural variant " .. other_plural .. " specified or detected, but special case (1) calls for plural variant " .. sc1_plural)
end
return stem, tr, sc1_decl, gender, was_accented, was_plural, was_autodetected
end
-- 5: Handle user-requested plural variant without explicit or detected
-- one. (If an explicit or detected one exists, we've already checked
-- that it agrees with the user-requested one, and so we already have
-- our full declension.)
if want_ya_plural and not detected_or_explicit_plural then
local variant_decl
if plural_variant_detection_map[decl] then
variant_decl = plural_variant_detection_map[decl][want_ya_plural]
end
if variant_decl then
return stem, tr, variant_decl, gender, was_accented, was_plural, was_autodetected
else
return stem, tr, decl .. (args.old and "/ъ-ья" or "/-ья"), gender, was_accented, was_plural, was_autodetected
end
end
-- 6: Just return the full declension, which will include any available
-- plural variant in it.
return stem, tr, decl, gender, was_accented, was_plural, was_autodetected
end
-- Convert soft adjectival declensions into hard ones following certain
-- stem-final consonants. FIXME: We call this in two places, once
-- to handle auto-detection and once to handle explicit declensions; but
-- in the former case we end up calling it twice.
local function determine_adj_stem_variant(decl, stem)
local iend = rmatch(decl, "^%+[іи]([йея]?)$")
-- Convert ій/ий to ый after velar or sibilant. This is important for
-- velars; doesn't really matter one way or the other for sibilants as
-- the sibilant rules will convert both sets of endings to the same
-- thing (whereas there will be a difference with о vs. е for velars).
if iend and rfind(stem, "[" .. com.velar .. com.sib .. "]$") then
decl = "+ы" .. iend
-- The following is necessary for -ц, unclear if makes sense for
-- sibilants. (Would be necessary -- I think -- if we were
-- inferring short adjective forms, but we're not.)
elseif decl == "+ее" and rfind(stem, "[" .. com.sib_c .. "]$") then
decl = "+ое"
end
return decl
end
-- Attempt to determine the actual adjective declension based on a
-- combination of the declension the user specified and what can be detected
-- from the stem. DECL is the value the user passed for the declension field,
-- after extraneous annotations have been removed (although none are probably
-- relevant here). What's left is one of the following, which always begins
-- with +:
--
-- 1. +, meaning to autodetect the declension from the stem
-- 2. +ь, same as + but selects +ьий instead of +ий if lemma ends in -ий
-- 3. +short, +mixed or +proper, with the declension partly specified but
-- the particular gender/number-specific short/mixed variant to be
-- autodetected
-- 4. A gender (+m, +f or +n), used only for detecting the singular of
-- plural-form lemmas (this is primarily used in conjunction with template
-- ru-noun+, to explicitly specify the gender; for the actual declension,
-- it doesn't much matter what singular gender we pick since we're a
-- plural only)
-- 5. A gender plus short/mixed/proper/ь (e.g. +f-mixed), again with the gender
-- used only for detecting the singular of plural-form short/mixed lemmas
-- 6. An actual declension, possibly including a slash declension
-- (WARNING: Unclear if slash declensions will work, especially those
-- that are adjective/noun combinations)
--
-- Returns the same seven args as for determine_decl(). The returned
-- declension will always begin with +.
detect_adj_type = function(lemma, tr, decl, old)
local was_autodetected
local base, ending
local basedecl, g = rmatch(decl, "^(%+)([mfn])$")
if not basedecl then
g, basedecl = rmatch(decl, "^%+([mfn])%-([a-zь]+)$")
if basedecl then
basedecl = "+" .. basedecl
end
end
decl = basedecl or decl
if decl == "+" or decl == "+ь" then
local loc = rfind(lemma, "[аеиіоыя][" .. AC .. GR .. "]?[йея]$") or rfind(lemma, "ь[йея]$")
if loc then
base, ending = usub(lemma, 1, loc - 1), usub(lemma, loc)
end
if ending == "ий" and decl == "+ь" then
decl = "+ьий"
elseif ending == "ій" and decl == "+ь" then
decl = "+ьій"
elseif ending then
decl = "+" .. ending
else
local loc, shortmixed = rfind(lemma, "[аоы][" .. AC .. GR .. "]?$") or rfind(lemma, "ъ?$")
if loc then
base, ending = usub(lemma, 1, loc - 1), usub(lemma, loc)
shortmixed = rfind(base, "^[" .. com.uppercase .. "].*[иы]" .. AC .. "н$") and "stressed-proper" or -- accented
rfind(base, "^[" .. com.uppercase .. "].*[иы]н$") and "proper" or --not accented
rlfind(base, "[ёео][" .. AC .. GR .. "]?в$") and "short" or
rlfind(base, "[ыи]" .. AC .. "н$") and "stressed-short" or -- accented
rlfind(base, "[ыи]н$") and "mixed" --not accented
end
if not shortmixed then
error("Cannot determine stem type of adjective: " .. lemma)
end
decl = "+" .. ending .. "-" .. shortmixed
end
was_autodetected = true
elseif m_table.contains({"+short", "+mixed", "+proper"}, decl) then
base, ending = rmatch(lemma, "^(.-)([оаыъ]?[" .. AC .. GR .. "]?)$")
assert(base)
local shortmixed = usub(decl, 2)
if rlfind(base, "[ыи]" .. AC .. "н$") then -- accented
if shortmixed == "short" then shortmixed = "stressed-short"
elseif shortmixed == "proper" then shortmixed = "stressed-proper"
end
end
decl = "+" .. ending .. "-" .. shortmixed
was_autodetected = true
else
base = lemma
end
if ending and ending ~= "" then
tr = com.strip_tr_ending(tr, ending)
end
-- Remove any accents from the declension, but not their presence.
-- We will convert was_accented into stress pattern b, and convert that
-- back to an accented version in determine_stress_variant(). This way
-- we end up with the stressed version whether the user placed an accent
-- in the ending or decl or specified stress pattern b.
-- FIXME, might not work in the presence of slash declensions
local was_accented = com.is_stressed(decl)
decl = com.remove_accents(decl)
decl = map_decl(decl, function(decl)
return determine_adj_stem_variant(decl, base)
end)
local singdecl
if decl == "+ые" then
singdecl = (g == "m" or not g) and (was_accented and "+ой" or "+ый") or not old and g == "f" and "+ая" or not old and g == "n" and "+ое"
elseif decl == "+ыя" and old then
singdecl = (g == "f" or not g) and "+ая" or g == "n" and "+ое"
elseif decl == "+ие" and not old then
singdecl = (g == "m" or not g) and "+ий" or g == "f" and "+яя" or g == "n" and "+ее"
elseif decl == "+іе" and old and (g == "m" or not g) then
singdecl = "+ій"
elseif decl == "+ія" and old then
singdecl = (g == "f" or not g) and "+яя" or g == "n" and "+ее"
elseif decl == "+ьи" then
singdecl = (g == "m" or not g) and (old and "+ьій" or "+ьий") or g == "f" and "+ья" or g == "n" and "+ье"
elseif rfind(decl, "^%+ы%-") then -- decl +ы-mixed or similar
local beg = (g == "m" or not g) and (old and "ъ" or "") or g == "f" and "а" or g == "n" and "о"
singdecl = beg and "+" .. beg .. usub(decl, 3)
end
if singdecl then
was_plural = true
decl = singdecl
end
return base, tr, canonicalize_decl(decl, old), g, was_accented, was_plural, was_autodetected
end
-- If stress pattern omitted, detect it based on whether ending is stressed
-- or the decl class or stem accent calls for inherent stress, defaulting to
-- pattern a. This is run after alias resolution and accent removal of DECL;
-- WAS_ACCENTED indicates whether the ending was originally stressed.
-- FIXME: This is run before splitting slash patterns but should be run after.
detect_stress_pattern = function(stem, decl, decl_cats, reducible,
was_plural, was_accented)
-- ёнок and ёночек always bear stress
if rfind(decl, "ёнокъ?") or rfind(decl, "ёночекъ?") then
return "b"
-- stressed suffix и́н; missing in plural and true endings don't bear stress
-- (except for exceptional господи́н)
elseif rfind(decl, "инъ?") and was_accented then
return "d"
-- Adjectival -ой always bears the stress
elseif rfind(decl, "%+ой") then
return "b"
-- Adjectival stressed-short, stressed-proper bears the stress
elseif rfind(decl, "^%+.*%-stressed") then
return "b"
-- Pattern b if ending was accented by user
elseif was_accented then
return "b"
-- Nonsyllabic stem means pattern b
elseif com.is_nonsyllabic(stem) then
return "b"
-- Accent on reducible vowel in masc nom sg (not plural) means pattern b.
-- Think about whether we want to enable this.
-- elseif reducible and not was_plural then
-- -- FIXME hack. Eliminate plural part of slash declension.
-- decl = rsub(decl, "/.*", "")
-- if decl_cats[decl] and decl_cats[decl].g == "m" then
-- if com.is_ending_stressed(stem) or com.is_monosyllabic(stem) then
-- return "b"
-- end
-- end
end
return "a"
end
-- In certain special cases, depending on the declension, we override the
-- user-specified stress pattern and convert it to something else.
-- NOTE: This function is run after alias resolution and accent removal.
-- FIXME: It's also run before splitting slash patterns but should be run after.
override_stress_pattern = function(decl, stress)
-- ёнок and ёночек always bear stress; if user specified a,
-- convert to b. Don't do this with slash patterns (see FIXME above).
if stress == "a" and (rfind(decl, "^ёнокъ?$") or rfind(decl, "^ёночекъ?$")) then
return "b"
end
return stress
end
-- Canonicalize an adjectival declension to either the stressed or unstressed
-- variant depending on the stress. Ultimately this is what ensures that
-- the user's stress mark on an adjectival ending is respected.
determine_stress_variant = function(decl, stress)
if stress == "b" then
if decl == "+ая" then
return "+а́я"
elseif decl == "+ое" then
return "+о́е"
else
-- Convert +...-short to +...-stressed-short and same for -proper
local b, e = rmatch(decl, "^%+(.*)%-(short)$")
if not b then
b, e = rmatch(decl, "^%+(.*)%-(proper)$")
end
if b and not rfind(b, "%-stressed") then
return "+" .. b .. "-stressed-" .. e
end
end
end
return decl
end
-- Canonicalize a declension based on the final stem consonant, in
-- particular converting soft declensions to hard ones after velars and/or
-- sibilants. FIXME: We also do this canonicalization earlier on during
-- auto-detection (determine_adj_stem_variant() is called by
-- detect_adj_type(), and code in detect_lemma_type() does the equivalent
-- of the first clause below). Doing it here ensures that explicitly
-- specified declensions get handled as well, but it would be nice to not
-- do the same thing twice in the auto-detection case.
determine_stem_variant = function(decl, stem)
if decl == "е" and rfind(stem, "[" .. com.sib_c .. "]$") then
return "о"
end
return determine_adj_stem_variant(decl, stem)
end
is_reducible = function(decl_cat)
if decl_cat.suffix or decl_cat.cant_reduce or decl_cat.adj then
return false
elseif decl_cat.decl == "3rd" and decl_cat.g == "f" or decl_cat.g == "m" then
return true
else
return false
end
end
-- Reduce nom sg to stem by eliminating the "epenthetic" vowel. Applies to
-- masculine 2nd-declension hard and soft, and 3rd-declension feminine in
-- -ь. STEM and DECL are after determine_decl(), before converting
-- outward-facing declensions to inward ones.
function export.reduce_nom_sg_stem(stem, tr, decl, soft_n, can_err)
local full_stem = stem .. (decl == "й" and decl or "")
local full_tr = tr and tr .. (decl == "й" and "j" or "")
local ret, rettr = com.reduce_stem(full_stem, full_tr, soft_n)
if not ret and can_err then
error("Unable to reduce stem " .. stem)
end
return ret, rettr
end
is_dereducible = function(decl_cat)
if decl_cat.suffix or decl_cat.cant_reduce or decl_cat.adj then
return false
elseif decl_cat.decl == "1st" or decl_cat.decl == "2nd" and decl_cat.g == "n" then
return true
else
return false
end
end
-- Add a possible suffix to the bare stem, according to the declension and
-- value of OLD. This may be -ь, -ъ, -й or nothing. We need to do this here
-- because we don't actually attach such a suffix in attach_unstressed() due
-- to situations where we don't want the suffix added, e.g. dereducible nouns
-- in -ня.
add_bare_suffix = function(bare, baretr, old, sgdc, dereduced)
if old and sgdc.hard == "hard" then
-- Final -ъ isn't transliterated
return bare .. "ъ", baretr
elseif sgdc.hard == "soft" or sgdc.hard == "palatal" then
-- This next clause corresponds to a special case in Vitalik's module.
-- It says that nouns in -ня (accent class a) have gen pl without
-- trailing -ь. It appears to apply to most nouns in -ня (possibly
-- all in -льня), but ку́хня (gen pl ку́хонь) and дерéвня (gen pl
-- дереве́нь) is an exception. (Vitalik's module has an extra
-- condition here 'stress == "a"' that would exclude дере́вня but I
-- don't think this condition is in Zaliznyak, as he indicates
-- дере́вня as having an exceptional genitive plural.)
if dereduced and rfind(bare, "[нН]$") and sgdc.decl == "1st" then
-- FIXME: What happens in this case old-style? I assume that
-- -ъ is added, but this is a guess.
-- Final -ъ isn't transliterated
return bare .. (old and "ъ" or ""), baretr
elseif rfind(bare, "[" .. com.vowel .. "][" .. AC .. GR .. "]?$") then
return bare .. "й", baretr and (baretr .. "j")
else
return bare .. "ь", baretr and (baretr .. "ʹ")
end
else
return bare, baretr
end
end
-- Dereduce stem to the form found in the gen pl (and maybe nom sg) by
-- inserting an epenthetic vowel. Applies to 1st declension and 2nd
-- declension neuter, and to 2nd declension masculine when the stem was
-- specified as a plural form (in which case we're deriving the nom sg,
-- and also the gen pl in the alt-gen-pl scenario). STEM and DECL are
-- after determine_decl(), before converting outward-facing declensions
-- to inward ones. STRESS is the stess pattern.
function export.dereduce_nom_sg_stem(stem, tr, sgdc, stress, old, can_err)
local epenthetic_stress = ending_stressed_gen_pl_patterns[stress]
local ret, rettr = com.dereduce_stem(stem, tr, epenthetic_stress)
if not ret then
if can_err then
error("Unable to dereduce stem " .. stem)
else
return nil, nil
end
end
return add_bare_suffix(ret, rettr, old, sgdc, true)
end
--------------------------------------------------------------------------
-- Second-declension masculine --
--------------------------------------------------------------------------
----------------- Masculine hard -------------------
-- Hard-masculine declension, ending in a hard consonant
-- (ending in -ъ, old-style).
declensions_old["ъ"] = {
["nom_sg"] = "ъ",
["gen_sg"] = "а́",
["dat_sg"] = "у́",
["acc_sg"] = nil,
["ins_sg"] = "о́мъ",
["pre_sg"] = "ѣ́",
["nom_pl"] = "ы́",
["gen_pl"] = function(stem, stress)
return nom.sibilant_suffixes[ulower(usub(stem, -1))] and "е́й" or "о́въ"
end,
["alt_gen_pl"] = "ъ",
["dat_pl"] = "а́мъ",
["acc_pl"] = nil,
["ins_pl"] = "а́ми",
["pre_pl"] = "а́хъ",
}
declensions_old_cat["ъ"] = { decl="2nd", hard="hard", g="m" }
-- Normal mapping of old ъ would be "" (blank), but we set up "#" as an alias
-- so we have a way of referring to it without defaulting if need be and
-- distinct from auto-detection (e.g. in the second stem of a word, or to
-- override autodetection of -ёнок or -ин -- the latter is necessary in the
-- case of семьянин).
declensions_aliases["#"] = ""
----------------- Masculine hard, irregular plural -------------------
-- Hard-masculine declension, ending in a hard consonant
-- (ending in -ъ, old-style), with irreg nom pl -а.
declensions_old["ъ-а"] = mw.clone(declensions_old["ъ"])
declensions_old["ъ-а"]["nom_pl"] = "а́"
declensions_old_cat["ъ-а"] = { decl="2nd", hard="hard", g="m", alt_nom_pl=true }
declensions_aliases["#-a"] = "-a"
-- Hard-masculine declension, ending in a hard consonant
-- (ending in -ъ, old-style), with irreg soft pl -ья.
-- Differs from the normal declension throughout the plural.
declensions_old["ъ-ья"] = {
["nom_sg"] = "ъ",
["gen_sg"] = "а́",
["dat_sg"] = "у́",
["acc_sg"] = nil,
["ins_sg"] = "о́мъ",
["pre_sg"] = "ѣ́",
["nom_pl"] = "ья́",
["gen_pl"] = "ьёвъ",
["alt_gen_pl"] = "е́й",
["dat_pl"] = "ья́мъ",
["acc_pl"] = nil,
["ins_pl"] = "ья́ми",
["pre_pl"] = "ья́хъ",
}
declensions_old_cat["ъ-ья"] = { decl="2nd", hard="hard", g="m", irregpl=true }
declensions_aliases["#-ья"] = "-ья"
----------------- Masculine hard, suffixed, irregular plural -------------------
declensions_old["инъ"] = {
["nom_sg"] = "и́нъ",
["gen_sg"] = "и́на",
["dat_sg"] = "и́ну",
["acc_sg"] = nil,
["ins_sg"] = "и́номъ",
["pre_sg"] = "и́нѣ",
["nom_pl"] = "е́",
["gen_pl"] = "ъ",
["dat_pl"] = "а́мъ",
["acc_pl"] = nil,
["ins_pl"] = "а́ми",
["pre_pl"] = "а́хъ",
}
declensions_old_cat["инъ"] = { decl="2nd", hard="hard", g="m", suffix=true }
declensions_old["ёнокъ"] = {
["nom_sg"] = "ёнокъ",
["gen_sg"] = "ёнка",
["dat_sg"] = "ёнку",
["acc_sg"] = nil,
["ins_sg"] = "ёнкомъ",
["pre_sg"] = "ёнкѣ",
["nom_pl"] = "я́та",
["gen_pl"] = "я́тъ",
["dat_pl"] = "я́тамъ",
["acc_pl"] = nil,
["ins_pl"] = "я́тами",
["pre_pl"] = "я́тахъ",
}
declensions_old_cat["ёнокъ"] = { decl="2nd", hard="hard", g="m", suffix=true }
declensions_old_aliases["онокъ"] = "ёнокъ"
declensions_old_aliases["енокъ"] = "ёнокъ"
declensions_old["ёночекъ"] = {
["nom_sg"] = "ёночекъ",
["gen_sg"] = "ёночка",
["dat_sg"] = "ёночку",
["acc_sg"] = nil,
["ins_sg"] = "ёночкомъ",
["pre_sg"] = "ёночкѣ",
["nom_pl"] = "я́тки",
["gen_pl"] = "я́токъ",
["dat_pl"] = "я́ткамъ",
["acc_pl"] = nil,
["ins_pl"] = "я́тками",
["pre_pl"] = "я́ткахъ",
}
declensions_old_cat["ёночекъ"] = { decl="2nd", hard="hard", g="m", suffix=true }
declensions_old_aliases["оночекъ"] = "ёночекъ"
declensions_old_aliases["еночекъ"] = "ёночекъ"
----------------- Masculine soft -------------------
-- Normal soft-masculine declension in -ь
declensions_old["ь-m"] = {
["nom_sg"] = "ь",
["gen_sg"] = "я́",
["dat_sg"] = "ю́",
["acc_sg"] = nil,
["ins_sg"] = "ёмъ",
["pre_sg"] = "ѣ́",
["nom_pl"] = "и́",
["gen_pl"] = "е́й",
["alt_gen_pl"] = "ь",
["dat_pl"] = "я́мъ",
["acc_pl"] = nil,
["ins_pl"] = "я́ми",
["pre_pl"] = "я́хъ",
}
declensions_old_cat["ь-m"] = { decl="2nd", hard="soft", g="m" }
-- Soft-masculine declension in -ь with irreg nom pl -я
declensions_old["ь-я"] = mw.clone(declensions_old["ь-m"])
declensions_old["ь-я"]["nom_pl"] = "я́"
declensions_old_cat["ь-я"] = { decl="2nd", hard="soft", g="m", alt_nom_pl=true }
----------------- Masculine palatal -------------------
-- Masculine declension in palatal -й
declensions_old["й"] = {
["nom_sg"] = "й",
["gen_sg"] = "я́",
["dat_sg"] = "ю́",
["acc_sg"] = nil,
["ins_sg"] = "ёмъ",
["pre_sg"] = function(stem, stress)
return rlfind(stem, "[іи][" .. AC .. GR .. "]?$") and not ending_stressed_pre_sg_patterns[stress] and "и" or "ѣ́"
end,
["nom_pl"] = "и́",
["gen_pl"] = "ёвъ",
["alt_gen_pl"] = "й",
["dat_pl"] = "я́мъ",
["acc_pl"] = nil,
["ins_pl"] = "я́ми",
["pre_pl"] = "я́хъ",
}
declensions_old_cat["й"] = { decl="2nd", hard="palatal", g="m" }
declensions_old["й-я"] = mw.clone(declensions_old["й"])
declensions_old["й-я"]["nom_pl"] = "я́"
declensions_old_cat["й-я"] = { decl="2nd", hard="palatal", g="m", alt_nom_pl=true }
--------------------------------------------------------------------------
-- First-declension feminine --
--------------------------------------------------------------------------
----------------- Feminine hard -------------------
-- Hard-feminine declension in -а
declensions_old["а"] = {
["nom_sg"] = "а́",
["gen_sg"] = "ы́",
["dat_sg"] = "ѣ́",
["acc_sg"] = "у́",
["ins_sg"] = {"о́й<insa>", "о́ю<insb>"}, -- see concat_word_forms_1()
["pre_sg"] = "ѣ́",
["nom_pl"] = "ы́",
["gen_pl"] = function(stem, stress)
return nom.sibilant_suffixes[ulower(usub(stem, -1))] and ending_stressed_gen_pl_patterns[stress] and "е́й" or "ъ"
end,
["alt_gen_pl"] = "е́й",
["dat_pl"] = "а́мъ",
["acc_pl"] = nil,
["ins_pl"] = "а́ми",
["pre_pl"] = "а́хъ",
}
declensions_old_cat["а"] = { decl="1st", hard="hard", g="f" }
----------------- Feminine soft -------------------
-- Soft-feminine declension in -я
declensions_old["я"] = {
["nom_sg"] = "я́",
["gen_sg"] = "и́",
["dat_sg"] = function(stem, stress)
return rlfind(stem, "[іи][" .. AC .. GR .. "]?$") and not ending_stressed_dat_sg_patterns[stress] and "и" or "ѣ́"
end,
["acc_sg"] = "ю́",
["ins_sg"] = {"ёй<insa>", "ёю<insb>"}, -- see concat_word_forms_1()
["pre_sg"] = function(stem, stress)
return rlfind(stem, "[іи][" .. AC .. GR .. "]?$") and not ending_stressed_pre_sg_patterns[stress] and "и" or "ѣ́"
end,
["nom_pl"] = "и́",
["gen_pl"] = function(stem, stress)
return ending_stressed_gen_pl_patterns[stress] and not rlfind(stem, "[" .. com.vowel .. "]́?$") and "е́й" or "й"
end,
["alt_gen_pl"] = "е́й",
["dat_pl"] = "я́мъ",
["acc_pl"] = nil,
["ins_pl"] = "я́ми",
["pre_pl"] = "я́хъ",
}
declensions_old_cat["я"] = { decl="1st", hard="soft", g="f" }
-- Soft-feminine declension in -ья.
-- Almost like ь + -я endings except for genitive plural.
declensions_old["ья"] = {
["nom_sg"] = "ья́",
["gen_sg"] = "ьи́",
["dat_sg"] = "ьѣ́",
["acc_sg"] = "ью́",
["ins_sg"] = {"ьёй<insa>", "ьёю<insb>"}, -- see concat_word_forms_1()
["pre_sg"] = "ьѣ́",
["nom_pl"] = "ьи́",
["gen_pl"] = function(stem, stress)
-- circumflex accent is a signal that forces stress, particularly
-- in accent pattern d/d'.
return (ending_stressed_gen_pl_patterns[stress] or stress == "d" or stress == "d'") and "е̂й" or "ий"
end,
["dat_pl"] = "ья́мъ",
["acc_pl"] = nil,
["ins_pl"] = "ья́ми",
["pre_pl"] = "ья́хъ",
}
declensions_old_cat["ья"] = {
decl="1st", hard="soft", g="f",
stem_suffix="ь", gensg=true,
ignore_reduce=true -- already has dereduced gen pl
}
--------------------------------------------------------------------------
-- Second-declension neuter --
--------------------------------------------------------------------------
----------------- Neuter hard -------------------
-- Normal hard-neuter declension in -о
declensions_old["о"] = {
["nom_sg"] = "о́",
["gen_sg"] = "а́",
["dat_sg"] = "у́",
["acc_sg"] = function(stem, stress, args)
return not (args.explicit_gender == "m" and args.thisa == "a") and "о́" or nil
end,
["ins_sg"] = "о́мъ",
["pre_sg"] = "ѣ́",
["nom_pl"] = "а́",
["gen_pl"] = function(stem, stress)
return nom.sibilant_suffixes[ulower(usub(stem, -1))] and ending_stressed_gen_pl_patterns[stress] and "е́й" or "ъ"
end,
["alt_gen_pl"] = "о́въ",
["dat_pl"] = "а́мъ",
["acc_pl"] = nil,
["ins_pl"] = "а́ми",
["pre_pl"] = "а́хъ",
}
declensions_old_cat["о"] = { decl="2nd", hard="hard", g="n" }
-- Hard-neuter declension in -о with irreg nom pl -и
declensions_old["о-и"] = mw.clone(declensions_old["о"])
declensions_old["о-и"]["nom_pl"] = "ы́"
declensions_old_cat["о-и"] = { decl="2nd", hard="hard", g="n", alt_nom_pl=true }
declensions_old_aliases["о-ы"] = "о-и"
-- Masculine-gender neuter-form declension in -(ишк)о with irreg nom pl -и,
-- with colloquial feminine endings in some of the singular cases
-- (§5 p. 74 of Zaliznyak)
declensions_old["(ишк)о-и"] = mw.clone(declensions_old["о-и"])
declensions_old["(ишк)о-и"]["gen_sg"] = {"а́", "ы́1"}
declensions_old["(ишк)о-и"]["dat_sg"] = {"у́", "ѣ́1"}
declensions_old["(ишк)о-и"]["ins_sg"] = {"о́мъ", "о́й1"}
declensions_old_cat["(ишк)о-и"] = { decl="2nd", hard="hard", g="n", colloqfem=true, alt_nom_pl=true }
internal_notes_table_old["(ишк)о-и"] = "<sup>1</sup> Colloquial."
-- Masculine-gender animate neuter-form declension in -(ищ)е with irreg
-- nom pl -и, with colloquial feminine endings in some of the singular cases
-- (§4 p. 74 of Zaliznyak)
declensions_old["(ищ)е-и"] = mw.clone(declensions_old["о-и"])
declensions_old["(ищ)е-и"]["acc_sg"] = {"а́", "у́1"}
declensions_old["(ищ)е-и"]["gen_sg"] = {"а́", "ы́2"}
declensions_old["(ищ)е-и"]["dat_sg"] = {"у́", "ѣ́2"}
declensions_old["(ищ)е-и"]["ins_sg"] = {"о́мъ", "о́й2"}
declensions_old_cat["(ищ)е-и"] = { decl="2nd", hard="hard", g="n", colloqfem=true, alt_nom_pl=true }
internal_notes_table_old["(ищ)е-и"] = "<sup>1</sup> Colloquial.<br /><sup>2</sup> Less common, more colloquial."
----------------- Neuter soft -------------------
-- Soft-neuter declension in -е (stressed -ё)
declensions_old["е"] = {
["nom_sg"] = "ё",
["gen_sg"] = "я́",
["dat_sg"] = "ю́",
["acc_sg"] = function(stem, stress, args)
return not (args.explicit_gender == "m" and args.thisa == "a") and "ё" or nil
end,
["ins_sg"] = "ёмъ",
["pre_sg"] = function(stem, stress)
return rlfind(stem, "[іи][" .. AC .. GR .. "]?$") and not ending_stressed_pre_sg_patterns[stress] and "и" or "ѣ́"
end,
["nom_pl"] = "я́",
["gen_pl"] = function(stem, stress)
return ending_stressed_gen_pl_patterns[stress] and not rlfind(stem, "[" .. com.vowel .. "][" .. AC .. GR .. "]?$") and "е́й" or "й"
end,
["alt_gen_pl"] = "ёвъ",
["dat_pl"] = "я́мъ",
["acc_pl"] = nil,
["ins_pl"] = "я́ми",
["pre_pl"] = "я́хъ",
}
declensions_old_cat["е"] = {
singular = function(suffix)
if suffix == "ё" then
return "ending in -ё"
else
return {}
end
end,
decl="2nd", hard="soft", g="n", gensg=true
}
-- User-facing declension type "ё" = "е"
declensions_old_aliases["ё"] = "е"
-- Rare soft-neuter declension in stressed -е́ (e.g. муде́, бытие́)
declensions_old["е́"] = {
["nom_sg"] = "е́",
["gen_sg"] = "я́",
["dat_sg"] = "ю́",
["acc_sg"] = function(stem, stress, args)
return not (args.explicit_gender == "m" and args.thisa == "a") and "е́" or nil
end,
["ins_sg"] = "е́мъ",
["pre_sg"] = function(stem, stress)
-- FIXME!!! Are we sure about this condition? This is what was
-- found in the old template, but the related -е declension has
-- -ие prep sg ending -(и)и only when *not* stressed.
return rlfind(stem, "[іи][" .. AC .. GR .. "]?$") and "и́" or "ѣ́"
end,
["nom_pl"] = "я́",
["gen_pl"] = function(stem, stress)
return rlfind(stem, "[" .. com.vowel .. "][" .. AC .. GR .. "]?$") and "й" or "е́й"
end,
["alt_gen_pl"] = "ёвъ",
["dat_pl"] = "я́мъ",
["acc_pl"] = nil,
["ins_pl"] = "я́ми",
["pre_pl"] = "я́хъ",
}
declensions_old_cat["е́"] = {
singular = "ending in stressed -е",
decl="2nd", hard="soft", g="n", gensg=true
}
-- Soft-neuter declension in unstressed -ье (stressed -ьё).
declensions_old["ье"] = {
["nom_sg"] = "ьё",
["gen_sg"] = "ья́",
["dat_sg"] = "ью́",
["acc_sg"] = function(stem, stress, args)
return not (args.explicit_gender == "m" and args.thisa == "a") and "ьё" or nil
end,
["ins_sg"] = "ьёмъ",
["pre_sg"] = "ьѣ́",
["nom_pl"] = "ья́",
["gen_pl"] = function(stem, stress)
return ending_stressed_gen_pl_patterns[stress] and "е́й" or "ий"
end,
["alt_gen_pl"] = "ьёвъ",
["dat_pl"] = "ья́мъ",
["acc_pl"] = nil,
["ins_pl"] = "ья́ми",
["pre_pl"] = "ья́хъ",
}
declensions_old_cat["ье"] = {
decl="2nd", hard="soft", g="n",
stem_suffix="ь", gensg=true,
ignore_reduce=true -- already has dereduced gen pl
}
declensions_old_aliases["ьё"] = "ье"
--------------------------------------------------------------------------
-- Third declension --
--------------------------------------------------------------------------
declensions_old["ь-f"] = {
["nom_sg"] = "ь",
["gen_sg"] = "и́",
["dat_sg"] = "и́",
["acc_sg"] = "ь",
["ins_sg"] = "ью́",
["pre_sg"] = "и́",
["nom_pl"] = "и́",
["gen_pl"] = "е́й",
["dat_pl"] = "я́мъ",
["acc_pl"] = nil,
["ins_pl"] = "я́ми",
["pre_pl"] = "я́хъ",
}
declensions_old_cat["ь-f"] = { decl="3rd", hard="soft", g="f" }
declensions_old["мя"] = {
["nom_sg"] = "мя",
["gen_sg"] = "мени",
["dat_sg"] = "мени",
["acc_sg"] = nil,
["ins_sg"] = "менемъ",
["pre_sg"] = "мени",
["nom_pl"] = "мена́",
["gen_pl"] = "мёнъ",
["dat_pl"] = "мена́мъ",
["acc_pl"] = nil,
["ins_pl"] = "мена́ми",
["pre_pl"] = "мена́хъ",
}
declensions_old_cat["мя"] = { decl="3rd", hard="soft", g="n", cant_reduce=true }
--------------------------------------------------------------------------
-- Indeclinable --
--------------------------------------------------------------------------
-- Indeclinable declension; no endings.
declensions_old["$"] = {
["nom_sg"] = "",
["gen_sg"] = "",
["dat_sg"] = "",
["acc_sg"] = nil,
["ins_sg"] = "",
["pre_sg"] = "",
["nom_pl"] = "",
["gen_pl"] = "",
["dat_pl"] = "",
["acc_pl"] = nil,
["ins_pl"] = "",
["pre_pl"] = "",
}
declensions_old_cat["$"] = { decl="indeclinable", hard="none", g="none" }
--------------------------------------------------------------------------
-- Adjectival --
--------------------------------------------------------------------------
-- This needs to be up here because it is called just below.
local function old_to_new(v)
v = rsub(v, "ъ$", "")
v = rsub(v, "^ъ", "")
v = rsub(v, "(%A)ъ", "%1")
v = rsub(v, "ъ(%A)", "%1")
v = rsub(v, "і", "и")
v = rsub(v, "ѣ", "е")
return v
end
-- Meaning of entry is:
-- 1. The declension name in module ru-adjective
-- 2. The masculine declension name in this module
-- 3. The neuter declension name in this module
-- 4. The feminine declension name in this module
-- 5. The value of hard= for the declensions_cat entry
-- 6. The value of decl= for the declensions_cat entry
-- 7. The value of possadj= for the declensions_cat entry (true if possessive
-- or similar type of adjective)
local adj_decl_map = {
{"ый", "ый", "ое", "ая", "hard", "long", false},
{"ій", "ій", "ее", "яя", "soft", "long", false},
{"ой", "ой", "о́е", "а́я", "hard", "long", false},
{"ьій", "ьій", "ье", "ья", "palatal", "long", true},
{"short", "ъ-short", "о-short", "а-short", "hard", "short", true},
{"mixed", "ъ-mixed", "о-mixed", "а-mixed", "hard", "mixed", true},
{"proper", "ъ-proper", "о-proper", "а-proper", "hard", "proper", true},
{"stressed-short", "ъ-stressed-short", "о-stressed-short", "а-stressed-short", "hard", "short", true},
{"stressed-proper", "ъ-stressed-proper", "о-stressed-proper", "а-stressed-proper", "hard", "proper", true},
}
local function get_adjectival_decl(adjtype, gender, old)
local decl, intnotes = m_ru_adj.get_nominal_decl(adjtype, gender, old)
-- hack fem ins_sg to insert <insa>, <insb>; see concat_word_forms_1()
if gender == "f" and type(decl["ins_sg"]) == "table" and #decl["ins_sg"] == 2 then
decl["ins_sg"][1] = decl["ins_sg"][1] .. "<insa>"
decl["ins_sg"][2] = decl["ins_sg"][2] .. "<insb>"
end
return decl, intnotes
end
for _, declspec in ipairs(adj_decl_map) do
local oadjdecl = declspec[1]
local nadjdecl = old_to_new(oadjdecl)
local odecl_by_gender =
{m="+" .. declspec[2], n="+" .. declspec[3], f="+" .. declspec[4]}
local hard = declspec[5]
local decltype = declspec[6]
local possadj = declspec[7]
for _, g in ipairs({"m", "n", "f"}) do
local odecl = odecl_by_gender[g]
local ndecl = old_to_new(odecl)
declensions_old[odecl], internal_notes_table_old[odecl] =
get_adjectival_decl(oadjdecl, g, true)
declensions[ndecl], internal_notes_table[ndecl] =
get_adjectival_decl(nadjdecl, g, false)
declensions_old_cat[odecl] = {
decl=decltype, hard=hard, g=g, adj=true, possadj=possadj }
declensions_cat[ndecl] = {
decl=decltype, hard=hard, g=g, adj=true, possadj=possadj }
end
end
-- Set up some aliases.
declensions_old_aliases["+о́-short"] = "+о-stressed-short"
declensions_old_aliases["+а́-short"] = "+а-stressed-short"
declensions_old_aliases["+о́-proper"] = "+о-stressed-proper"
declensions_old_aliases["+а́-proper"] = "+а-stressed-proper"
declensions_aliases["+#-short"] = "+-short"
declensions_aliases["+#-mixed"] = "+-mixed"
declensions_aliases["+#-proper"] = "+-proper"
declensions_aliases["+#-stressed-short"] = "+-stressed-short"
declensions_aliases["+#-stressed-proper"] = "+-stressed-proper"
--------------------------------------------------------------------------
-- Populate new from old --
--------------------------------------------------------------------------
-- Function to convert an entry in an old declensions table to new.
local function old_decl_entry_to_new(v)
if not v then
return nil
elseif type(v) == "table" then
local new_entry = {}
for _, i in ipairs(v) do
table.insert(new_entry, old_decl_entry_to_new(i))
end
return new_entry
elseif type(v) == "function" then
return function(stem, suffix, args)
return old_decl_entry_to_new(v(stem, suffix, args))
end
else
return old_to_new(v)
end
end
-- Function to convert an old declensions table to new.
local function old_decl_to_new(odecl)
local ndecl = {}
for k, v in pairs(odecl) do
ndecl[k] = old_decl_entry_to_new(v)
end
return ndecl
end
-- Function to convert an entry in an old declensions_cat table to new.
local function old_decl_cat_entry_to_new(odecl_cat_entry)
if not odecl_cat_entry then
return nil
elseif type(odecl_cat_entry) == "function" then
return function(suffix)
return old_decl_cat_entry_to_new(odecl_cat_entry(suffix))
end
elseif type(odecl_cat_entry) == "table" then
local ndecl_cat_entry = {}
for k, v in pairs(odecl_cat_entry) do
ndecl_cat_entry[k] = old_decl_cat_entry_to_new(v)
end
return ndecl_cat_entry
elseif type(odecl_cat_entry) == "boolean" then
return odecl_cat_entry
else
assert(type(odecl_cat_entry) == "string")
return old_to_new(odecl_cat_entry)
end
end
-- Function to convert an old declensions_cat table to new.
local function old_decl_cat_to_new(odeclcat)
local ndeclcat = {}
for k, v in pairs(odeclcat) do
ndeclcat[k] = old_decl_cat_entry_to_new(v)
end
return ndeclcat
end
-- populate declensions[] from declensions_old[]
for odecltype, odecl in pairs(declensions_old) do
local ndecltype = old_to_new(odecltype)
if not declensions[ndecltype] then
declensions[ndecltype] = old_decl_to_new(odecl)
end
end
-- populate declensions_cat[] from declensions_old_cat[]
for odecltype, odeclcat in pairs(declensions_old_cat) do
local ndecltype = old_to_new(odecltype)
if not declensions_cat[ndecltype] then
declensions_cat[ndecltype] = old_decl_cat_to_new(odeclcat)
end
end
-- populate declensions_aliases[] from declensions_old_aliases[]
for ofrom, oto in pairs(declensions_old_aliases) do
local from = old_to_new(ofrom)
if not declensions_aliases[from] then
declensions_aliases[from] = old_to_new(oto)
end
end
-- populate internal_notes_table[] from internal_notes_table_old[]
for odecl, note in pairs(internal_notes_table_old) do
local ndecl = old_to_new(odecl)
if not internal_notes_table[ndecl] then
-- FIXME, should we be calling old_to_new() here?
internal_notes_table[ndecl] = note
end
end
--------------------------------------------------------------------------
-- Inflection functions --
--------------------------------------------------------------------------
-- Attach the stressed stem (or plural stem, or barestem) out of ARGS
-- to the unstressed suffix SUF, modifying the suffix as necessary for the
-- last letter of the stem (e.g. if it is velar, sibilant or ц). CASE is
-- the case form being created and is used to select the plural stem if
-- needed. Returns two values, the combined form and the modified suffix.
local function attach_unstressed(args, case, suf, was_stressed)
if suf == nil then
return nil, nil
elseif rfind(suf, CFLEX) then -- if suf has circumflex accent, it forces stressed
return attach_stressed(args, case, suf)
end
local stem, tr
if rfind(case, "_pl") then
stem, tr = args.pl, args.pltr
end
if not stem and case == "ins_sg" then
stem, tr = args.ins_sg_stem, args.ins_sg_tr
end
if not stem then
stem, tr = args.stem, args.stemtr
end
if nom.nonsyllabic_suffixes[suf] then
-- If gen_pl, use special args.gen_pl_bare if given, else regular
-- args.bare if there isn't a plural stem. If nom_sg, always use
-- regular args.bare.
local barearg, bareargtr
if case == "gen_pl" then
barearg, bareargtr = args.gen_pl_bare, args.gen_pl_baretr
if not barearg and args.pl == args.stem then
barearg, bareargtr = args.bare, args.baretr
end
else
barearg, bareargtr = args.bare, args.baretr
end
local barestem = barearg or stem
local barestem, baretr
if barearg then
barestem, baretr = barearg, bareargtr
else
barestem, baretr = stem, tr
end
if was_stressed and case == "gen_pl" then
if not barearg then
local gen_pl_stem, gen_pl_tr = com.make_ending_stressed(stem, tr)
barestem, baretr = gen_pl_stem, gen_pl_tr
end
end
if rlfind(barestem, "[йьъ]$") then
suf = ""
else
if suf == "ъ" then
-- OK
elseif suf == "й" or suf == "ь" then
if barearg and case == "gen_pl" then
-- explicit bare or reducible, don't add -ь
suf = ""
elseif rfind(barestem, "[" .. com.vowel .. "][" .. AC .. GR .. "]?$") then
-- not reducible, do add -ь and correct to -й if necessary
suf = "й"
else
suf = "ь"
end
end
end
return com.concat_russian_tr(barestem, baretr, suf, nil, "dopair"), suf
end
suf = com.make_unstressed(suf)
local rules = nom.unstressed_rules[ulower(usub(stem, -1))]
return nom.combine_stem_and_suffix(stem, tr, suf, rules, args.old)
end
-- Analogous to attach_unstressed() but for the unstressed stem and a
-- stressed suffix.
attach_stressed = function(args, case, suf)
if suf == nil then
return nil, nil
end
-- circumflex forces stress even when the accent pattern calls for no stress
suf = rsub(suf, "̂", AC)
if com.is_unstressed(suf) then
return attach_unstressed(args, case, suf, "was stressed")
end
local stem, tr
if rfind(case, "_pl") then
stem, tr = args.upl, args.upltr
end
if not stem then
stem, tr = args.ustem, args.ustemtr
end
local rules = nom.stressed_rules[ulower(usub(stem, -1))]
return nom.combine_stem_and_suffix(stem, tr, suf, rules, args.old)
end
-- Attach the appropriate stressed or unstressed stem (or plural stem as
-- determined by CASE, or barestem) out of ARGS to the suffix SUF, which may
-- be a list of alternative suffixes (e.g. in the inst sg of feminine nouns).
-- Calls FUN (either attach_stressed() or attach_unstressed()) to do the work
-- for an individual suffix. Returns two values, a list of combined forms
-- and a list of the real suffixes used (which may be modified from the
-- passed-in suffixes, e.g. by removing stress marks or modifying vowels in
-- various ways after a stem-final velar, sibilant or ц). Each combined form
-- is a two-element list {stem, tr} (or a one-element list if tr is nil).
-- IRREG is true if this is an irregular form. We are handling the Nth word;
-- ISLAST is true if this is the last one.
local function attach_with(args, case, suf, fun, irreg, n, islast)
if type(suf) == "table" then
local all_combineds = {}
local all_realsufs = {}
for _, x in ipairs(suf) do
local combineds, realsufs =
attach_with(args, case, x, fun, irreg, n, islast)
for _, combined in ipairs(combineds) do
table.insert(all_combineds, combined)
end
for _, realsuf in ipairs(realsufs) do
table.insert(all_realsufs, realsuf)
end
end
return all_combineds, all_realsufs
else
local combined, realsuf = fun(args, case, suf)
local irregsuf = irreg and {IRREGMARKER} or {""}
return {combined and com.concat_paired_russian_tr(
com.concat_paired_russian_tr(args["prefix" .. n], combined),
com.concat_paired_russian_tr(args["suffix" .. n], irregsuf)) or nil},
{realsuf and realsuf .. args["suffix" .. n][1] or nil}
end
end
-- Generate the form(s) and suffix(es) for CASE according to the declension
-- table DECL, using the attachment function FUN (one of attach_stressed()
-- or attach_unstressed()). IS_SLASH is true if this is a slash declension
-- (different declensions for singular and plural). We are handling the Nth
-- word; ISLAST is true if this is the last one.
local function gen_form(args, decl, case, stress, fun, is_slash, n, islast)
local irreg = false
if not args.suffixes[case] then
args.suffixes[case] = {}
end
local decl_sufs = args.old and declensions_old or declensions
decl_sufs = decl_sufs[decl]
local suf = decl_sufs[case]
local decl_cats = args.old and declensions_old_cat or declensions_cat
local ispl = rfind(case, "_pl")
if ispl and (decl_cats[decl].irregpl or args.pl and args.pl ~= args.stem or is_slash) then
irreg = true
end
if case == "nom_pl" and decl_cats[decl].alt_nom_pl then
irreg = true
end
if type(suf) == "function" then
suf = suf(ispl and args.pl or args.stem, stress, args)
end
if case == "gen_pl" and args.alt_gen_pl then
suf = decl_sufs.alt_gen_pl
irreg = true
if not suf then
error("No alternative genitive plural available for this declension class")
end
end
local combineds, realsufs = attach_with(args, case, suf, fun, irreg, n, islast)
for _, realsuf in ipairs(realsufs) do
args.any_non_nil[case] = true
args.this_any_non_nil[case] = true
insert_if_not(args.suffixes[case], realsuf)
end
return combineds
end
local attachers = {
["+"] = attach_stressed,
["-"] = attach_unstressed,
}
do_stress_pattern = function(stress, args, decl, number, n, islast)
local f = {}
for _, case in ipairs(decl_cases) do
if not number or (number == "sg" and rfind(case, "_sg")) or
(number == "pl" and rfind(case, "_pl")) then
f[case] = gen_form(args, decl, case, stress,
attachers[stress_patterns[stress][case]], not not number,
n, islast)
-- Turn empty form lists into nil to facilitate computation of
-- animate/inanimate accusatives below
if f[case] and #f[case] == 0 then
f[case] = nil
end
-- Compute linked versions of potential lemma cases, for use
-- in the ru-noun+ headword. We substitute the original lemma
-- (before removing links) for forms that are the same as the
-- lemma, if the original lemma has links.
if f[case] and (case == "nom_sg" or case == "nom_pl") then
local linked_forms = {}
for _, form in ipairs(f[case]) do
-- Return true if FORM is "close enough" to LEMMA that we can substitute the
-- linked form of the lemma. Currently this means exactly the same except that
-- we ignore acute and grave accent differences in monosyllables, and ignore
-- notes that may have been appended (e.g. the triangle marking irregularity).
local entry, notes = m_table_tools.separate_notes(form[1])
local lemma = args.lemma_no_links
local close_enough_to_lemma =
entry == lemma or (com.is_monosyllabic(entry) and
com.is_monosyllabic(lemma) and
com.remove_accents(entry) == com.remove_accents(lemma))
if close_enough_to_lemma and
rfind(args.orig_lemma, "%[%[") then
table.insert(linked_forms, {args.orig_lemma .. notes, args.lemmatr and args.lemmatr .. notes})
else
table.insert(linked_forms, form)
end
end
f[case .. "_linked"] = linked_forms
end
end
end
-- Set acc an/in variants now as appropriate. We used to do this in
-- handle_forms_and_overrides(), which simplified the handling of
-- nom/gen/acc overrides but caused problems for words like мазло and
-- трепло that had a mixture of nil and non-nil accusative forms.
local an = args.thisa
if not number or number == "sg" then
f.acc_sg_an = f.acc_sg_an or f.acc_sg or an == "i" and f.nom_sg or f.gen_sg
f.acc_sg_in = f.acc_sg_in or f.acc_sg or an == "a" and f.gen_sg or f.nom_sg
end
if not number or number == "pl" then
f.acc_pl_an = f.acc_pl_an or f.acc_pl or an == "i" and f.nom_pl or f.gen_pl
f.acc_pl_in = f.acc_pl_in or f.acc_pl or an == "a" and f.gen_pl or f.nom_pl
end
for case, forms in pairs(f) do
if not args.forms[case] then
args.forms[case] = {}
end
for _, form in ipairs(forms) do
insert_if_not(args.forms[case], form)
end
end
end
stress_patterns["a"] = {
nom_sg="-", gen_sg="-", dat_sg="-", acc_sg="-", ins_sg="-", pre_sg="-",
nom_pl="-", gen_pl="-", dat_pl="-", acc_pl="-", ins_pl="-", pre_pl="-",
}
stress_patterns["b"] = {
nom_sg="+", gen_sg="+", dat_sg="+", acc_sg="+", ins_sg="+", pre_sg="+",
nom_pl="+", gen_pl="+", dat_pl="+", acc_pl="+", ins_pl="+", pre_pl="+",
}
stress_patterns["b'"] = {
nom_sg="+", gen_sg="+", dat_sg="+", acc_sg="+", ins_sg="-", pre_sg="+",
nom_pl="+", gen_pl="+", dat_pl="+", acc_pl="+", ins_pl="+", pre_pl="+",
}
stress_patterns["c"] = {
nom_sg="-", gen_sg="-", dat_sg="-", acc_sg="-", ins_sg="-", pre_sg="-",
nom_pl="+", gen_pl="+", dat_pl="+", acc_pl="+", ins_pl="+", pre_pl="+",
}
stress_patterns["d"] = {
nom_sg="+", gen_sg="+", dat_sg="+", acc_sg="+", ins_sg="+", pre_sg="+",
nom_pl="-", gen_pl="-", dat_pl="-", acc_pl="-", ins_pl="-", pre_pl="-",
}
stress_patterns["d'"] = {
nom_sg="+", gen_sg="+", dat_sg="+", acc_sg="-", ins_sg="+", pre_sg="+",
nom_pl="-", gen_pl="-", dat_pl="-", acc_pl="-", ins_pl="-", pre_pl="-",
}
stress_patterns["e"] = {
nom_sg="-", gen_sg="-", dat_sg="-", acc_sg="-", ins_sg="-", pre_sg="-",
nom_pl="-", gen_pl="+", dat_pl="+", acc_pl="+", ins_pl="+", pre_pl="+",
}
stress_patterns["f"] = {
nom_sg="+", gen_sg="+", dat_sg="+", acc_sg="+", ins_sg="+", pre_sg="+",
nom_pl="-", gen_pl="+", dat_pl="+", acc_pl="+", ins_pl="+", pre_pl="+",
}
stress_patterns["f'"] = {
nom_sg="+", gen_sg="+", dat_sg="+", acc_sg="-", ins_sg="+", pre_sg="+",
nom_pl="-", gen_pl="+", dat_pl="+", acc_pl="+", ins_pl="+", pre_pl="+",
}
stress_patterns["f''"] = {
nom_sg="+", gen_sg="+", dat_sg="+", acc_sg="+", ins_sg="-", pre_sg="+",
nom_pl="-", gen_pl="+", dat_pl="+", acc_pl="+", ins_pl="+", pre_pl="+",
}
ending_stressed_gen_pl_patterns = m_table.listToSet({"b", "b'", "c", "e", "f", "f'", "f''"})
ending_stressed_pre_sg_patterns = m_table.listToSet({"b", "b'", "d", "d'", "f", "f'", "f''"})
ending_stressed_dat_sg_patterns = ending_stressed_pre_sg_patterns
ending_stressed_sg_patterns = ending_stressed_pre_sg_patterns
ending_stressed_pl_patterns = m_table.listToSet({"b", "b'", "c"})
local numbers = {
["s"] = "singular",
["p"] = "plural",
}
local old_title_temp = [=[Pre-reform declension of <b lang="ru" class="Cyrl">{lemma}</b>]=]
local title_temp = [=[Declension of <b lang="ru" class="Cyrl">{lemma}</b>]=]
local extra_case_template, extra_case_template_with_plural
local internal_notes_template
local notes_template
local templates = {}
-- Convert a raw override into a canonicalized list of individual overrides.
-- If input is nil, so is output. Certain junk (e.g. <br/>) is removed,
-- and ~ and ~~ are substituted appropriately; ARGS and CASE are required for
-- this purpose. FORMS is the list of existing case forms and is currently
-- used only to retrieve the dat_sg for handling + in loc/par (this means
-- that any overrides of the dat_sg have to be handled before handling
-- overrides of loc/par). N is the suffix used to retrieve the override --
-- either a number for word-specific overrides, or an empty string for
-- overall overrides.
--
-- It will still be necessary to call m_table_tools.separate_notes() to separate
-- off any trailing "notes" (asterisks, superscript numbers, etc.), and
-- m_links.remove_links() to remove any links to get the raw override form.
canonicalize_override = function(args, case, forms, n)
local val = args[case .. n]
if not val then
return nil
end
-- clean <br /> that's in many multi-form entries and messes up linking
val = rsub(val, "<br%s*/>", "")
-- substitute ~ and ~~ and split by commas
local stem, manualtr
if rfind(case, "_pl") then
stem, manualtr = args.pl, args.pltr
end
if not stem then
stem, manualtr = args.stem, args.stemtr
end
local ustem, umanualtr = com.make_unstressed_once(stem, manualtr)
local stemtr, ustemtr = manualtr, umanualtr
local vals = rsplit(val, "%s*,%s*")
local retvals = {}
for _, val in ipairs(vals) do
local valru, valtr = com.split_russian_tr(val)
valru = rsub(valru, "~~", ustem)
valru = rsub(valru, "~", com.is_stressed(val) and ustem or stem)
if rfind(valru, "^%*") then
valru = rsub(valru, "^%*", "") .. HYPMARKER
end
if valtr then
stemtr = stemtr or com.translit_no_links(stem)
ustemtr = ustemtr or com.translit_no_links(ustem)
valtr = rsub(valtr, "~~", ustemtr)
valtr = rsub(valtr, "~", com.is_stressed(val) and ustemtr or stemtr)
elseif val:find("~") and manualtr then
valtr = com.translit_no_links(val)
valtr = rsub(valtr, "~~", umanualtr)
valtr = rsub(valtr, "~", com.is_stressed(val) and umanualtr or manualtr)
end
if valtr then
if valtr:sub(1, 1) == "*" then
valtr = valtr:sub(2) .. HYPMARKER
end
valtr = com.j_correction(valtr)
end
table.insert(retvals, {valru, valtr})
end
vals = retvals
-- handle + in loc/par meaning "the expected form"; NOTE: This requires
-- that dat_sg has already been processed!
if case == "loc" or case == "par" then
local new_vals = {}
for _, rutr in ipairs(vals) do
-- don't just handle + by itself in case the arg has в or на
-- or whatever attached to it
if rfind(rutr[1], "^%+") or rfind(rutr[1], "[%s%[|]%+") then
for _, dat in ipairs(forms["dat_sg"]) do
local ru, tr = rutr[1], rutr[2]
local datru, dattr = dat[1], dat[2]
local valru, valtr
-- separate off any footnote symbols (which may have been
-- introduced by a *tail or *tailall argument, which we need
-- to process before this stage so overrides don't get
-- automatically marked with footnote symbols); if par,
-- try to preserve them, but with loc this can cause
-- problems in case there are multiple dative forms
-- (stress variants) and the last one is marked with a
-- footnote symbol (occurs in грудь)
local datru_entry, datru_notes = m_table_tools.separate_notes(datru)
local dattr_entry, dattr_notes
if dattr then
dattr_entry, dattr_notes = m_table_tools.separate_notes(dattr)
end
if case == "par" then
valru, valtr = datru_entry, dattr_entry
else
valru, valtr = com.make_ending_stressed(datru_entry, dattr_entry)
datru_notes = ""
dattr_notes = ""
end
-- wrap the word in brackets so it's linked; but not if it
-- appears to already be linked
ru = rsub(ru, "^%+", "[[" .. valru .. "]]")
ru = rsub(ru, "([%[|])%+", "%1" .. valru)
ru = rsub(ru, "(%s)%+", "%1[[" .. valru .. "]]")
ru = ru .. datru_notes
-- do the translit; but it shouldn't have brackets in it
if tr or valtr then
tr = tr or com.translit_no_links(rutr[1])
valtr = valtr or com.translit_no_links(valru)
tr = rsub(tr, "^%+", valtr)
tr = rsub(tr, "(%s)%+", "%1" .. valtr)
tr = tr .. dattr_notes
end
table.insert(new_vals, {ru, tr})
end
else
table.insert(new_vals, rutr)
end
end
vals = new_vals
end
-- auto-accent/check for necessary accents
local newvals = {}
for _, v in ipairs(vals) do
local ru, tr = v[1], v[2]
if not args.allow_unaccented then
if tr and com.is_unstressed(ru) ~= com.is_unstressed(tr) then
error("Override " .. ru .. " and translit " .. tr .. " must have same accent pattern")
end
-- it's safe to accent monosyllabic stems
if com.is_monosyllabic(ru) then
ru, tr = com.make_ending_stressed(ru, tr)
elseif com.needs_accents(ru) then
error("Override " .. ru .. " for case " .. case .. n .. " requires an accent")
end
end
table.insert(newvals, {ru, tr})
end
vals = newvals
return vals
end
local function process_overrides(args, f, n)
local function process_override(case)
if args[case .. n] then
local overrides = canonicalize_override(args, case, f, n)
if not f[case] then
f[case] = {}
end
local new_overrides = {}
for _, form in ipairs(overrides) do
-- Don't consider overrides of loc/par/voc irregular since
-- they're only specified through overrides; FIXME: Theoretically
-- we could consider loc/par irregular if they don't follow the
-- expected forms; but we'd have to figure out how to eliminate
-- the preposition that may be specified
if not overridable_only_cases_set[case] and
not args.manual and
not contains_rutr_pair(f[case], form) then
local formru, formnotes = m_table_tools.separate_notes(form[1])
if formru ~= "-" then
-- don't mark an override of - as irregular, even if
-- it has an attached footnote symbol
form = com.concat_paired_russian_tr(form, {IRREGMARKER})
end
end
if case == "pauc" then
-- Internal note indicating that the form is for numbers 2, 3 and 4.
form = com.concat_paired_russian_tr(form, {paucal_marker})
end
table.insert(new_overrides, form)
end
f[case] = new_overrides
args.any_overridden[case] = true
end
end
-- do dative singular first because it will be used by loc/par
process_override("dat_sg")
-- now do the rest
for _, case in ipairs(overridable_cases) do
if case ~= "dat_sg" then
process_override(case)
end
end
-- if the nominative is overridden, use it to set the linked version unless
-- that is also specifically overridden
if args["nom_sg" .. n] and not args["nom_sg_linked" .. n] then
f.nom_sg_linked = f.nom_sg
end
if args["nom_pl" .. n] and not args["nom_pl_linked" .. n] then
f.nom_pl_linked = f.nom_pl
end
-- convert empty lists to nil to facilitate computation of accusative
-- case variants below.
for _, case in ipairs(all_cases) do
if f[case] then
if type(f[case]) ~= "table" then
error("Logic error, args[case] should be nil or table")
end
if #f[case] == 0 then
f[case] = nil
end
end
end
end
local function process_tail_args(args, f, n)
local function append_note_all(case, value)
value = com.split_russian_tr(value, "dopair")
local function append1(case)
if f[case] then
for i=1,#f[case] do
f[case][i] = com.concat_paired_russian_tr(f[case][i], value)
end
end
end
append1(case)
if case == "acc_sg" then
append1("acc_sg_in")
append1("acc_sg_an")
elseif case == "acc_pl" then
append1("acc_pl_in")
append1("acc_pl_an")
end
end
local function append_note_last(case, value, gt_one)
value = com.split_russian_tr(value, "dopair")
local function append1(case)
if f[case] then
local lastarg = #f[case]
if lastarg > (gt_one and 1 or 0) then
f[case][lastarg] = com.concat_paired_russian_tr(f[case][lastarg], value)
end
end
end
append1(case)
if case == "acc_sg" then
append1("acc_sg_in")
append1("acc_sg_an")
elseif case == "acc_pl" then
append1("acc_pl_in")
append1("acc_pl_an")
end
end
local function handle_tail_hyp(suf)
local arg, val
for _, case in ipairs(all_cases) do
arg = args[case .. "_" .. suf .. n]
if arg then
append_note_last(case, suf == "hyp" and HYPMARKER or arg)
end
arg = args[case .. "_" .. suf .. "all" .. n]
if arg then
append_note_all(case, suf == "hyp" and HYPMARKER or arg)
end
if not rfind(case, "_pl") then
arg = args["sg" .. suf .. "all" .. n]
if arg then
append_note_all(case, suf == "hyp" and HYPMARKER or arg)
end
arg = args["sg" .. suf .. n]
if arg then
append_note_last(case, suf == "hyp" and HYPMARKER or arg, ">1")
end
else
arg = args["pl" .. suf .. "all" .. n]
if arg then
append_note_all(case, suf == "hyp" and HYPMARKER or arg)
end
arg = args["pl" .. suf .. n]
if arg then
append_note_last(case, suf == "hyp" and HYPMARKER or arg, ">1")
end
end
if not rfind(case, "nom_") and not rfind(case, "acc_") then
arg = args["obl" .. suf .. "all" .. n]
if arg then
append_note_all(case, suf == "hyp" and HYPMARKER or arg)
end
arg = args["obl" .. suf .. n]
if arg then
append_note_last(case, suf == "hyp" and HYPMARKER or arg, ">1")
end
end
end
end
handle_tail_hyp("tail")
handle_tail_hyp("hyp")
end
handle_forms_and_overrides = function(args, n, islast)
local f = args.forms
process_tail_args(args, f, n)
process_overrides(args, f, n)
local an = args.thisa
-- Maybe set the value of the animate/inanimate accusative variants based
-- on nom/acc/gen overrides. Don't do this if there was a specific override
-- of this form. Otherwise, always propagate an accusative singular
-- override, and propagate the nom/gen sg if there wasn't a specific
-- accusative suffix anywhere (occurs in the fem sg and sometimes the neut
-- sg). This logic duplicates logic in handle_overall_forms_and_overrides();
-- see long comment there. We need to duplicate the whole logic here to
-- handle words like мать-одиночка, which has an override of acc_sg1.
if not args["acc_sg_an" .. n] then
if args["acc_sg" .. n] then
f.acc_sg_an = f.acc_sg
elseif not args.this_any_non_nil.acc_sg then
f.acc_sg_an = f.acc_sg or an == "i" and f.nom_sg or f.gen_sg or f.acc_sg_an
end
end
if not args["acc_sg_in" .. n] then
if args["acc_sg" .. n] then
f.acc_sg_in = f.acc_sg
elseif not args.this_any_non_nil.acc_sg then
f.acc_sg_in = f.acc_sg or an == "a" and f.gen_sg or f.nom_sg or f.acc_sg_in
end
end
if not args["acc_pl_an" .. n] then
if args["acc_pl" .. n] then
f.acc_pl_an = f.acc_pl
elseif not args.this_any_non_nil.acc_pl then
f.acc_pl_an = f.acc_pl or an== "i" and f.nom_pl or f.gen_pl or f.acc_pl_an
end
end
if not args["acc_pl_in" .. n] then
if args["acc_pl" .. n] then
f.acc_pl_in = f.acc_pl
elseif not args.this_any_non_nil.acc_pl then
f.acc_pl_in = f.acc_pl or an == "a" and f.gen_pl or f.nom_pl or f.acc_pl_in
end
end
f.loc = f.loc or f.pre_sg
f.par = f.par or f.gen_sg
f.voc = f.voc or f.nom_sg
-- Set these in case we have plural only, in which case the
-- singular will also get set to these same values in case we are
-- a plural-only word in a singular-only expression.
f.loc_pl = f.loc_pl or f.pre_pl
f.par_pl = f.par_pl or f.gen_pl
f.voc_pl = f.voc_pl or f.nom_pl
f.count = f.count or f.gen_pl
f.pauc = f.pauc or f.gen_sg
local nu = args.thisn
-- If we have a singular-only, set the plural forms to the singular forms,
-- and vice-versa. This is important so that things work in multi-word
-- expressions that combine different number restrictions (e.g.
-- singular-only with singular/plural or singular-only with plural-only,
-- compare "St. Vincent and the Grenadines" [Сент-Винсент и Гренадины]).
if nu == "s" then
f.nom_pl_linked = f.nom_sg_linked
f.nom_pl = f.nom_sg
f.gen_pl = f.gen_sg
f.dat_pl = f.dat_sg
f.acc_pl = f.acc_sg
f.acc_pl_an = f.acc_sg_an
f.acc_pl_in = f.acc_sg_in
f.ins_pl = f.ins_sg
f.pre_pl = f.pre_sg
f.nom_pl = f.nom_sg
f.loc_pl = f.loc
f.par_pl = f.par
f.voc_pl = f.voc
elseif nu == "p" then
f.nom_sg_linked = f.nom_pl_linked
f.nom_sg = f.nom_pl
f.gen_sg = f.gen_pl
f.dat_sg = f.dat_pl
f.acc_sg = f.acc_pl
f.acc_sg_an = f.acc_pl_an
f.acc_sg_in = f.acc_pl_in
f.ins_sg = f.ins_pl
f.pre_sg = f.pre_pl
f.nom_sg = f.nom_pl
f.loc = f.loc_pl
f.par = f.par_pl
f.voc = f.voc_pl
end
end
handle_overall_forms_and_overrides = function(args)
local overall_forms = {}
for _, case in ipairs(displayable_cases) do
overall_forms[case] = concat_word_forms(args.per_word_info, case)
end
local acc_sg_overridden = args.acc_sg
local acc_pl_overridden = args.acc_pl
process_tail_args(args, overall_forms, "")
process_overrides(args, overall_forms, "")
if case_will_be_displayed(args, "pauc") then
insert_if_not(args.internal_notes, paucal_internal_note)
end
-- if IRREGMARKER is anywhere in text, remove all instances and put
-- at the end before any notes.
local function clean_irreg_marker(case, text)
if rfind(text, IRREGMARKER) then
text = rsub(text, IRREGMARKER, "")
local entry, notes = m_table_tools.separate_notes(text)
if case_will_be_displayed(args, case) then
insert_if_not(args.internal_notes, IRREGMARKER .. " Irregular.")
args.any_irreg = true
args.any_irreg_case[case] = true
end
return entry .. IRREGMARKER .. notes
else
return text
end
end
-- set final args[case] and clean up IRREGMARKER.
for _, case in ipairs(all_cases) do
args[case] = overall_forms[case]
if args[case] then
local cleaned_forms = {}
for _, form in ipairs(args[case]) do
local ru, tr = form[1], form[2]
ru = clean_irreg_marker(case, ru)
if tr then
tr = clean_irreg_marker(case, tr)
end
table.insert(cleaned_forms, {ru, tr})
end
args[case] = cleaned_forms
end
end
-- Maybe set the value of the animate/inanimate accusative variants based
-- on nom/acc/gen overrides. Don't do this if there was a specific override
-- of this form. Otherwise, always propagate an accusative singular
-- override, and propagate the nom/gen sg if there wasn't a specific
-- accusative suffix anywhere (occurs in the fem sg and sometimes the neut
-- sg). We need to do this somewhat complicated procedure to get overrides
-- to work correctly, e.g. acc_sg overrides of feminine and neuter nouns
-- (such as мать and полслова) and gen_sg/gen_pl overrides of masculine
-- animate nouns. We also ran into an issue with words like мазло that are
-- neuter-form but can be both masculine animate (in which case the
-- acc sg inherits from the genitive) and neuter animate (in which case the
-- acc sg has the fixed ending -о, same as nominative, despite the animacy).
-- Remember also that the animate/inanimate accusative variants are the ones
-- displayed, not the plain acc_sg/acc_pl ones.
if not args.any_overridden.acc_sg_an then
if acc_sg_overridden then
args.acc_sg_an = args.acc_sg
elseif not args.any_non_nil.acc_sg then
args.acc_sg_an = args.acc_sg or args.a == "i" and args.nom_sg or args.gen_sg or args.acc_sg_an
end
end
if not args.any_overridden.acc_sg_in then
if acc_sg_overridden then
args.acc_sg_in = args.acc_sg
elseif not args.any_non_nil.acc_sg then
args.acc_sg_in = args.acc_sg or args.a == "a" and args.gen_sg or args.nom_sg or args.acc_sg_in
end
end
if not args.any_overridden.acc_pl_an then
if acc_pl_overridden then
args.acc_pl_an = args.acc_pl
elseif not args.any_non_nil.acc_pl then
args.acc_pl_an = args.acc_pl or args.a == "i" and args.nom_pl or args.gen_pl or args.acc_pl_an
end
end
if not args.any_overridden.acc_pl_in then
if acc_pl_overridden then
args.acc_pl_in = args.acc_pl
elseif not args.any_non_nil.acc_pl then
args.acc_pl_in = args.acc_pl or args.a == "a" and args.gen_pl or args.nom_pl or args.acc_pl_in
end
end
-- Try to set the values of acc_sg and acc_pl. The only time we can't is
-- when the noun is bianimate and the anim/inan values are different.
-- This is used primarily for generate_forms() and generate_multi_forms(),
-- since we don't actually display these forms.
if args.a == "a" then
args.acc_sg = args.acc_sg or args.acc_sg_an
args.acc_pl = args.acc_pl or args.acc_pl_an
elseif args.a == "i" then
args.acc_sg = args.acc_sg or args.acc_sg_in
args.acc_pl = args.acc_pl or args.acc_pl_in
else -- bianimate
args.acc_sg = args.acc_sg or m_table.deepEquals(args.acc_sg_in, args.acc_sg_an) and args.acc_sg_in or nil
args.acc_pl = args.acc_pl or m_table.deepEquals(args.acc_pl_in, args.acc_pl_an) and args.acc_pl_in or nil
end
end
-- Subfunction of concat_word_forms(), used to implement recursively
-- generating all combinations of elements from WORD_FORMS (a list, one
-- element per word, of a list of the forms for a word, each of which is a
-- two-element list of {RUSSIAN, TR}) and TRAILING_FORMS (a list of forms, the
-- accumulated suffixes for trailing words so far in the recursion process,
-- again where each form is a two-element list {RUSSIAN, TR}). Each time we
-- recur we take the last FORMS item off of WORD_FORMS and to each form in
-- FORMS we add all elements in TRAILING_FORMS, passing the newly generated
-- list of items down the next recursion level with the shorter WORD_FORMS.
-- We end up returning a list of concatenated forms, where each list item
-- is a two-element list {RUSSIAN, TR}.
local function concat_word_forms_1(word_forms, trailing_forms)
if #word_forms == 0 then
local retforms = {}
for _, form in ipairs(trailing_forms) do
local ru, tr = form[1], form[2]
-- Remove <insa> and <insb> markers; they've served their purpose.
ru = rsub(ru, "<ins[ab]>", "")
tr = tr and rsub(tr, "<ins[ab]>", "")
table.insert(retforms, {ru, tr})
end
return retforms
else
local last_form_info = table.remove(word_forms)
local last_forms, joiner = last_form_info[1], last_form_info[2]
local new_trailing_forms = {}
for _, form in ipairs(last_forms) do
for _, trailing_form in ipairs(trailing_forms) do
-- If form to prepend is empty, don't add the joiner; this
-- is principally used in overall overrides, where we stuff
-- the entire override into the last word
local full_form = form[1] == "" and trailing_form or
com.concat_paired_russian_tr(form,
com.concat_paired_russian_tr(joiner, trailing_form),
"movenotes")
if rfind(full_form[1], "<insa>") and rfind(full_form[1], "<insb>") then
-- REJECT! So we don't get mixtures of the two feminine
-- instrumental singular endings.
else
table.insert(new_trailing_forms, full_form)
end
end
end
return concat_word_forms_1(word_forms, new_trailing_forms)
end
end
-- Generate a list of overall forms by concatenating the per-word forms.
-- PER_WORD_INFO comes from args.per_word_info and is a list of
-- WORD_INFO items, one per word, each of which a two element list of
-- WORD_FORMS (a table listing the forms for each case) and JOINER (a string).
-- We loop over all possible combinations of elements from each word's list
-- of forms for the given case; this requires recursion.
concat_word_forms = function(per_word_info, case)
local word_forms = {}
-- Gather the appropriate word forms. We have to recreate this anew
-- because it will be destructively modified by concat_word_forms_1().
for _, word_info in ipairs(per_word_info) do
table.insert(word_forms, {word_info[1][case], word_info[2]})
end
-- We need to start the recursion with the second parameter containing
-- one blank element rather than no elements, otherwise no elements
-- will be propagated to the next recursion level.
return concat_word_forms_1(word_forms, {{""}})
end
local accel_forms = {
nom_sg = "nom|s",
nom_sg_linked = "nom|s",
nom_pl = "nom|p",
nom_pl_linked = "nom|p",
gen_sg = "gen|s",
gen_pl = "gen|p",
dat_sg = "dat|s",
dat_pl = "dat|p",
acc_sg_an = "an|acc|s",
acc_pl_an = "an|acc|p",
acc_sg_in = "in|acc|s",
acc_pl_in = "in|acc|p",
ins_sg = "ins|s",
ins_pl = "ins|p",
pre_sg = "pre|s",
pre_pl = "pre|p",
loc = "loc|s",
loc_pl = "loc|p",
voc = "voc|s",
voc_pl = "voc|p",
par = "par|s",
par_pl = "par|p",
count = "count form",
pauc = "pau",
}
-- Make the table
make_table = function(args)
local data = {}
data.after_title = " " .. args.heading
data.number = args.nonumber and "" or numbers[args.n]
local lemma_forms = args[args.n == "p" and "nom_pl" or "nom_sg"]
data.lemma = nom.show_form(args.explicit_lemma and {{args.explicit_lemma, args.explicit_lemmatr}} or
args[args.n == "p" and "nom_pl_linked" or "nom_sg_linked"], "lemma", nil, nil)
data.title = args.title or format(args.old and old_title_temp or title_temp, data)
local sg_an_in_equal = m_table.deepEquals(args.acc_sg_an, args.acc_sg_in)
local pl_an_in_equal = m_table.deepEquals(args.acc_pl_an, args.acc_pl_in)
for _, case in ipairs(displayable_cases) do
local accel_form = accel_forms[case]
if not accel_form then
error("Something wrong, can't find accelerator form for " .. case)
end
if (sg_an_in_equal and (case == "acc_sg_an" or case == "acc_sg_in") or
pl_an_in_equal and (case == "acc_pl_an" or case == "acc_pl_in")) then
accel_form = accel_form:gsub("^[ai]n|acc%f[%W]", "acc")
end
data[case] = nom.show_form(args[case], false, accel_form, lemma_forms, "remove monosyllabic accents only lemma")
end
local temp = nil
if args.n == "s" then
data.nom_x = data.nom_sg
data.gen_x = data.gen_sg
data.dat_x = data.dat_sg
data.acc_x_an = data.acc_sg_an
data.acc_x_in = data.acc_sg_in
data.ins_x = data.ins_sg
data.pre_x = data.pre_sg
if sg_an_in_equal then
temp = "one_number"
else
temp = "one_number_split_animacy"
end
elseif args.n == "p" then
data.nom_x = data.nom_pl
data.gen_x = data.gen_pl
data.dat_x = data.dat_pl
data.acc_x_an = data.acc_pl_an
data.acc_x_in = data.acc_pl_in
data.ins_x = data.ins_pl
data.pre_x = data.pre_pl
data.par = data.par_pl
data.loc = data.loc_pl
data.voc = data.voc_pl
if pl_an_in_equal then
temp = "one_number"
else
temp = "one_number_split_animacy"
end
else
if pl_an_in_equal then
temp = "both_numbers"
elseif sg_an_in_equal then
temp = "both_numbers_split_animacy_plural_only"
else
temp = "both_numbers_split_animacy"
end
end
for _, extra_case in ipairs({
{"par", "partitive"},
{"loc", "locative"},
{"voc", "vocative"},
}) do
local case, engcase, template = unpack(extra_case)
local a = args.a
local colspan = (a == "b" or a == "bi" or a == "both" or a == "ai" or a == "ia") and 2 or 1
if args.n ~= "s" and args.n ~= "p" and args.any_overridden[case .. "_pl"] then
if not args.any_overridden[case] then
data[case] = ""
end
template = extra_case_template_with_plural
elseif args.n ~= "p" and args.any_overridden[case] or args.n == "p" and args.any_overridden[case .. "_pl"] then
template = extra_case_template
end
if template then
template = format(template(colspan), {case=case, engcase=engcase})
data[case .. "_clause"] = format(template, data)
else
data[case .. "_clause"] = ""
end
end
for _, extra_case in ipairs({
{"count", "count form"},
{"pauc", "paucal"},
}) do
local case, engcase, template = unpack(extra_case)
local a = args.a
local colspan = (a == "b" or a == "bi" or a == "both" or a == "ai" or a == "ia") and 2 or 1
if args.n ~= "p" and args.any_overridden[case] then
template = extra_case_template
end
if template then
template = format(template(colspan), {case=case, engcase=engcase})
data[case .. "_clause"] = format(template, data)
else
data[case .. "_clause"] = ""
end
end
local notes = get_arg_chain(args, "notes", "notes")
local all_notes = {}
for _, note in ipairs(args.internal_notes) do
-- Superscript footnote marker at beginning of note, similarly to what's
-- done at end of forms.
local symbol, entry = m_table_tools.separate_and_superscript_initial_notes(note)
table.insert(all_notes, symbol .. entry)
end
for _, note in ipairs(notes) do
-- Here too.
local symbol, entry = m_table_tools.separate_and_superscript_initial_notes(note)
table.insert(all_notes, symbol .. entry)
end
data.notes = table.concat(all_notes, "<br />")
data.notes_clause = data.notes ~= "" and format(notes_template, data) or ""
return format(templates[temp], data)
end
function extra_case_template(colspan)
colspan = colspan or 1
return ([===[
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=%s | {engcase}
| {\op}{case}{\cl}
|
|-]===]):format(colspan)
end
function extra_case_template_with_plural(colspan)
colspan = colspan or 1
return ([===[
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=%s | {engcase}
| {\op}{case}{\cl}
| {\op}{case}_pl{\cl}
|-]===]):format(colspan)
end
notes_template = [===[
<div style="width:100%;text-align:left;background:var(--wikt-palette-lightblue, #d9ebff);">
<div style="display:inline-block;text-align:left;padding-left:1em;padding-right:1em">
{notes}
</div></div>
]===]
local function template_prelude(min_width)
min_width = min_width or "70"
return rsub([===[
<div>
<div class="NavFrame" data-toggle-category="declension" style="max-width:MINWIDTHem">
<div class="NavHead" style="background:var(--wikt-palette-lighterblue, #ebf4ff);">{title}<span style="font-weight:normal;">{after_title}</span> </div>
<div class="NavContent">
{\op}| style="table-layout:fixed; text-align:center; max-width:MINWIDTHem; width:100%;" class="inflection inflection-table"
|- class="rowgroup"
]===], "MINWIDTH", min_width)
end
local function template_postlude()
return [===[|-{par_clause}{loc_clause}{voc_clause}{count_clause}{pauc_clause}
|{\cl}{notes_clause}</div></div></div>]===]
end
templates["both_numbers"] = template_prelude("45") .. [===[
! style="width:7em;background:var(--wikt-palette-lightblue, #d9ebff);" |
! style="background:var(--wikt-palette-lightblue, #d9ebff);" | singular
! style="background:var(--wikt-palette-lightblue, #d9ebff);" | plural
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | nominative
| data-accel-col=1 | {nom_sg}
| data-accel-col=2 | {nom_pl}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | genitive
| data-accel-col=1 | {gen_sg}
| data-accel-col=2 | {gen_pl}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | dative
| data-accel-col=1 | {dat_sg}
| data-accel-col=2 | {dat_pl}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | accusative
| data-accel-col=1 | {acc_sg_an}
| data-accel-col=2 | {acc_pl_an}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | instrumental
| data-accel-col=1 | {ins_sg}
| data-accel-col=2 | {ins_pl}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | prepositional
| data-accel-col=1 | {pre_sg}
| data-accel-col=2 | {pre_pl}
]===] .. template_postlude()
templates["both_numbers_split_animacy"] = template_prelude("50") .. [===[
! style="width:15em;background:var(--wikt-palette-lightblue, #d9ebff);" colspan=2 |
! style="background:var(--wikt-palette-lightblue, #d9ebff);" | singular
! style="background:var(--wikt-palette-lightblue, #d9ebff);" | plural
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | nominative
| data-accel-col=1 | {nom_sg}
| data-accel-col=2 | {nom_pl}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | genitive
| data-accel-col=1 | {gen_sg}
| data-accel-col=2 | {gen_pl}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | dative
| data-accel-col=1 | {dat_sg}
| data-accel-col=2 | {dat_pl}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" rowspan=2 | accusative
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | animate
| data-accel-col=1 | {acc_sg_an}
| data-accel-col=2 | {acc_pl_an}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | inanimate
| data-accel-col=1 | {acc_sg_in}
| data-accel-col=2 | {acc_pl_in}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | instrumental
| data-accel-col=1 | {ins_sg}
| data-accel-col=2 | {ins_pl}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | prepositional
| data-accel-col=1 | {pre_sg}
| data-accel-col=2 | {pre_pl}
]===] .. template_postlude()
templates["both_numbers_split_animacy_plural_only"] = template_prelude("50") .. [===[
! style="width:15em;background:var(--wikt-palette-lightblue, #d9ebff);" colspan=2 |
! style="background:var(--wikt-palette-lightblue, #d9ebff);" | singular
! style="background:var(--wikt-palette-lightblue, #d9ebff);" | plural
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | nominative
| data-accel-col=1 | {nom_sg}
| data-accel-col=2 | {nom_pl}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | genitive
| data-accel-col=1 | {gen_sg}
| data-accel-col=2 | {gen_pl}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | dative
| data-accel-col=1 | {dat_sg}
| data-accel-col=2 | {dat_pl}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" rowspan=2 | accusative
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | animate
| data-accel-col=1 rowspan=2 | {acc_sg_an}
| data-accel-col=2 | {acc_pl_an}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | inanimate
| data-accel-col=2 | {acc_pl_in}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | instrumental
| data-accel-col=1 | {ins_sg}
| data-accel-col=2 | {ins_pl}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | prepositional
| data-accel-col=1 | {pre_sg}
| data-accel-col=2 | {pre_pl}
]===] .. template_postlude()
templates["one_number"] = template_prelude("30") .. [===[
! style="width:7em;background:var(--wikt-palette-lightblue, #d9ebff);" |
! style="background:var(--wikt-palette-lightblue, #d9ebff);" | {number}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | nominative
| {nom_x}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | genitive
| {gen_x}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | dative
| {dat_x}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | accusative
| {acc_x_an}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | instrumental
| {ins_x}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | prepositional
| {pre_x}
]===] .. template_postlude()
templates["one_number_split_animacy"] = template_prelude("35") .. [===[
! style="width:15em;background:var(--wikt-palette-lightblue, #d9ebff);" colspan=2 |
! style="background:var(--wikt-palette-lightblue, #d9ebff);" | {number}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | nominative
| {nom_x}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | genitive
| {gen_x}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | dative
| {dat_x}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" rowspan=2 | accusative
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | animate
| {acc_x_an}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" | inanimate
| {acc_x_in}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | instrumental
| {ins_x}
|-
! style="background:var(--wikt-palette-lighterblue, #ebf4ff);" colspan=2 | prepositional
| {pre_x}
]===] .. template_postlude()
return export
qoxcg82wr5qccmaqka78c3s8w3aeixc
Modul:ru-nominal
828
12167
379769
109319
2026-10-20T08:11:34Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/86739030|86739030]])
379769
Scribunto
text/plain
--[[
This module holds functions shared between ru-noun and ru-adjective.
]]
local export = {}
local lang = require("Module:languages").getByCode("ru")
local m_links = require("Module:links")
local m_table = require("Module:table")
local com = require("Module:ru-common")
local m_ru_translit = require("Module:ru-translit")
local m_table_tools = require("Module:table tools")
local u = mw.ustring.char
local rfind = mw.ustring.find
local rsubn = mw.ustring.gsub
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local usub = mw.ustring.sub
local HYPMARKER = "⟐"
-- version of rsubn() that discards all but the first return value
local function rsub(term, foo, bar)
local retval = rsubn(term, foo, bar)
return retval
end
-- Insert an entry into an existing list if not already present, comparing the entry to items in the existing list
-- using a key function. If entry already found, combine it into the existing entry using combine_func, a function of
-- two arguments (the existing and new entries), which should return the combined entry. Return false if entry already
-- found, true if new entry inserted. If combine_func not specified, the existing entry is left alone. If combine_func
-- is specified, the return value will be written over the existing value (i.e. the existing list will be modified
-- in-place).
--
-- FIXME: General enough to consider moving to [[Module:table]].
local function insert_if_not_by_key(list, new_entry, keyfunc, combine_func)
local new_entry_key = keyfunc(new_entry)
for i, item in ipairs(list) do
local item_key = keyfunc(item)
if m_table.deepEquals(item_key, new_entry_key) then
if combine_func then
list[i] = combine_func(item, new_entry)
end
return false
end
end
table.insert(list, new_entry)
return true
end
--------------------------------------------------------------------------
-- Used for manual translit --
--------------------------------------------------------------------------
function export.combine_stem_and_suffix(stem, tr, suf, rules, old)
local first = usub(suf, 1, 1)
if rules then
local conv = rules[first]
if conv then
local ending = usub(suf, 2)
-- The following regexp is not quite the same as com.vowels. For one thing
-- it includes й, which is important. It leaves out ы, which may or may not
-- be important.
if old and conv == "и" and rfind(ending, "^́?[аеёиійоуэюяѣ]") then
conv = "і"
end
suf = conv .. ending
end
end
-- If <adj> is present in the suffix, it means we need to translate it
-- specially; do that now.
local is_adj = rfind(suf, "<adj>")
suf = rsub(suf, "<adj>", "")
local suftr = is_adj and m_ru_translit.tr_adj(suf, "mono")
return com.concat_russian_tr(stem, tr, suf, suftr, "dopair"), suf
end
--------------------------------------------------------------------------
-- Formatting forms for display --
--------------------------------------------------------------------------
-- Generate a string to substitute into a particular form in a Wiki-markup table. `forms` is the list of forms,
-- generated by concat_word_forms(). `is_lemma` is true if we're formatting the entry for use in displaying the lemma
-- in the declension table title. In this case, we don't include the translit, and remove monosyllabic accents from the
-- Cyrillic (but not in multiword expressions). `accel_form` is the form code to speicfy in the accelerator, e.g.
-- 'nom|m|s', or nil for no accelerator. `lemma_forms` is the list of {RU, TR} lemma forms for use in the accelerator,
-- or nil if `accel_form` is nil. `remove_monosyllabic_accents_lemma_only` indicates that monosyllabic accents should
-- be removed only in the lemma; otherwise we remove them from all forms. (FIXME: Rethink why we have this flag; we
-- should be consistent.)
function export.show_form(forms, is_lemma, accel_form, lemma_forms, remove_monosyllabic_accents_lemma_only)
local russianvals = {}
local latinvals = {}
local lemmavals = {}
-- First fetch the lemma forms and translit. If there are adjacent forms that have identical Russian including
-- stress but different translit (e.g. азербайджа́нец with translits 'azerbajdžánec' and 'azɛrbajdžánec'), we
-- combine the translits, comma-separating them. (This is necessary because there is currently only one tr=
-- field per term.) We don't do this when processing the forms below; instead we handle this in a different
-- and more general fashion (see below).
local lemmaru, lemmatr
if accel_form and lemma_forms and lemma_forms[1] ~= "-" then
lemma_forms = com.combine_translit_of_duplicate_forms(com.strip_notes_from_forms(lemma_forms))
for i, form in ipairs(lemma_forms) do
local ru, tr = unpack(lemma_forms[i])
ru, tr = com.remove_monosyllabic_accents(ru, tr)
lemma_forms[i] = {ru, tr}
end
lemmaru, lemmatr = com.unzip_forms(lemma_forms)
end
-- Accumulate separately the Russian and transliteration into RUSSIANVALS and LATINVALS, then concatenate each down
-- below. We need a fair amount of logic here:
-- (1) to separate out footnote symbols;
-- (2) to separate out the hypothetical marker (a footnote symbol but causes display of the Russian and translit
-- in a special font);
-- (3) to maybe remove monosyllabic accents;
-- (4) to deduplicate repeated forms.
-- We used to generate the display (HTML) as we went, but this prevented proper deduplication because the
-- accelerator classes included in the HTML were different for otherwise identical forms. (Specifically, if two
-- forms are the same in the Russian but different in the translit, the Russian will have different display forms
-- because the translit is included in the accelerator classes.) So what we do is accumulate, separately for the
-- Russian and translit, objects containing the entry (Russian or translit), the separated footnote symbols,
-- whether the entry is hypothetical, and (for Russian only) the corresponding translit(s), for accelerator
-- generation. As we accumulate, we duduplicate based only on comparing the entries of two objects. If we need to
-- deduplicate two objects, we also need to combine their footnotes and transliteration (in the latter case, by
-- comma-separating; we do this because there is only one tr= field for each term).
for _, form in ipairs(forms) do
local ru, tr = form[1], form[2]
local ruentry, runotes = m_table_tools.separate_notes(ru)
local trentry, trnotes
if tr then
trentry, trnotes = m_table_tools.separate_notes(tr)
trnotes = rsub(trnotes, HYPMARKER, "")
end
if (is_lemma or not remove_monosyllabic_accents_lemma_only) then
ruentry, trentry = com.remove_monosyllabic_accents(ruentry, trentry)
end
local ishyp = rfind(runotes, HYPMARKER)
if ishyp then
runotes = rsub(runotes, HYPMARKER, "")
end
local ruobj = {entry = ruentry, tr = {trentry or true}, ishyp = ishyp, notes = runotes}
if not trentry then
trentry = com.translit_no_links(ruentry)
end
if not trnotes then
trnotes = com.translit_no_links(runotes)
end
local trobj = {entry = trentry, ishyp = ishyp, notes = trnotes}
local function keyfunc(obj)
return obj.entry
end
local function combine_func_ru(obj1, obj2)
for _, tr in ipairs(obj2.tr) do
m_table.insertIfNot(obj1.tr, tr)
end
obj1.notes = obj1.notes .. obj2.notes
obj1.ishyp = obj1.ishyp or obj2.ishyp
return obj1
end
local function combine_func_tr(obj1, obj2)
obj1.notes = obj1.notes .. obj2.notes
obj1.ishyp = obj1.ishyp or obj2.ishyp
return obj1
end
if is_lemma then
-- m_table.insertIfNot(lemmavals, ruspan .. " (" .. trspan .. ")")
insert_if_not_by_key(lemmavals, ruobj, keyfunc, combine_func_ru)
else
insert_if_not_by_key(russianvals, ruobj, keyfunc, combine_func_ru)
insert_if_not_by_key(latinvals, trobj, keyfunc, combine_func_tr)
end
end
-- Now finally format each object and concatenate them together.
local function concatenate_ru(objs)
local is_missing = false
for i, obj in ipairs(objs) do
local accel = nil
if lemmaru then
local translit = nil
if #obj.tr == 1 and obj.tr[1] == true then
-- no translit
else
for j, tr in ipairs(obj.tr) do
if tr == true then
obj.tr[j] = com.translit_no_links(obj.entry)
end
end
translit = table.concat(obj.tr, ", ")
end
accel = {form = accel_form, translit = translit, lemma = lemmaru, lemma_translit = lemmatr}
end
if obj.entry == "-" and #forms == 1 then
objs[i] = "—"
is_missing = true
end
if obj.ishyp then
-- no accelerator for hypothetical forms
objs[i] = m_links.full_link({lang = lang, term = nil, alt = obj.entry, tr = "-"}, "hypothetical")
else
objs[i] = m_links.full_link({lang = lang, term = obj.entry, tr = "-", accel = accel})
end
objs[i] = objs[i] .. m_table_tools.superscript_notes(obj.notes)
end
return table.concat(objs, ", "), is_missing
end
local function concatenate_tr(objs)
local scriptutils = require("Module:script utilities")
for i, obj in ipairs(objs) do
local trspan = m_links.remove_links(obj.entry) .. m_table_tools.superscript_notes(obj.notes)
if obj.ishyp then
-- FIXME, in the old [[Module:ru-noun]] code, notes were omitted from hypothetical entries. Correct?
objs[i] = scriptutils.tag_text(trspan, lang, require("Module:scripts").getByCode("Latn"),
"hypothetical")
else
objs[i] = scriptutils.tag_translit(trspan, lang, "default", " style=\"color: var(--wikt-palette-grey-8,#888);\"")
end
end
return table.concat(objs, ", ")
end
if is_lemma then
local russian_span, is_missing = concatenate_ru(lemmavals)
return russian_span
else
local russian_span, is_missing = concatenate_ru(russianvals)
if is_missing then
return russian_span
end
local latin_span = concatenate_tr(latinvals)
return russian_span .. "<br />" .. latin_span
end
end
--------------------------------------------------------------------------
-- Sibilant/Velar/ц rules --
--------------------------------------------------------------------------
local stressed_sibilant_rules = {
["я"] = "а",
["ы"] = "и",
["ё"] = "о́",
["ю"] = "у",
}
local stressed_c_rules = {
["я"] = "а",
["ё"] = "о́",
["ю"] = "у",
}
local unstressed_sibilant_rules = {
["я"] = "а",
["ы"] = "и",
["о"] = "е",
["ю"] = "у",
}
local unstressed_c_rules = {
["я"] = "а",
["о"] = "е",
["ю"] = "у",
}
local velar_rules = {
["ы"] = "и",
}
export.stressed_rules = {
["ш"] = stressed_sibilant_rules,
["щ"] = stressed_sibilant_rules,
["ч"] = stressed_sibilant_rules,
["ж"] = stressed_sibilant_rules,
["ц"] = stressed_c_rules,
["к"] = velar_rules,
["г"] = velar_rules,
["х"] = velar_rules,
}
export.unstressed_rules = {
["ш"] = unstressed_sibilant_rules,
["щ"] = unstressed_sibilant_rules,
["ч"] = unstressed_sibilant_rules,
["ж"] = unstressed_sibilant_rules,
["ц"] = unstressed_c_rules,
["к"] = velar_rules,
["г"] = velar_rules,
["х"] = velar_rules,
}
export.nonsyllabic_suffixes = m_table.listToSet({"", "ъ", "ь", "й"})
export.sibilant_suffixes = m_table.listToSet({"ш", "щ", "ч", "ж"})
return export
1uawletg0fhb4bjt39sk8u6mg2vk01k
Modul:ru-adjective
828
12168
379771
109320
2026-10-20T08:14:28Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/92537941|92537941]])
379771
Scribunto
text/plain
--[=[
This module contains functions for creating inflection tables for Russian
adjectives.
Author: Benwing, rewritten from early version by Wikitiki89
Arguments:
1: lemma; nom sg, or just the stem if an explicit declension type is
given in arg 2
2: declension type (usually omitted to autodetect based on the lemma),
along with any short accent type and optional irregular short stem;
see below.
CASE_NUMGEN: Override a given form; see abbreviations below
suffix: any suffix to attach unchanged to the end of each form
notes: Notes to add to the end of the table
title: Override the title
shorttail: Footnote (e.g. *, 1, 2, etc.) to add to short forms if
there's more than one; automatically superscripted
shorttailall: Same as shorttail= but applies to all short forms even
if there's only one
CASE_NUMGEN_tail: Like shorttailall but only for a specific form
nofull: Short forms only
Case abbreviations:
nom: nominative
gen: genitive
dat: dative
acc: accusative
ins: instrumental
pre: prepositional
short: short form
Number/gender abbreviations:
m: masculine singular
n: neuter singular
f: feminine singular
p: plural
mp: masculine plural (old-style and special numeral tables only)
fp: masculine plural (special numeral tables only)
Animacy abbreviations:
an: animate
in: inanimate
Declension-type argument (arg 2):
Form is DECLSPEC or DECLSPEC,DECLSPEC,... where DECLSPEC is
one of the following:
DECLTYPE:SHORTACCENT:SHORTSTEM
DECLTYPE:SHORTACCENT
DECLTYPE
SHORTACCENT:SHORTSTEM
SHORTACCENT
(blank)
DECLTYPE should normally be omitted, and the declension autodetected
from the ending; or it should be ь, to indicate that an adjective
in -ий is of the possessive variety with an extra -ь- in most of
the endings. Alternatively, it can be an explicit declension type,
in which case the lemma field needs to be replaced with the bare
stem; the following are the possibilities:
ый ий ой ьий short mixed manual (new-style)
ый ій ьій short stressed-short mixed proper stressed-proper
ъ-short ъ-stressed-short ъ-mixed ъ-proper ъ-stressed-proper
manual (old-style, where ъ-* is a synonym for *, for any *)
SHORTACCENT is one of a a' b b' c c' c'' to auto-generate the
short forms with the specified accent pattern (following
Zaliznyak); if omitted, no short forms will be auto-generated.
SHORTACCENT can contain a * in it for "reducible" adjectives,
where the short masculine singular has an epenthetic vowel
in the final syllable. It can also contain (1) or (2) to
indicate special cases. Both are used in conjunction with
adjectives in -нный/-нний. Special case (1) causes the
short masculine singular to end in -н instead of -нн; special
case (2) causes all short forms to end this way.
SHORTSTEM, if present, is used as the short stem to base the
short forms off of, instead of the normal adjective long stem
(possibly with a final-syllable accent added in the case of
declension type -о́й).
TODO:
1. Figure out what the symbol X-inside-square (⊠) means, which seems to go with
all adjectives in -о́й with multi-syllabic stems. It may mean that the
masculine singular short form is missing. If this indeed a regular thing,
we need to implement it (and if it's regular but means something else,
we need to implement that, too). Also figure out what the other signs
on pages 68-76 etc. mean: -, X, ~, П₂, Р₂, diamond (♢), triangle (△; might
simply mean a misc. irregularity; explained on p. 61).
2. Mark irregular overridden forms with △ (esp. appropriate for short forms).
3. Should non-reducible adjectives in -нный and -нний default to special case
(1)?
4. In the case of a non-dereducible short masc sing of stress type b, we don't
currently move the stress to the last syllable. Should we?
5. FIXME: In decline(), we used to default acc_n to nom_n. Now we do that in
handle_forms_and_overrides(). Verify that this is more correct.
6. FIXME: Allow multiple heads, both to handle cases where two manual translits
exist (or rather, one automatic and one manual), and cases where two
stresses are possible, e.g. мину́вший or ми́нувший.
7. FIXME: Add code to generate the regular comparative. The rules are as
follows: If the stem doesn't end in к г х, add -ee with no stress change
if the short form is type a, else add -е́е (including type a'). If the stem
ends in к г х, turn the last consonant into ч ж ш, add -е, and place the
stress on the syllable preceding the ending. (E.g. дорого́й -> доро́же)
]=]--
local m_utilities = require("Module:utilities")
local m_links = require("Module:links")
local m_table = require("Module:table")
local com = require("Module:ru-common")
local nom = require("Module:ru-nominal")
local strutils = require("Module:string utilities")
local m_table_tools = require("Module:table tools")
local m_debug = require("Module:debug")
local export = {}
local lang = require("Module:languages").getByCode("ru")
----------------------------- Utility functions ------------------------
local rfind = mw.ustring.find
local rsubn = mw.ustring.gsub
local rmatch = mw.ustring.match
local rsplit = mw.text.split
local ulower = mw.ustring.lower
local usub = mw.ustring.sub
-- version of rsubn() that discards all but the first return value
local function rsub(term, foo, bar)
local retval = rsubn(term, foo, bar)
return retval
end
-- version of rsubn() that returns a 2nd argument boolean indicating whether
-- a substitution was made.
local function rsubb(term, foo, bar)
local retval, nsubs = rsubn(term, foo, bar)
return retval, nsubs > 0
end
-- Insert a single form (consisting of {RUSSIAN, TR}) or a list of such
-- forms into an existing list of such forms, adding NOTESYM (a string or nil)
-- to the new forms if not nil. Return whether an insertion was performed.
local function insert_forms_into_existing_forms(existing, newforms, notesym)
if type(newforms) ~= "table" then
newforms = {newforms}
end
local inserted = false
for _, item in ipairs(newforms) do
if not m_table.contains(existing, item) then
if notesym then
item = com.concat_paired_russian_tr(item, {notesym})
end
table.insert(existing, item)
inserted = true
end
end
return inserted
end
local function track(page)
m_debug.track("ru-adjective/" .. page)
return true
end
-- Fancy version of ine() (if-not-empty). Converts empty string to nil,
-- but also strips leading/trailing space and then single or double quotes,
-- to allow for embedded spaces.
local function ine(arg)
if not arg then return nil end
arg = rsub(arg, "^%s*(.-)%s*$", "%1")
if arg == "" then return nil end
local inside_quotes = rmatch(arg, '^"(.*)"$')
if inside_quotes then
return inside_quotes
end
inside_quotes = rmatch(arg, "^'(.*)'$")
if inside_quotes then
return inside_quotes
end
return arg
end
-------------------- Global declension/case/etc. variables -------------------
-- 'enable_categories' is a special hack for testing, which disables all
-- category insertion if false. Delete this as soon as we've verified the
-- working of the category code and created all the necessary categories.
local enable_categories = true
local declensions = {}
local declensions_old = {}
local internal_notes_table = {}
local internal_notes_table_old = {}
local internal_notes_genders = {}
local internal_notes_genders_old = {}
local short_declensions = {}
local short_declensions_old = {}
-- Formerly used for the ой-rare type, which has been removed; but keep
-- the code around in case we need it later.
local short_internal_notes_table = {}
local short_internal_notes_table_old = {}
local short_stress_patterns = {}
local long_cases = {
"nom_m", "nom_n", "nom_f", "nom_p",
"gen_m", "gen_f", "gen_p",
"dat_m", "dat_f", "dat_p",
"acc_m_an", "acc_m_in", "acc_p_an", "acc_p_in", "acc_f", "acc_n",
"ins_m", "ins_f", "ins_p",
"pre_m", "pre_f", "pre_p",
-- extra old long cases
"nom_mp",
"acc_mp_an", "acc_mp_in",
-- extra dva cases
"nom_fp",
"acc_fp_an", "acc_fp_in",
-- extra oba cases
"gen_mp", "gen_fp",
"dat_mp", "dat_fp",
"ins_mp", "ins_fp",
"pre_mp", "pre_fp",
-- extra cases for compounds of два
"acc_mp", "acc_fp",
}
-- Short cases and corresponding numbered arguments
local short_cases = {
"short_m", "short_n", "short_f", "short_p"
}
-- Create master list of all possible cases (actually case/number/gender pairs)
local all_cases = mw.clone(long_cases)
for _, case in ipairs(short_cases) do
m_table.insertIfNot(all_cases, case)
end
-- If enabled, compare this module with new version of module to make
-- sure all declensions are the same.
local test_new_ru_adjective_module = false
-- Forward references to functions
local tracking_code
local categorize
local detect_stem_and_accent_type
local construct_bare_and_short_stem
local decline
local canonicalize_override
local handle_forms_and_overrides
local decline_short
local make_table
--------------------------------------------------------------------------
-- Main code --
--------------------------------------------------------------------------
-- Implementation of main entry point
function export.do_generate_forms(args, old, manual)
local orig_args
if test_new_ru_adjective_module then
orig_args = mw.clone(args)
end
if args[3] or args[4] or args[5] or args[6] then
error("Numbered short forms no longer supported")
end
if args.shorttailall then
track("shorttailall")
end
local SUBPAGENAME = mw.loadData("Module:headword/data").pagename
args.forms = {}
args.categories = {}
old = old or args.old
args.old = old
args.suffix = com.split_russian_tr(args.suffix or "", "dopair")
args.internal_notes = {}
-- Superscript footnote marker at beginning of note, similarly to what's
-- done at end of forms.
if args.notes then
local notes, entry = m_table_tools.separate_and_superscript_initial_notes(args.notes)
args.notes = notes .. entry
end
local overall_short_forms_allowed
manual = manual or args[2] == "manual"
args.manual = manual
local decl_types = manual and "$" or args[2] or ""
local lemmas = manual and "-" or args[1] or SUBPAGENAME
local normal_short_classes, rare_short_classes, dated_short_classes
local saw_no_short
for _, lemma_and_tr in ipairs(rsplit(lemmas, ",")) do
-- reset these for each lemma so we get the short classes of the last
-- lemma (doesn't really matter, as they should be the same for all
-- lemmas)
normal_short_classes = {}
rare_short_classes = {}
dated_short_classes = {}
saw_no_short = false
for _, decl_type in ipairs(rsplit(decl_types, ",")) do
local lemma, lemmatr = com.split_russian_tr(lemma_and_tr)
-- if lemma ends with -ся, strip it and act as if suffix=ся given
-- (or rather, prepend ся to suffix)
local active_base = rmatch(lemma, "^(.*)ся$")
if active_base then
lemma = active_base
lemmatr = com.strip_tr_ending(lemmatr, "ся")
args.refl = true
args.real_suffix = com.concat_paired_russian_tr({"ся"}, args.suffix)
else
args.refl = false
args.real_suffix = args.suffix
end
-- Auto-detect actual decl type, and get short accent and overriding
-- short stem, if specified.
local stem, stemtr, short_accent, short_stem, short_stemtr, datedrare
stem, stemtr, decl_type, short_accent, short_stem, datedrare =
detect_stem_and_accent_type(lemma, lemmatr, decl_type, args)
if rfind(decl_type, "^[іи]й$") and rfind(stem, "[" .. com.velar .. com.sib .. "]$") then
decl_type = "ый"
end
if datedrare == "none" then
saw_no_short = true
end
local proper_decl = m_table.contains({"proper", "stressed-proper"}, decl_type)
-- Use args.real_surname to avoid overwriting args.surname, since we may be overwriting
-- `args` multiple times. FIXME: Use a separate data structure for this.
args.real_surname = proper_decl or args.surname
local short_title
if short_accent then
short_title = short_accent
elseif args.real_surname then
short_title = "surname"
elseif m_table.contains({"ьій", "ьий", "ьей", "short", "stressed-short", "й-short", "mixed"}, decl_type) then
short_title = "possessive"
end
if short_title then
if datedrare == "dated" then
m_table.insertIfNot(dated_short_classes, short_title)
elseif datedrare == "rare" then
m_table.insertIfNot(rare_short_classes, short_title)
else
m_table.insertIfNot(normal_short_classes, short_title)
end
end
if short_stem then
short_stem, short_stemtr = com.split_russian_tr(short_stem)
end
stem, args.allow_unaccented = rsubb(stem, "^%*", "")
if args.allow_unaccented then
track("allow-unaccented")
end
-- Treat suffixes without an accent, and suffixes with an accent on
-- the initial hyphen, as if they were preceded with a *, which
-- overrides all the logic that normally (a) normalizes the accent,
-- and (b) complains about multisyllabic words without an accent.
-- Don't do this if lemma is just -, which is used specially in
-- manual declension tables.
if lemma ~= "-" and (rfind(lemma, "^%-́") or (com.is_unstressed(lemma) and rfind(lemma, "^%-"))) then
args.allow_unaccented = true
end
if not args.allow_unaccented and com.needs_accents(lemma) then
-- Technically we don't need accents in -ой adjectives (which are
-- always ending-stressed) and in -ый adjectives with a monosyllabic
-- stem, such as полный (which are always stem-stressed), but it's
-- better to enforce accents in all multisyllabic words, for
-- consistency with nouns and verbs. Note that we still don't
-- require an accent in monosyllabic adjectives such as злой.
error("Lemma must have an accent in it: " .. lemma)
end
-- Set stem and unstressed version. Also construct end-accented version
-- of stem if unstressed; needed for short forms of adjectives of
-- type -о́й. We do this here before doing the dereduction
-- transformation so that we don't end up stressing an unstressed
-- epenthetic vowel, and so that the previous syllable instead ends
-- up stressed (in type -о́й adjectives the stem won't have any stress).
-- Note that the closest equivalent in nouns is handled in
-- attach_unstressed(), which puts the stress onto the final syllable
-- if the stress pattern calls for ending stress in the genitive
-- plural. This works there because
-- (1) It won't stress an unstressed epenthetic vowel because the
-- cases where the epenthetic vowel is unstressed are precisely
-- those with stem stress in the gen pl, not ending stress;
-- (2) There isn't a need to stress the syllable preceding an
-- unstressed epenthetic vowel because that syllable should
-- already have stress, since we require that the base stem form
-- (parameter 2) have stress in it whenever any case form has
-- stem stress. This isn't the case here in type -о́й adjectives.
-- NOTE: FIXME: I can't actually quote any forms from Zaliznyak
-- (at least not from pages 58-60, where this is discussed) that
-- seem to require last-stem-syllable-stress, i.e. where the stem is
-- multisyllabic. The two examples given are both unusual: дорого́й
-- has stress on the initial syllable до́рог etc. and these forms are
-- marked with a triangle (indicating an apparent irregularity); and
-- голубо́й seems not to have a masculine singular short form, and it's
-- accent pattern b, so the remaining forms are all ending-stressed.
-- In fact it's possible that these examples don't exist: It appears
-- that all the multisyllabic adjectives in -о́й listed in the
-- dictionary have a marking next to them consisting of an X inside of
-- a square, which is glossed p. 69 to something I don't understand,
-- but may be saying that the masculine singular short form is
-- missing. If this is regular, we need to implement it.
args.stem, args.stemtr = stem, stemtr
args.ustem, args.ustemtr = com.make_unstressed_once(stem, stemtr)
local accented_stem, accented_stemtr = stem, stemtr
if not args.allow_unaccented then
if accented_stemtr and com.is_unstressed(accented_stem) ~= com.is_unstressed(accented_stemtr) then
error("Stem " .. accented_stem .. " and translit " .. accented_stemtr .. " must have same accent pattern")
end
if com.is_unstressed(accented_stem) then
accented_stem, accented_stemtr =
com.make_ending_stressed(accented_stem, accented_stemtr)
end
end
local short_forms_allowed = manual and true or decl_type == "ый" or
decl_type == "ой" or decl_type == (old and "ій" or "ий")
overall_short_forms_allowed = overall_short_forms_allowed or
short_forms_allowed
if not short_forms_allowed then
-- FIXME: We might want to allow this in case we have a
-- reducible short, mixed or proper possessive adjective. But
-- in that case we need to reduce rather than dereduce to get
-- the stem.
if short_accent or short_stem then
error("Cannot specify short accent or short stem with declension type " .. decl_type .. ", as short forms aren't allowed")
end
if args.short_m or args.short_f or args.short_n or args.short_p then
error("Cannot specify explicit short forms with declension type " .. decl_type .. ", as short forms aren't allowed")
end
end
local orig_short_accent = short_accent
local short_decl_type
short_accent, short_decl_type = construct_bare_and_short_stem(args,
short_accent, short_stem, short_stemtr, accented_stem,
accented_stemtr, old, decl_type)
local decls = old and declensions_old or declensions
local short_decls = old and short_declensions_old or short_declensions
if not decls[decl_type] then
error("Unrecognized declension type " .. decl_type)
end
if short_accent == "" then
error("Short accent type cannot be blank, should be omitted or given")
end
if short_accent and not short_stress_patterns[short_accent] then
error("Unrecognized short accent type " .. short_accent)
end
tracking_code(decl_type, args, orig_short_accent, short_accent,
short_stem, datedrare, short_forms_allowed)
if not manual and enable_categories then
categorize(decl_type, args, orig_short_accent, short_accent,
short_stem)
end
decline(args, decls[decl_type], m_table.contains({"ой", "ьей", "й-short", "stressed-short", "stressed-proper"}, decl_type))
if short_forms_allowed and short_accent then
decline_short(args, short_decls[short_decl_type],
short_stress_patterns[short_accent], datedrare)
end
local intable = old and internal_notes_table_old or internal_notes_table
local shortintab = old and short_internal_notes_table_old or
short_internal_notes_table
local internal_note = intable[decl_type] or shortintab[short_decl_type]
if internal_note then
m_table.insertIfNot(args.internal_notes, internal_note)
end
end
end
local short_class_titles = {}
local function make_short_class_title(short_classes, datedrare)
local sct = table.concat(short_classes, ",") -- short class title
if sct == "" then
return
end
if not m_table.contains({"surname", "possessive"}, sct) then
-- Convert e.g. a*,a(1) into a*[(1)], either finally or followed by
-- comma.
sct = rsub(sct, "([abc]'*)%*,%1%*?(%([12]%))$", "%1*[%2]")
sct = rsub(sct, "([abc]'*)%*,%1%*?(%([12]%)),", "%1*[%2],")
-- Same for a(1),a*.
sct = rsub(sct, "([abc]'*)%*?(%([12]%)),%1%*$", "%1*[%2]")
sct = rsub(sct, "([abc]'*)%*?(%([12]%)),%1%*,", "%1*[%2],")
-- Convert (1), (2) to ①, ②.
sct = rsub(sct, "%(1%)", "①")
sct = rsub(sct, "%(2%)", "②")
-- Add a * before ①, ②, consistent with Zaliznyak.
sct = rsub(sct, "([abc]'*)([①②])", "%1*%2")
-- Convert ' to ′ and '' to ″, consistent with Zaliznyak.
sct = sct:gsub("''", "″"):gsub("'", "′")
if args.nofull then
sct = "short-form-only class " .. sct
else
sct = "short class " .. sct
end
end
if datedrare then
sct = datedrare .. " " .. sct
end
table.insert(short_class_titles, sct)
end
make_short_class_title(normal_short_classes, nil)
make_short_class_title(rare_short_classes, "rare")
make_short_class_title(dated_short_classes, "dated")
if #short_class_titles == 0 then
if saw_no_short then
args.short_class_title = "no short forms"
else
args.short_class_title = "unknown short forms"
end
else
args.short_class_title = table.concat(short_class_titles, " / ")
end
handle_forms_and_overrides(args, overall_short_forms_allowed)
-- Test code to compare existing module to new one.
if test_new_ru_adjective_module then
local m_new_ru_adjective = require("Module:User:Benwing2/ru-adjective")
local newargs = m_new_ru_adjective.do_generate_forms(orig_args, old, manual)
local difdecl = false
for _, case in ipairs(all_cases) do
local arg = args[case]
local newarg = newargs[case]
if not m_table.deepEquals(arg, newarg) then
-- Uncomment this to display the particular case and
-- differing forms.
--error(case .. " " .. (arg and com.concat_forms(arg) or "nil") .. " || " .. (newarg and com.concat_forms(newarg) or "nil"))
track("different-decl")
difdecl = true
end
break
end
if not difdecl then
track("same-decl")
end
end
return args
end
local function process_params(frame, include_form)
local boolean = {type = "boolean"}
local params = {
[1] = true,
[2] = true,
suffix = true,
title = true,
special = true,
shorttail = true,
shorttailall = true,
notes = true,
old = boolean,
noneuter = boolean,
nofull = boolean,
surname = boolean,
pronoun = boolean
}
if include_form then
params.form = {required = true}
end
for _, case in ipairs(all_cases) do
params[case] = true
params[case .. "_tail"] = true
params[case .. "_tailall"] = true
end
return require("Module:parameters").process(frame:getParent().args, params)
end
-- Implementation of main entry point
local function do_show(frame, old, manual)
local args = export.do_generate_forms(process_params(frame), old, manual)
return make_table(args) .. m_utilities.format_categories(args.categories, lang)
end
-- The main entry point for modern declension tables.
function export.show(frame)
return do_show(frame, false)
end
-- The main entry point for old declension tables.
function export.show_old(frame)
return do_show(frame, true)
end
-- The main entry point for manual declension tables.
function export.show_manual(frame)
return do_show(frame, false, "manual")
end
-- The main entry point for manual old declension tables.
function export.show_manual_old(frame)
return do_show(frame, true, "manual")
end
-- Entry point for use in Module:ru-noun.
function export.get_nominal_decl(decl, gender, old)
local d = old and declensions_old[decl] or declensions[decl]
local n = {}
if gender == "m" then
n.nom_sg = d.nom_m
n.gen_sg = d.gen_m
n.dat_sg = d.dat_m
n.ins_sg = d.ins_m
n.pre_sg = d.pre_m
elseif gender == "f" then
n.nom_sg = d.nom_f
n.gen_sg = d.gen_f
n.dat_sg = d.dat_f
n.acc_sg = d.acc_f
n.ins_sg = d.ins_f
n.pre_sg = d.pre_f
elseif gender == "n" then
n.nom_sg = d.nom_n
n.gen_sg = d.gen_m
n.dat_sg = d.dat_m
n.acc_sg = d.acc_n
n.ins_sg = d.ins_m
n.pre_sg = d.pre_m
else
assert(false, "Unrecognized gender: " .. gender)
end
n.nom_pl = d.nom_p
n.gen_pl = d.gen_p
n.dat_pl = d.dat_p
n.ins_pl = d.ins_p
n.pre_pl = d.pre_p
if gender == "m" and d.nom_mp then
n.nom_pl = d.nom_mp
end
local intable = old and internal_notes_table_old or internal_notes_table
local ingenders = old and internal_notes_genders_old or internal_notes_genders
-- FIXME, what if there are multiple internal notes? See comment in
-- do_generate_forms().
local internal_notes = ingenders[decl] and m_table.contains(ingenders[decl], gender) and intable[decl]
return n, internal_notes
end
local function get_form(forms)
local canon_forms = {}
for _, form in ipairs(forms) do
local ru, tr = form[1], form[2]
local ruentry = m_table_tools.separate_and_superscript_notes(ru)
local trentry, trnotes
if tr then
trentry, trnotes = m_table_tools.separate_and_superscript_notes(tr)
end
ruentry = m_links.remove_links(ruentry)
m_table.insertIfNot(canon_forms, {ruentry, trentry})
end
return com.concat_forms(canon_forms)
end
-- The entry point for 'ru-adj-forms' to generate all adjective forms.
function export.generate_forms(frame)
local args = export.do_generate_forms(process_params(frame), false)
local ins_text = {}
for _, case in ipairs(all_cases) do
if args[case] then
table.insert(ins_text, case .. "=" .. get_form(args[case]))
end
end
return table.concat(ins_text, "|")
end
-- The entry point for 'ru-adj-form' to generate a particular adjective form.
function export.generate_form(frame)
local args = process_params(frame, "include form")
local form = args.form
if not m_table.contains(all_cases, form) then
error("Unrecognized form " .. form)
end
args = export.do_generate_forms(args, false)
if not args[form] then
return ""
else
return get_form(args[form])
end
end
--------------------------------------------------------------------------
-- Tracking and categorization --
--------------------------------------------------------------------------
tracking_code = function(decl_class, args, orig_short_accent, short_accent,
short_stem, datedrare)
local hint_types = com.get_stem_trailing_letter_type(args.stem)
local function dotrack(prefix)
if prefix ~= "" then
track(prefix)
prefix = prefix .. "/"
end
track(prefix .. decl_class)
for _, hint_type in ipairs(hint_types) do
track(prefix .. hint_type)
track(prefix .. decl_class .. "/" .. hint_type)
end
end
dotrack("")
if args.short_m or args.short_f or args.short_n or args.short_p then
dotrack("short")
end
if orig_short_accent then
if rfind(orig_short_accent, "%*") then
dotrack("reducible")
dotrack("reducible/" .. short_accent)
end
if rfind(orig_short_accent, "%(1%)") then
dotrack("special-case-1")
dotrack("special-case-1/" .. short_accent)
end
if rfind(orig_short_accent, "%(2%)") then
dotrack("special-case-2")
dotrack("special-case-2/" .. short_accent)
end
end
if short_accent then
dotrack("short-accent/" .. short_accent)
if datedrare == "dated" then
dotrack("short-accent-dated")
dotrack("short-accent-dated/" .. short_accent)
elseif datedrare == "rare" then
dotrack("short-accent-rare")
dotrack("short-accent-rare/" .. short_accent)
end
elseif datedrare == "none" then
dotrack("short-accent/none")
else
dotrack("short-accent/unknown")
end
if short_stem then
dotrack("explicit-short-stem")
dotrack("explicit-short-stem/" .. short_accent)
end
for _, case in ipairs(all_cases) do
if args[case] then
track("irreg/" .. case)
-- questionable use: dotrack("irreg/" .. case)
end
end
end
-- Insert the category CAT (a string) into list CATEGORIES. String will
-- have "Russian " prepended and ~ substituted for the part of speech.
local function insert_category(categories, cat, plpos)
table.insert(categories, "Russian " .. rsub(cat, "~", plpos))
end
categorize = function(decl_type, args, orig_short_accent, short_accent,
short_stem)
local plpos = args.real_surname and "surnames" or args.pronoun and "pronouns" or "adjectives"
-- Insert category CAT into the list of categories in ARGS.
local function insert_cat(cat)
insert_category(args.categories, cat, plpos)
end
-- FIXME: For compatibility with old {{temp|ru-adj7}}, {{temp|ru-adj8}},
-- {{temp|ru-adj9}}; maybe there's a better way.
if m_table.contains({"ьій", "ьий", "ьей", "short", "stressed-short", "й-short", "mixed",
"proper", "stressed-proper"}, decl_type) then
insert_cat("possessive ~")
end
if not args.pronoun then
if m_table.contains({"ьій", "ьий", "ьей"}, decl_type) then
insert_cat("long possessive ~")
elseif m_table.contains({"short", "stressed-short", "й-short"}, decl_type) then
insert_cat("short possessive ~")
elseif m_table.contains({"mixed"}, decl_type) then
insert_cat("mixed possessive ~")
elseif m_table.contains({"proper", "stressed-proper"}, decl_type) then
-- Don't insert a category like [[:Category:Russian proper-name surnames]]. Instead, rely on
-- [[:Category:Russian possessive surnames]] generated above.
if not args.real_surname then
insert_cat("proper-name ~")
end
elseif decl_type == "$" then
insert_cat("indeclinable ~")
else
local hint_types = com.get_stem_trailing_letter_type(args.stem)
-- insert English version of Zaliznyak stem type
local stem_type =
m_table.contains(hint_types, "velar") and "velar-stem" or
m_table.contains(hint_types, "sibilant") and "sibilant-stem" or
m_table.contains(hint_types, "c") and "ц-stem" or
m_table.contains(hint_types, "i") and "i-stem" or
m_table.contains(hint_types, "vowel") and "vowel-stem" or
m_table.contains(hint_types, "soft-cons") and "vowel-stem" or
m_table.contains(hint_types, "palatal") and "vowel-stem" or
decl_type == "ий" and "soft-stem" or
"hard-stem"
if stem_type == "soft-stem" or stem_type == "vowel-stem" then
insert_cat(stem_type .. " ~")
else
insert_cat(stem_type .. " " .. (m_table.contains({"ой", "ьей", "й-short"}, decl_type) and "ending-stressed" or "stem-stressed") .. " ~")
end
end
if m_table.contains({"ой", "ьей", "й-short"}, decl_type) then
insert_cat("ending-stressed ~")
end
end
local short_forms_allowed = m_table.contains({"ый", "ой", "ій", "ий"}, decl_type)
if short_forms_allowed then
local override_m = args.short_m
local override_f = args.short_f
local override_n = args.short_n
local override_p = args.short_p
local has_short = short_accent or override_m or override_f or
override_n or override_p
local missing_short = override_m == "-" or
override_f == "-" or override_n == "-" or override_p == "-" or
not short_accent and (not override_m or not override_f or
not override_n or not override_p)
if has_short then
insert_cat("~ with short forms")
if missing_short then
insert_cat("~ with missing short forms")
end
end
if short_accent then
insert_cat("~ with short accent pattern " .. (short_accent:gsub("''", "″"):gsub("'", "′")))
end
if orig_short_accent then
if rfind(orig_short_accent, "%*") then
insert_cat("~ with reducible short stem")
end
if rfind(orig_short_accent, "%(1%)") then
insert_cat("~ with Zaliznyak short form special case 1")
end
if rfind(orig_short_accent, "%(2%)") then
insert_cat("~ with Zaliznyak short form special case 2")
end
end
if short_stem and short_stem ~= args.stem then
insert_cat("~ with irregular short stem")
end
end
for _, case in ipairs(all_cases) do
if args[case] and args[case] ~= "-" then
local engcase = rsub(case, "^([a-z]*)", {
nom="nominative", gen="genitive", dat="dative",
acc="accusative", ins="instrumental", pre="prepositional",
short="short",
})
engcase = rsub(engcase, "(_[a-z]*)$", {
_m=" masculine singular", _f=" feminine singular",
_n=" neuter singular", _p=" plural",
_mp=" masculine plural"
})
insert_cat("~ with irregular " .. engcase)
end
end
if args.nofull then
insert_cat("short-form-only ~")
end
end
--------------------------------------------------------------------------
-- Autodetection and stem munging --
--------------------------------------------------------------------------
-- Attempt to detect the type of the lemma based on its ending, separating
-- off the base and the ending; also extract the accent type for short
-- adjectives and optional short stem. DECL is the value passed in, and
-- might already specify the ending. Return five values: STEM, STEMTR, DECL,
-- SHORT_ACCENT (accent class of short adjective, or nil for no short
-- adjectives other than specified through overrides), SHORT_STEM (special
-- stem of short adjective, nil if same as long stem). The return value of
-- SHORT_STEM is taken directly from the argument and will include any
-- manual translit.
detect_stem_and_accent_type = function(lemma, tr, decl, args)
local datedrare = false
-- If it looks like a short decl type, canonicalize. [abc] for types
-- a, b, c'', etc.; [d] for dated-*; [r] for rare-*;
-- [-] for - (no short forms); [*(] for * and (1) and (2) modifiers,
-- which might precede the letter.
if rfind(decl, "^[abcdr*(-]") then
decl = ":" .. decl
end
local splitvals = rsplit(decl, ":")
if #splitvals > 3 then
error("Should be at most three colon-separated parts of a declension spec: " .. decl)
end
local short_accent, short_stem
decl, short_accent, short_stem = splitvals[1], splitvals[2], splitvals[3]
-- Check for dated variant of short accent
local dated_short = short_accent and rmatch(short_accent, "^dated%-(.*)$")
if dated_short then
datedrare = "dated"
short_accent = dated_short
else
-- Check for rare variant of short accent
local rare_short = short_accent and rmatch(short_accent, "^rare%-(.*)$")
if rare_short then
datedrare = "rare"
short_accent = rare_short
end
end
decl = ine(decl)
-- Resolve aliases
if decl then
decl = rsub(decl, "^ъ%-", "")
end
if short_accent == "-" then
short_accent = nil
datedrare = "none"
end
short_accent = ine(short_accent)
short_stem = ine(short_stem)
if short_stem and not short_accent then
error("With explicit short stem " .. short_stem .. ", must specify short accent")
end
local base, ending
-- The while loop appears to function solely as a way of allowing a
-- jump to the end of the loop with a 'break' statement. I don't think
-- the loop is ever run more than once.
while true do
if not decl or decl == "ь" then
base, ending = rmatch(lemma, "^(.*)([ыиіое]́?й)$")
if base then
if ending == "ий" and decl == "ь" then
ending = "ьий"
elseif ending == "ій" and decl == "ь" then
ending = "ьій"
elseif ending == "е́й" or (ending == "ей" and com.is_monosyllabic(lemma)) then
ending = "ье́й"
end
tr = com.strip_tr_ending(tr, ending)
-- -ий/-ій will be converted to -ый after velars and sibilants
-- by the caller
decl = com.make_unstressed(ending)
break
else
-- It appears that short, mixed and proper adjectives are always
-- either of the -ов/ев/ёв or -ин/ын types. The former type is
-- always (or almost always?) short, while the latter can be
-- either; apparently mixed is the more "modern" type of
-- declension, and short is "older". However, both -ов/ев/ёв or
-- -ин/ын are of type "proper" (similar to "short") when
-- capitalized.
--
-- NOTE: Following regexp is accented
base = rmatch(lemma, "^([" .. com.uppercase .. "].*[иы]́н)ъ?$")
if base then
decl = "stressed-proper"
break
end
base = rmatch(lemma, "^([" .. com.uppercase .. "].*[еёо]́?в)ъ?$")
if not base then
-- Following regexp is not stressed
base = rmatch(lemma, "^([" .. com.uppercase .. "].*[иы]н)ъ?$")
end
if base then
decl = "proper"
break
end
base = rmatch(lemma, "^(.*[еёо]́?в)ъ?$")
if base then
decl = "short"
break
end
base = rmatch(lemma, "(.*[иы]́н)ъ?$") --accented
if base then
decl = "stressed-short"
break
end
base = rmatch(lemma, "(.*[иы]н)ъ?$") --unaccented
if base then
decl = "mixed"
break
-- error("With -ин/ын adjectives, must specify 'short' or 'mixed':" .. lemma)
end
error("Cannot determine stem type of adjective: " .. lemma)
end
elseif m_table.contains({"short", "stressed-short", "mixed", "proper",
"stressed-proper"}, decl) then
base = rmatch(lemma, "^(.-)ъ?$")
assert(base)
break
elseif decl == "й" or decl == "й-short" then
base = rmatch(lemma, "^(.-)й$")
assert(base)
tr = com.strip_tr_ending(tr, "й")
decl = "й-short"
break
else
base = lemma
break
end
end
if not datedrare and (args.refl or not (decl == "ый" or decl == "ий" and not rfind(base, "[цс]к$"))) then
datedrare = "none"
end
return base, tr, decl, short_accent, short_stem, datedrare
end
-- Add a possible suffix to the bare stem, according to the declension and
-- value of OLD. This may be -ь, -ъ, -й or nothing. We need to do this here
-- because we don't actually attach such a suffix in attach_unstressed() due
-- to situations where we don't want the suffix added, e.g. бескра́йний with
-- dereduced nom sg бескра́ен without expected -ь.
local function add_bare_suffix(bare, baretr, old, decl, dereduced)
if decl == "й-short" then
return bare .. "й", baretr and baretr .. "j" or nil
elseif old and decl ~= "ій" and decl ~= "$" then
return bare .. "ъ", baretr
elseif decl == "ий" or decl == "ій" then
-- This next special case is mentioned in Zaliznyak's 1980 grammatical
-- dictionary for adjectives (footnote, p. 60).
if dereduced and rfind(bare, "[нН]$") then
-- FIXME: What happens in this case old-style? I assume that
-- -ъ is added, but this is a guess.
return bare .. (old and "ъ" or ""), baretr
elseif rfind(bare, "[" .. com.vowel .. "]́?$") then
-- This happens with adjectives like длинноше́ий, short masculine
-- singular длинноше́й.
return bare .. "й", baretr and baretr .. "j" or nil
else
return bare .. "ь", baretr and baretr .. "ʹ" or nil
end
else
return bare, baretr
end
end
-- Construct and set bare and short form in args, and canonicalize
-- short accent spec, handling cases *, (1) and (2). Return canonicalized
-- short accent and the short declension, which is usually the same as
-- the corresponding long one.
construct_bare_and_short_stem = function(args, short_accent, short_stem,
short_stemtr, accented_stem, accented_stemtr, old, decl)
-- Check if short forms allowed; if not, no short-form params can be given.
-- Construct bare version of stem; used for cases where the ending
-- is non-syllabic (i.e. short masculine singular of long adjectives,
-- and masculine singular of short, mixed and proper adjectives). Comes
-- from short masculine or 3rd argument if explicitly given, else from the
-- accented stem, possibly with the dereduction transformation applied
-- (if * occurs in the short accent spec).
local reducible, sc1, sc2
if short_accent then
short_accent, reducible = rsubb(short_accent, "%*", "")
short_accent, sc1 = rsubb(short_accent, "%(1%)", "")
short_accent, sc2 = rsubb(short_accent, "%(2%)", "")
end
if sc1 or sc2 then
-- Reducible isn't compatible with sc1 or sc2, but Zaliznyak's
-- dictionary always seems to notate sc1 and sc2 with reducible *,
-- so ignore it.
reducible = false
end
if sc1 and sc2 then
error("Special cases 1 and 2, i.e. (1) and (2), not compatible")
end
local short_decl = decl
-- Construct short stem. May be explicitly given, else comes from
-- end-accented stem.
if not short_stem then
short_stem, short_stemtr = accented_stem, accented_stemtr
end
-- Try to accent unaccented short stem (happens only when explicitly given),
-- but be conservative -- only if monosyllabic, since otherwise we have
-- no idea where stress should end up; after all, the explicit short stem
-- is for exceptional cases.
if not args.allow_unaccented then
if short_stemtr and com.is_unstressed(short_stem) ~= com.is_unstressed(short_stemtr) then
error("Explicit short stem " .. short_stem .. " and translit " .. short_stemtr .. " must have same accent pattern")
end
if com.is_monosyllabic(short_stem) then
short_stem, short_stemtr = com.make_ending_stressed(short_stem, short_stemtr)
elseif com.needs_accents(short_stem) then
error("Explicit short stem " .. short_stem .. " needs an accent")
end
end
if sc2 then
if not rfind(short_stem, "нн$") then
error("With special case 2, stem needs to end in -нн: " .. short_stem)
end
short_stem = rsub(short_stem, "нн$", "н")
if short_stemtr then
if not rfind(short_stemtr, "nn$") then
error("With special case 2, stem translit needs to end in -nn: " .. short_stemtr)
end
short_stemtr = rsub(short_stemtr, "nn$", "n")
end
end
-- Construct bare form, used for short masculine.
local bare, baretr
if reducible then
bare, baretr = com.dereduce_stem(short_stem, short_stemtr, rfind(short_accent, "^b"))
if not bare then
error("Unable to dereduce stem: " .. short_stem)
end
bare, baretr = add_bare_suffix(bare, baretr, old, decl, true)
else
bare, baretr = short_stem, short_stemtr
if sc1 then
if not rfind(bare, "нн$") then
error("With special case 1, stem needs to end in -нн: " .. bare)
end
bare = rsub(bare, "нн$", "н")
if baretr then
if not rfind(baretr, "nn$") then
error("With special case 1, stem translit needs to end in -nn: " .. baretr)
end
baretr = rsub(baretr, "nn$", "n")
end
end
-- With special case 1 or 2, we don't ever want -ь added, so treat
-- it like a reducible (that may be why these are marked as
-- reducible in Zaliznyak).
bare, baretr = add_bare_suffix(bare, baretr, old, decl, sc1 or sc2)
end
args.short_stem, args.short_stemtr = short_stem, short_stemtr
args.short_ustem, args.short_ustemtr = com.make_unstressed_once(short_stem, short_stemtr)
args.bare, args.baretr = bare, baretr
return short_accent, short_decl
end
--------------------------------------------------------------------------
-- Declensions --
--------------------------------------------------------------------------
declensions["ый"] = {
["nom_m"] = "ый",
["nom_n"] = "ое",
["nom_f"] = "ая",
["nom_p"] = "ые",
["gen_m"] = "ого",
["gen_f"] = "ой",
["gen_p"] = "ых",
["dat_m"] = "ому",
["dat_f"] = "ой",
["dat_p"] = "ым",
["acc_f"] = "ую",
["acc_n"] = "ое",
["ins_m"] = "ым",
["ins_f"] = {"ой", "ою"},
["ins_p"] = "ыми",
["pre_m"] = "ом",
["pre_f"] = "ой",
["pre_p"] = "ых",
}
declensions["ий"] = {
["nom_m"] = "ий",
["nom_n"] = "ее",
["nom_f"] = "яя",
["nom_p"] = "ие",
["gen_m"] = "его",
["gen_f"] = "ей",
["gen_p"] = "их",
["dat_m"] = "ему",
["dat_f"] = "ей",
["dat_p"] = "им",
["acc_f"] = "юю",
["acc_n"] = "ее",
["ins_m"] = "им",
["ins_f"] = {"ей", "ею"},
["ins_p"] = "ими",
["pre_m"] = "ем",
["pre_f"] = "ей",
["pre_p"] = "их",
}
declensions["ой"] = {
["nom_m"] = "о́й",
["nom_n"] = "о́е",
["nom_f"] = "а́я",
["nom_p"] = "ы́е",
["gen_m"] = "о́го",
["gen_f"] = "о́й",
["gen_p"] = "ы́х",
["dat_m"] = "о́му",
["dat_f"] = "о́й",
["dat_p"] = "ы́м",
["acc_f"] = "у́ю",
["acc_n"] = "о́е",
["ins_m"] = "ы́м",
["ins_f"] = {"о́й", "о́ю"},
["ins_p"] = "ы́ми",
["pre_m"] = "о́м",
["pre_f"] = "о́й",
["pre_p"] = "ы́х",
}
-- These can be stressed in ничья́ "draw, tie (in sports)".
declensions["ьий"] = {
["nom_m"] = "и́й",
["nom_n"] = "ье́",
["nom_f"] = "ья́",
["nom_p"] = "ьи́",
["gen_m"] = "ье́го",
["gen_f"] = "ье́й",
["gen_p"] = "ьи́х",
["dat_m"] = "ье́му",
["dat_f"] = "ье́й",
["dat_p"] = "ьи́м",
["acc_f"] = "ью́",
["acc_n"] = "ье́",
["ins_m"] = "ьи́м",
["ins_f"] = {"ье́й", "ье́ю"},
["ins_p"] = "ьи́ми",
["pre_m"] = "ье́м",
["pre_f"] = "ье́й",
["pre_p"] = "ьи́х",
}
declensions["ьей"] = {
["nom_m"] = "е́й",
["nom_n"] = "ьё",
["nom_f"] = "ья́",
["nom_p"] = "ьи́",
["gen_m"] = "ьего́",
["gen_f"] = "ье́й",
["gen_p"] = "ьи́х",
["dat_m"] = "ьему́",
["dat_f"] = "ье́й",
["dat_p"] = "ьи́м",
["acc_f"] = "ью́",
["acc_n"] = "ьё",
["ins_m"] = "ьи́м",
["ins_f"] = {"ье́й", "ье́ю"},
["ins_p"] = "ьи́ми",
["pre_m"] = "ьём",
["pre_f"] = "ье́й",
["pre_p"] = "ьи́х",
}
declensions["short"] = {
["nom_m"] = "",
["nom_n"] = "о́",
["nom_f"] = "а́",
["nom_p"] = "ы́",
["gen_m"] = "а́",
["gen_f"] = "о́й",
["gen_p"] = "ы́х",
["dat_m"] = "у́",
["dat_f"] = "о́й",
["dat_p"] = "ы́м",
["acc_f"] = "у́",
["acc_n"] = "о́",
["ins_m"] = "ы́м",
["ins_f"] = {"о́й", "о́ю"},
["ins_p"] = "ы́ми",
["pre_m"] = "о́м",
["pre_f"] = "о́й",
["pre_p"] = "ы́х",
}
declensions["stressed-short"] = mw.clone(declensions["short"])
declensions["stressed-short"]["pre_m"] = "е́"
declensions["й-short"] = {
["nom_m"] = "й",
["nom_n"] = "ё",
["nom_f"] = "я́",
["nom_p"] = "и́",
["gen_m"] = "его́",
["gen_f"] = "е́й",
["gen_p"] = "и́х",
["dat_m"] = "ему́",
["dat_f"] = "е́й",
["dat_p"] = "и́м",
["acc_f"] = "ю́",
["acc_n"] = "ё",
["ins_m"] = "и́м",
["ins_f"] = {"е́й", "е́ю"},
["ins_p"] = "и́ми",
["pre_m"] = "ём",
["pre_f"] = "е́й",
["pre_p"] = "и́х",
}
declensions["mixed"] = {
["nom_m"] = "",
["nom_n"] = "о",
["nom_f"] = "а",
["nom_p"] = "ы",
["gen_m"] = {"ого", "а2"},
["gen_f"] = "ой",
["gen_p"] = "ых",
["dat_m"] = {"ому", "у2"},
["dat_f"] = "ой",
["dat_p"] = "ым",
["acc_f"] = "у",
["acc_n"] = "о",
["ins_m"] = "ым",
["ins_f"] = {"ой", "ою"},
["ins_p"] = "ыми",
["pre_m"] = "ом",
["pre_f"] = "ой",
["pre_p"] = "ых",
}
internal_notes_table["mixed"] = "<sup>2</sup> Obsolete."
internal_notes_genders["mixed"] = {"m"}
internal_notes_table_old["mixed"] = "<sup>2</sup> Obsolete."
internal_notes_genders_old["mixed"] = {"m"}
declensions["proper"] = {
["nom_m"] = "",
["nom_n"] = nil,
["nom_f"] = "а́",
["nom_p"] = "ы́",
["gen_m"] = "а́",
["gen_f"] = "о́й",
["gen_p"] = "ы́х",
["dat_m"] = "у́",
["dat_f"] = "о́й",
["dat_p"] = "ы́м",
["acc_f"] = "у́",
["acc_n"] = nil,
["ins_m"] = "ы́м",
["ins_f"] = {"о́й", "о́ю1"},
["ins_p"] = "ы́ми",
["pre_m"] = "е́",
["pre_f"] = "о́й",
["pre_p"] = "ы́х",
}
declensions["stressed-proper"] = declensions["proper"]
for _, decl in ipairs({"proper", "stressed-proper"}) do
internal_notes_table[decl] = "<sup>1</sup> Rare."
internal_notes_table_old[decl] = "<sup>1</sup> Rare."
internal_notes_genders[decl] = {"f"}
internal_notes_genders_old[decl] = {"f"}
end
declensions["$"] = {
["nom_m"] = "",
["nom_n"] = "",
["nom_f"] = "",
["nom_p"] = "",
["gen_m"] = "",
["gen_f"] = "",
["gen_p"] = "",
["dat_m"] = "",
["dat_f"] = "",
["dat_p"] = "",
["acc_f"] = "",
-- don't do this; instead we default it to nom_n
-- ["acc_n"] = "",
["ins_m"] = "",
["ins_f"] = "",
["ins_p"] = "",
["pre_m"] = "",
["pre_f"] = "",
["pre_p"] = "",
-- for old-style templates, два, оба, compounds of два
["nom_mp"] = "",
["nom_fp"] = "",
["gen_mp"] = "",
["gen_fp"] = "",
["dat_mp"] = "",
["dat_fp"] = "",
-- don't do this; instead we default them to nom_mp, nom_fp
-- ["acc_mp"] = "",
-- ["acc_fp"] = "",
["ins_mp"] = "",
["ins_fp"] = "",
["pre_mp"] = "",
["pre_fp"] = "",
}
declensions_old["ый"] = {
["nom_m"] = "ый",
["nom_n"] = "ое",
["nom_f"] = "ая",
["nom_mp"] = "ые",
["nom_p"] = "ыя",
["gen_m"] = "аго",
["gen_f"] = "ой",
["gen_p"] = "ыхъ",
["dat_m"] = "ому",
["dat_f"] = "ой",
["dat_p"] = "ымъ",
["acc_f"] = "ую",
["acc_n"] = "ое",
["ins_m"] = "ымъ",
["ins_f"] = {"ой", "ою"},
["ins_p"] = "ыми",
["pre_m"] = "омъ",
["pre_f"] = "ой",
["pre_p"] = "ыхъ",
}
declensions_old["ій"] = {
["nom_m"] = "ій",
["nom_n"] = "ее",
["nom_f"] = "яя",
["nom_mp"] = "іе",
["nom_p"] = "ія",
["gen_m"] = "яго",
["gen_f"] = "ей",
["gen_p"] = "ихъ",
["dat_m"] = "ему",
["dat_f"] = "ей",
["dat_p"] = "имъ",
["acc_f"] = "юю",
["acc_n"] = "ее",
["ins_m"] = "имъ",
["ins_f"] = {"ей", "ею"},
["ins_p"] = "ими",
["pre_m"] = "емъ",
["pre_f"] = "ей",
["pre_p"] = "ихъ",
}
declensions_old["ой"] = {
["nom_m"] = "о́й",
["nom_n"] = "о́е",
["nom_f"] = "а́я",
["nom_mp"] = "ы́е",
["nom_p"] = "ы́я",
["gen_m"] = {"а́го", "о́го"},
["gen_f"] = "о́й",
["gen_p"] = "ы́хъ",
["dat_m"] = "о́му",
["dat_f"] = "о́й",
["dat_p"] = "ы́мъ",
["acc_f"] = "у́ю",
["acc_n"] = "о́е",
["ins_m"] = "ы́мъ",
["ins_f"] = {"о́й", "о́ю"},
["ins_p"] = "ы́ми",
["pre_m"] = "о́мъ",
["pre_f"] = "о́й",
["pre_p"] = "ы́хъ",
}
declensions_old["ьій"] = {
["nom_m"] = "ій",
["nom_n"] = "ье",
["nom_f"] = "ья",
["nom_p"] = "ьи",
["gen_m"] = "ьяго",
["gen_f"] = "ьей",
["gen_p"] = "ьихъ",
["dat_m"] = "ьему",
["dat_f"] = "ьей",
["dat_p"] = "ьимъ",
["acc_f"] = "ью",
["acc_n"] = "ье",
["ins_m"] = "ьимъ",
["ins_f"] = {"ьей", "ьею"},
["ins_p"] = "ьими",
["pre_m"] = "ьемъ",
["pre_f"] = "ьей",
["pre_p"] = "ьихъ",
}
declensions_old["ьей"] = {
["nom_m"] = "е́й",
["nom_n"] = "ьё",
["nom_f"] = "ья́",
["nom_p"] = "ьи́",
["gen_m"] = "ьего́",
["gen_f"] = "ье́й",
["gen_p"] = "ьи́хъ",
["dat_m"] = "ьему́",
["dat_f"] = "ье́й",
["dat_p"] = "ьи́мъ",
["acc_f"] = "ью́",
["acc_n"] = "ьё",
["ins_m"] = "ьи́мъ",
["ins_f"] = {"ье́й", "ье́ю"},
["ins_p"] = "ьи́ми",
["pre_m"] = "ьёмъ",
["pre_f"] = "ье́й",
["pre_p"] = "ьи́хъ",
}
declensions_old["short"] = {
["nom_m"] = "ъ",
["nom_n"] = "о",
["nom_f"] = "а",
["nom_p"] = "ы",
["gen_m"] = "а",
["gen_f"] = "ой",
["gen_p"] = "ыхъ",
["dat_m"] = "у",
["dat_f"] = "ой",
["dat_p"] = "ымъ",
["acc_f"] = "у",
["acc_n"] = "о",
["ins_m"] = "ымъ",
["ins_f"] = {"ой", "ою"},
["ins_p"] = "ыми",
["pre_m"] = "омъ",
["pre_f"] = "ой",
["pre_p"] = "ыхъ",
}
declensions_old["stressed-short"] = mw.clone(declensions_old["short"])
declensions_old["stressed-short"]["pre_m"] = "ѣ́"
declensions_old["й-short"] = {
["nom_m"] = "й",
["nom_n"] = "ё",
["nom_f"] = "я́",
["nom_p"] = "и́",
["gen_m"] = "его́",
["gen_f"] = {"е́й", "ея́"},
["gen_p"] = "и́хъ",
["dat_m"] = "ему́",
["dat_f"] = "е́й",
["dat_p"] = "и́мъ",
["acc_f"] = "ю́",
["acc_n"] = "ё",
["ins_m"] = "и́мъ",
["ins_f"] = {"е́й", "е́ю"},
["ins_p"] = "и́ми",
["pre_m"] = "ёмъ",
["pre_f"] = "е́й",
["pre_p"] = "и́хъ",
}
declensions_old["mixed"] = {
["nom_m"] = "ъ",
["nom_n"] = "о",
["nom_f"] = "а",
["nom_p"] = "ы",
["gen_m"] = {"аго", "а1"},
["gen_f"] = "ой",
["gen_p"] = "ыхъ",
["dat_m"] = {"ому", "у1"},
["dat_f"] = "ой",
["dat_p"] = "ымъ",
["acc_f"] = "у",
["acc_n"] = "о",
["ins_m"] = "ымъ",
["ins_f"] = {"ой", "ою"},
["ins_p"] = "ыми",
["pre_m"] = "омъ",
["pre_f"] = "ой",
["pre_p"] = "ыхъ",
}
declensions_old["proper"] = {
["nom_m"] = "ъ",
["nom_n"] = nil,
["nom_f"] = "а́",
["nom_p"] = "ы́",
["gen_m"] = "а́",
["gen_f"] = "о́й",
["gen_p"] = "ы́хъ",
["dat_m"] = "у́",
["dat_f"] = "о́й",
["dat_p"] = "ы́мъ",
["acc_f"] = "у́",
["acc_n"] = nil,
["ins_m"] = "ы́мъ",
["ins_f"] = {"о́й", "о́ю1"},
["ins_p"] = "ы́ми",
["pre_m"] = "ѣ́",
["pre_f"] = "о́й",
["pre_p"] = "ы́хъ",
}
declensions_old["stressed-proper"] = declensions_old["proper"]
declensions_old["$"] = declensions["$"]
local function frob_genitive_masc(decl)
-- signal to combine_stem_and_suffix() to use the special tr_adj()
-- function so that -го gets transliterated to -vo
if type(decl["gen_m"]) == "table" then
local entries = {}
for _, entry in ipairs(decl["gen_m"]) do
table.insert(entries, rsub(entry, "го$", "го<adj>"))
end
decl["gen_m"] = entries
else
decl["gen_m"] = rsub(decl["gen_m"], "го$", "го<adj>")
end
end
-- Frob declensions, adding <adj> to gen_m forms ending in -го. This is
-- a signal to add manual translit early on that renders -го as -vo.
for _, decl in pairs(declensions_old) do
frob_genitive_masc(decl)
end
for _, decl in pairs(declensions) do
frob_genitive_masc(decl)
end
--------------------------------------------------------------------------
-- Declension functions --
--------------------------------------------------------------------------
local function attach_unstressed(args, suf, short)
local stem, stemtr
if short then
stem, stemtr = args.short_stem, args.short_stemtr
else
stem, stemtr = args.stem, args.stemtr
end
if suf == nil then
return nil
elseif nom.nonsyllabic_suffixes[suf] then
if not args.bare then
return nil
elseif rfind(args.bare, "[йьъ]$") then
return {args.bare, args.baretr}
elseif suf == "ъ" then
return com.concat_russian_tr(args.bare, args.baretr, suf, nil, "dopair")
else
return {args.bare, args.baretr}
end
end
suf = com.make_unstressed(suf)
local rules = nom.unstressed_rules[ulower(usub(stem, -1))]
-- The parens around the return value drop all but the first return value
return (nom.combine_stem_and_suffix(stem, stemtr, suf, rules, args.old))
end
local function attach_stressed(args, suf, short)
local ustem, ustemtr
if short then
ustem, ustemtr = args.short_ustem, args.short_ustemtr
else
ustem, ustemtr = args.ustem, args.ustemtr
end
if suf == nil then
return nil
elseif not rfind(suf, "[ё́]") then -- if suf has no "ё" or accent marks
return attach_unstressed(args, suf, short)
end
local rules = nom.stressed_rules[ulower(usub(ustem, -1))]
-- The parens around the return value drop all but the first return value
return (nom.combine_stem_and_suffix(ustem, ustemtr, suf, rules, args.old))
end
local function attach_both(args, suf, short)
local results = {}
-- Examples with stems with ё on Zaliznyak p. 61 list the
-- ending-stressed forms first, so go with that.
-- NOTE: This assumes we get one value returned, not a list of such values.
table.insert(results, attach_stressed(args, suf, short))
table.insert(results, attach_unstressed(args, suf, short))
return results
end
local function attach_with(args, suf, fun, short)
if type(suf) == "table" then
local all_combineds = {}
for _, x in ipairs(suf) do
local combineds = attach_with(args, x, fun, short)
for _, combined in ipairs(combineds) do
table.insert(all_combineds, combined)
end
end
return all_combineds
else
local funval = fun(args, suf, short)
if funval then
local tbl = {}
assert(type(funval) == "table")
if type(funval[1]) ~= "table" then
funval = {funval}
end
for _, x in ipairs(funval) do
table.insert(tbl,
com.concat_paired_russian_tr(x, args.real_suffix))
end
return tbl
else
return {}
end
end
end
local function gen_form(args, decl, case, fun)
if not args.forms[case] then
args.forms[case] = {}
end
insert_forms_into_existing_forms(args.forms[case],
attach_with(args, decl[case], fun, false))
end
decline = function(args, decl, stressed)
local attacher = stressed and attach_stressed or attach_unstressed
for _, case in ipairs(long_cases) do
gen_form(args, decl, case, attacher)
end
end
canonicalize_override = function(args, case)
local val = args[case]
if not val then
return nil
end
val = rsplit(val, "%s*,%s*")
-- auto-accent/check for necessary accents
local newvals = {}
for _, v in ipairs(val) do
local ru, tr = com.split_russian_tr(v)
if not args.allow_unaccented then
if tr and com.is_unstressed(ru) ~= com.is_unstressed(tr) then
error("Override " .. ru .. " and translit " .. tr .. " must have same accent pattern")
end
-- it's safe to accent monosyllabic stems
if com.is_monosyllabic(ru) then
ru, tr = com.make_ending_stressed(ru, tr)
elseif com.needs_accents(ru) then
error("Override " .. ru .. " for case " .. case .. " requires an accent")
end
end
table.insert(newvals, {ru, tr})
end
val = newvals
return val
end
handle_forms_and_overrides = function(args, short_forms_allowed)
local f = args.forms
local function append_note_all(case, value)
value = com.split_russian_tr(value, "dopair")
if f[case] then
for i=1,#f[case] do
f[case][i] = com.concat_paired_russian_tr(f[case][i], value)
end
end
end
local function append_note_last(case, value, gt_one)
value = com.split_russian_tr(value, "dopair")
if f[case] then
local lastarg = #f[case]
if lastarg > (gt_one and 1 or 0) then
f[case][lastarg] = com.concat_paired_russian_tr(f[case][lastarg], value)
end
end
end
for _, case in ipairs(long_cases) do
if args[case .. "_tail"] then
append_note_last(case, args[case .. "_tail"])
end
if args[case .. "_tailall"] then
append_note_all(case, args[case .. "_tailall"])
end
args[case] = canonicalize_override(args, case) or f[case]
end
for _, case in ipairs(short_cases) do
if short_forms_allowed then
if args[case .. "_tail"] then
append_note_last(case, args[case .. "_tail"])
end
if args[case .. "_tailall"] then
append_note_all(case, args[case .. "_tailall"])
end
if args.shorttailall then
append_note_all(case, args.shorttailall)
end
if args.shorttail then
append_note_last(case, args.shorttail, ">1")
end
args[case] = canonicalize_override(args, case) or f[case]
else
args[case] = nil
end
end
-- Convert an empty list to nil, so that an mdash is inserted. This happens,
-- for example, with words like голубой where args.bare is set to nil.
for _, case in ipairs(all_cases) do
if args[case] and #args[case] == 0 then
args[case] = nil
end
end
-- default acc_n to nom_n; applies chiefly in the indeclinable declension
-- (used with manual declension tables)
if not args.acc_n then
args.acc_n = args.nom_n
end
-- default inanimate/animate accusative variants as appropriate; this is
-- almost always correct, but not with e.g. два́дцать оди́н, where the
-- masculine animate accusative два́дцать одного́ differs from both the
-- masculine nominative два́дцать оди́н and the masculine genitive
-- двадцати́ одного́.
if not args.acc_m_an then
args.acc_m_an = args.gen_m
end
if not args.acc_m_in then
args.acc_m_in = args.nom_m
end
-- Compounds of два do not have animacy; everything else does.
-- Only copy either the with-animacy or without-animacy variants to avoid
-- having both forms in {{ru-generate-adj-forms}}.
if args.special ~= "cdva" then
if not args.acc_p_an then
args.acc_p_an = args.gen_p
end
if not args.acc_p_in then
args.acc_p_in = args.nom_p
end
if not args.acc_mp_an then
args.acc_mp_an = args.gen_mp
end
if not args.acc_mp_in then
args.acc_mp_in = args.nom_mp
end
if not args.acc_fp_an then
args.acc_fp_an = args.gen_fp
end
if not args.acc_fp_in then
args.acc_fp_in = args.nom_fp
end
else
if not args.acc_mp then
args.acc_mp = args.nom_mp
end
if not args.acc_fp then
args.acc_fp = args.nom_fp
end
end
end
--------------------------------------------------------------------------
-- Short adjective declension --
--------------------------------------------------------------------------
short_declensions["ый"] = { m="", f="а́", n="о́", p="ы́" }
short_declensions["ой"] = short_declensions["ый"]
short_declensions["ий"] = { m="ь", f="я́", n="е́", p="и́" }
short_declensions_old["ый"] = { m="ъ", f="а́", n="о́", p="ы́" }
short_declensions_old["ой"] = short_declensions_old["ый"]
short_declensions_old["ій"] = short_declensions["ий"]
-- Short adjective stress patterns:
-- "-" = stem-stressed
-- "+" = ending-stressed (drawn onto the last syllable of stem in masculine)
-- "-+" = both possibilities
short_stress_patterns["a"] = { m="-", f="-", n="-", p="-" }
short_stress_patterns["a'"] = { m="-", f="-+", n="-", p="-" }
short_stress_patterns["b"] = { m="+", f="+", n="+", p="+" }
short_stress_patterns["b'"] = { m="+", f="+", n="+", p="-+" }
short_stress_patterns["c"] = { m="-", f="+", n="-", p="-" }
short_stress_patterns["c'"] = { m="-", f="+", n="-", p="-+" }
short_stress_patterns["c''"] = { m="-", f="+", n="-+", p="-+" }
local function gen_short_form(args, decl, case, fun, datedrare)
if not args.forms["short_" .. case] then
args.forms["short_" .. case] = {}
end
local inserted = insert_forms_into_existing_forms(
args.forms["short_" .. case], attach_with(args, decl[case], fun, true),
(datedrare == "dated" and datedrare == "rare") and "*" or nil)
if datedrare == "dated" and inserted then
m_table.insertIfNot(args.internal_notes, "<sup>*</sup> Dated.")
elseif datedrare == "rare" and inserted then
m_table.insertIfNot(args.internal_notes, "<sup>*</sup> Rare.")
end
end
local attachers = {
["+"] = attach_stressed,
["-"] = attach_unstressed,
["-+"] = attach_both,
}
decline_short = function(args, decl, stress_pattern, dated)
if stress_pattern then
for _, case in ipairs({"m", "f", "n", "p"}) do
gen_short_form(args, decl, case, attachers[stress_pattern[case]],
dated)
end
end
end
--------------------------------------------------------------------------
-- Create the table --
--------------------------------------------------------------------------
local title_temp = [=[Declension of <b lang="ru" class="Cyrl">{lemma}</b> ({short_class_title})]=]
local old_title_temp = [=[Pre-reform declension of <b lang="ru" class="Cyrl">{lemma}</b> ({short_class_title})]=]
local title_temp_no_short_msg = [=[Declension of <b lang="ru" class="Cyrl">{lemma}</b>]=]
local old_title_temp_no_short_msg = [=[Pre-reform declension of <b lang="ru" class="Cyrl">{lemma}</b>]=]
local template = nil
local full_clause = nil
local template_no_neuter = nil
local full_clause_no_neuter = nil
local surname_full_clause = nil
local template_mp = nil
local full_clause_mp = nil
local template_mp_no_neuter = nil
local full_clause_mp_no_neuter = nil
local surname_full_clause_mp = nil
local template_dva = nil
local full_clause_dva = nil
local full_clause_compound_dva = nil
local template_oba = nil
local full_clause_oba = nil
local short_clause_separator = nil
local short_clause = nil
local short_clause_no_neuter_separator = nil
local short_clause_no_neuter = nil
local short_clause_mp_separator = nil
local short_clause_mp = nil
local short_clause_mp_no_neuter_separator = nil
local short_clause_mp_no_neuter = nil
local internal_notes_template = nil
local notes_template = nil
local function get_accel_forms(old, special, has_nom_mp)
return {
-- used with all variants
nom_m = "nom|m|s",
nom_f = "nom|f|s",
nom_n = "nom|n|s",
-- not used with special; applies to all genders normally but only
-- feminine and neuter with old=1
nom_p = old and has_nom_mp and "nom|f//n|p" or "nom|p",
-- only used with old=1 or special; applies to the masculine and neuter if
-- special, but only masculine if old=1
nom_mp = special and "nom|m//n|p" or "nom|m|p",
-- only used with special
nom_fp = "nom|f|p",
-- the remaining singulars and non-gendered plurals used with all variants
-- except special == "oba"
gen_m = "gen|m//n|s",
gen_f = "gen|f|s",
gen_p = "gen|p",
dat_m = "dat|m//n|s",
dat_f = "dat|f|s",
dat_p = "dat|p",
acc_m_an = "an|acc|m|s",
acc_m_in = "in|acc|m|s",
acc_f = "acc|f|s",
acc_n = "acc|n|s",
-- the following two not used with special in ("dva", "oba"); applies to
-- all genders normally but only feminine and neuter with old=1
acc_p_an = old and has_nom_mp and "an|acc|f//n|p" or "an|acc|p",
acc_p_in = old and has_nom_mp and "in|acc|f//n|p" or "in|acc|p",
-- the following two only used with old=1 or special in ("dva|oba");
-- applies to the masculine and neuter if special, but only masculine if
-- old=1
acc_mp_an = special and "an|acc|m//n|p" or "an|acc|m|p",
acc_mp_in = special and "in|acc|m//n|p" or "in|acc|m|p",
-- the following two only used with special in ("dva", "oba")
acc_fp_an = "an|acc|f|p",
acc_fp_in = "in|acc|f|p",
-- the next 6 are used with all variants except special == "oba"
ins_m = "ins|m//n|s",
ins_f = "ins|f|s",
ins_p = "ins|p",
pre_m = "pre|m//n|s",
pre_f = "pre|f|s",
pre_p = "pre|p",
-- the following two gendered plurals are only used with special == "cdva"
acc_mp = "acc|m//n|p",
acc_fp = "acc|f|p",
-- the remaining gendered plurals are only used with special == "oba"
gen_mp = "gen|m//n|p",
gen_fp = "gen|f|p",
dat_mp = "dat|m//n|p",
dat_fp = "dat|f|p",
ins_mp = "ins|m//n|p",
ins_fp = "ins|f|p",
pre_mp = "pre|m//n|p",
pre_fp = "pre|f|p",
-- short forms
short_m = "short|m|s",
short_f = "short|f|s",
short_n = "short|n|s",
short_p = "short|p",
}
end
-- Make the table
make_table = function(args)
local lemma_forms = args.special and args.nom_mp or
args.nofull and args.short_m or args.nom_m
args.lemma = m_links.remove_links(nom.show_form(lemma_forms, true, nil, nil))
args.title = args.title or strutils.format(
(args.special or args.manual) and args.old and old_title_temp_no_short_msg or
(args.special or args.manual) and title_temp_no_short_msg or
args.old and old_title_temp or
title_temp,
args)
local has_nom_mp = args.nom_mp and not (#args.nom_mp == 1 and args.nom_mp[1][1] == "-")
local accel_forms = get_accel_forms(args.old, args.special, has_nom_mp)
for _, case in ipairs(all_cases) do
if args[case] then
local accel_form = accel_forms[case]
if not accel_form then
error("Internal error: Unrecognized case " .. case .. " when looking up accelerator form")
end
if args.noneuter or args.real_surname then
accel_form = rsub(accel_form, "//n", "")
end
if args.real_surname then
accel_form = rsub(accel_form, "an|", "")
end
args[case] = nom.show_form(args[case], false, accel_form, lemma_forms)
else
args[case] = nil
end
end
local temp, fullc
if args.special == "oba" then
temp = template_oba
fullc = full_clause_oba
elseif args.special == "dva" then
temp = template_dva
fullc = full_clause_dva
elseif args.special == "cdva" then
temp = template_dva -- no template_cdva
fullc = full_clause_compound_dva
elseif args.real_surname and args.old and has_nom_mp then
temp = template_mp_no_neuter
fullc = surname_full_clause_mp
elseif args.real_surname then
temp = template_no_neuter
fullc = surname_full_clause
elseif args.noneuter and args.old and has_nom_mp then
temp = template_mp_no_neuter
fullc = full_clause_mp_no_neuter
elseif args.noneuter then
temp = template_no_neuter
fullc = full_clause_no_neuter
elseif args.old and has_nom_mp then
temp = template_mp
fullc = full_clause_mp
else
temp = template
fullc = full_clause
end
if args.old then
if has_nom_mp then
if args.short_m or args.short_n or args.short_f or args.short_p then
args.short_m = args.short_m or "—"
args.short_n = args.short_n or "—"
args.short_f = args.short_f or "—"
args.short_p = args.short_p or "—"
args.shortsep = not args.nofull and (
args.noneuter and short_clause_mp_no_neuter_separator or short_clause_mp_separator
) or ""
args.short_clause = strutils.format(args.noneuter and short_clause_mp_no_neuter or short_clause_mp, args)
else
args.short_clause = ""
end
else
args.short_clause = ""
end
else
if args.short_m or args.short_n or args.short_f or args.short_p then
args.short_m = args.short_m or "—"
args.short_n = args.short_n or "—"
args.short_f = args.short_f or "—"
args.short_p = args.short_p or "—"
args.shortsep = not args.nofull and (
args.noneuter and short_clause_no_neuter_separator or short_clause_separator
) or ""
args.short_clause = strutils.format(args.noneuter and short_clause_no_neuter or short_clause, args)
else
args.short_clause = ""
end
end
if not args.nofull then
args.full_clause = strutils.format(fullc, args)
else
args.full_clause = ""
end
args.internal_notes = table.concat(args.internal_notes, "<br />")
args.internal_notes_clause = #args.internal_notes > 0 and strutils.format(internal_notes_template, args) or ""
args.notes_clause = args.notes and strutils.format(notes_template, args) or ""
return strutils.format(temp, args)
end
-- Used for new-style templates
short_clause_separator = [===[
! style="height:0.2em;background:var(--wikt-palette-lightblue)" colspan=6 |
|-
]===]
-- Used for new-style templates
short_clause = [===[
{shortsep}! style="background:var(--wikt-palette-lighterblue)" colspan=2 | short form
| data-accel-col=1 | {short_m}
| data-accel-col=3 | {short_n}
| data-accel-col=2 | {short_f}
| data-accel-col=4 | {short_p}]===]
-- Used for new-style templates
short_clause_no_neuter_separator = [===[
! style="height:0.2em;background:var(--wikt-palette-lightblue)" colspan=5 |
|-
]===]
-- Used for new-style templates
short_clause_no_neuter = [===[
{shortsep}! style="background:var(--wikt-palette-lighterblue)" colspan=2 | short form
| data-accel-col=1 | {short_m}
| data-accel-col=2 | {short_f}
| data-accel-col=4 | {short_p}]===]
-- Used for old-style templates
short_clause_mp_separator = [===[
! style="height:0.2em;background:var(--wikt-palette-lightblue)" colspan=7 |
|-
]===]
-- Used for old-style templates
short_clause_mp = [===[
{shortsep}! style="background:var(--wikt-palette-lighterblue)" colspan=2 | short form
| data-accel-col=1 | {short_m}
| data-accel-col=3 | {short_n}
| data-accel-col=2 | {short_f}
| data-accel-col=4 colspan=2 | {short_p}]===]
-- Used for old-style templates
short_clause_mp_no_neuter_separator = [===[
! style="height:0.2em;background:var(--wikt-palette-lightblue)" colspan=6 |
|-
]===]
-- Used for old-style templates
short_clause_mp_no_neuter = [===[
{shortsep}! style="background:var(--wikt-palette-lighterblue)" colspan=2 | short form
| data-accel-col=1 | {short_m}
| data-accel-col=2 | {short_f}
| data-accel-col=4 colspan=2 | {short_p}]===]
-- Used for both new-style and old-style templates
notes_template = [===[
<div style="width:100%;text-align:left">
<div style="display:inline-block;text-align:left;padding-left:1em;padding-right:1em">
{notes}
</div></div>
]===]
-- Used for both new-style and old-style templates
internal_notes_template = rsub(notes_template, "notes", "internal_notes")
local function template_prelude(min_width)
min_width = min_width or "70"
return rsub([===[
<div>
<div class="NavFrame">
<div class="NavHead" style="background:var(--wikt-palette-lighterblue)">{title}</div>
<div class="NavContent">
{\op}| style="background:var(--wikt-palette-paleblue);text-align:center" class="inflection-table"
|-
]===], "MINWIDTH", min_width)
end
local function template_postlude()
return [===[{full_clause}|-{short_clause}
|{\cl}{internal_notes_clause}{notes_clause}</div></div></div>]===]
end
-- Used for both new-style and old-style templates
template = template_prelude() .. [===[
! style="width:20%;background:var(--wikt-palette-lightblue)" colspan=2 |
! style="background:var(--wikt-palette-lightblue)" | masculine
! style="background:var(--wikt-palette-lightblue)" | neuter
! style="background:var(--wikt-palette-lightblue)" | feminine
! style="background:var(--wikt-palette-lightblue)" | plural
]===] .. template_postlude()
-- Used for both new-style and old-style templates
full_clause = [===[
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | nominative
| data-accel-col=1 | {nom_m}
| data-accel-col=3 | {nom_n}
| data-accel-col=2 | {nom_f}
| data-accel-col=4 | {nom_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | genitive
| data-accel-col=1 colspan=2 | {gen_m}
| data-accel-col=2 | {gen_f}
| data-accel-col=4 | {gen_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | dative
| data-accel-col=1 colspan=2 | {dat_m}
| data-accel-col=2 | {dat_f}
| data-accel-col=4 | {dat_p}
|-
! style="background:var(--wikt-palette-lighterblue)" rowspan=2 | accusative
! style="background:var(--wikt-palette-lighterblue)" | animate
| data-accel-col=1 | {acc_m_an}
| data-accel-col=3 rowspan=2 | {acc_n}
| data-accel-col=2 rowspan=2 | {acc_f}
| data-accel-col=4 | {acc_p_an}
|-
! style="background:var(--wikt-palette-lighterblue)" | inanimate
| data-accel-col=1 | {acc_m_in}
| data-accel-col=4 | {acc_p_in}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | instrumental
| data-accel-col=1 colspan=2 | {ins_m}
| data-accel-col=2 | {ins_f}
| data-accel-col=4 | {ins_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | prepositional
| data-accel-col=1 colspan=2 | {pre_m}
| data-accel-col=2 | {pre_f}
| data-accel-col=4 | {pre_p}
]===]
-- Used for both new-style and old-style templates
template_no_neuter = template_prelude("55") .. [===[
! style="width:20%;background:var(--wikt-palette-lightblue)" colspan=2 |
! style="background:var(--wikt-palette-lightblue)" | masculine
! style="background:var(--wikt-palette-lightblue)" | feminine
! style="background:var(--wikt-palette-lightblue)" | plural
]===] .. template_postlude()
-- Used for both new-style and old-style templates
full_clause_no_neuter = [===[
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | nominative
| data-accel-col=1 | {nom_m}
| data-accel-col=2 | {nom_f}
| data-accel-col=4 | {nom_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | genitive
| data-accel-col=1 | {gen_m}
| data-accel-col=2 | {gen_f}
| data-accel-col=4 | {gen_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | dative
| data-accel-col=1 | {dat_m}
| data-accel-col=2 | {dat_f}
| data-accel-col=4 | {dat_p}
|-
! style="background:var(--wikt-palette-lighterblue)" rowspan=2 | accusative
! style="background:var(--wikt-palette-lighterblue)" | animate
| data-accel-col=1 | {acc_m_an}
| data-accel-col=2 rowspan=2 | {acc_f}
| data-accel-col=4 | {acc_p_an}
|-
! style="background:var(--wikt-palette-lighterblue)" | inanimate
| data-accel-col=1 | {acc_m_in}
| data-accel-col=4 | {acc_p_in}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | instrumental
| data-accel-col=1 | {ins_m}
| data-accel-col=2 | {ins_f}
| data-accel-col=4 | {ins_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | prepositional
| data-accel-col=1 | {pre_m}
| data-accel-col=2 | {pre_f}
| data-accel-col=4 | {pre_p}
]===]
-- Used for both new-style and old-style templates
surname_full_clause = [===[
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | nominative
| data-accel-col=1 | {nom_m}
| data-accel-col=2 | {nom_f}
| data-accel-col=4 | {nom_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | genitive
| data-accel-col=1 | {gen_m}
| data-accel-col=2 | {gen_f}
| data-accel-col=4 | {gen_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | dative
| data-accel-col=1 | {dat_m}
| data-accel-col=2 | {dat_f}
| data-accel-col=4 | {dat_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | accusative
| data-accel-col=1 | {acc_m_an}
| data-accel-col=2 | {acc_f}
| data-accel-col=4 | {acc_p_an}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | instrumental
| data-accel-col=1 | {ins_m}
| data-accel-col=2 | {ins_f}
| data-accel-col=4 | {ins_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | prepositional
| data-accel-col=1 | {pre_m}
| data-accel-col=2 | {pre_f}
| data-accel-col=4 | {pre_p}
]===]
-- Used for old-style templates
template_mp = template_prelude() .. [===[
! style="width:20%;background:var(--wikt-palette-lightblue)" colspan=2 |
! style="background:var(--wikt-palette-lightblue)" | masculine
! style="background:var(--wikt-palette-lightblue)" | neuter
! style="background:var(--wikt-palette-lightblue)" | feminine
! style="background:var(--wikt-palette-lightblue)" | m. plural
! style="background:var(--wikt-palette-lightblue)" | n./f. plural
]===] .. template_postlude()
-- Used for old-style templates
full_clause_mp = [===[
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | nominative
| data-accel-col=1 | {nom_m}
| data-accel-col=3 | {nom_n}
| data-accel-col=2 | {nom_f}
| data-accel-col=4 | {nom_mp}
| data-accel-col=5 | {nom_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | genitive
| data-accel-col=1 colspan=2 | {gen_m}
| data-accel-col=2 | {gen_f}
| data-accel-col=4 colspan=2 | {gen_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | dative
| data-accel-col=1 colspan=2 | {dat_m}
| data-accel-col=2 | {dat_f}
| data-accel-col=4 colspan=2 | {dat_p}
|-
! style="background:var(--wikt-palette-lighterblue)" rowspan=2 | accusative
! style="background:var(--wikt-palette-lighterblue)" | animate
| data-accel-col=1 | {acc_m_an}
| data-accel-col=3 rowspan=2 | {acc_n}
| data-accel-col=2 rowspan=2 | {acc_f}
| data-accel-col=4 colspan=2 | {acc_p_an}
|-
! style="background:var(--wikt-palette-lighterblue)" | inanimate
| data-accel-col=1 | {acc_m_in}
| data-accel-col=4 | {acc_mp_in}
| data-accel-col=5 | {acc_p_in}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | instrumental
| data-accel-col=1 colspan=2 | {ins_m}
| data-accel-col=2 | {ins_f}
| data-accel-col=4 colspan=2 | {ins_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | prepositional
| data-accel-col=1 colspan=2 | {pre_m}
| data-accel-col=2 | {pre_f}
| data-accel-col=4 colspan=2 | {pre_p}
]===]
-- Used for old-style templates
template_mp_no_neuter = template_prelude("60") .. [===[
! style="width:20%;background:var(--wikt-palette-lightblue)" colspan=2 |
! style="background:var(--wikt-palette-lightblue)" | masculine
! style="background:var(--wikt-palette-lightblue)" | feminine
! style="background:var(--wikt-palette-lightblue)" | m. plural
! style="background:var(--wikt-palette-lightblue)" | f. plural
]===] .. template_postlude()
-- Used for old-style templates
full_clause_mp_no_neuter = [===[
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | nominative
| data-accel-col=1 | {nom_m}
| data-accel-col=2 | {nom_f}
| data-accel-col=4 | {nom_mp}
| data-accel-col=5 | {nom_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | genitive
| data-accel-col=1 | {gen_m}
| data-accel-col=2 | {gen_f}
| data-accel-col=4 colspan=2 | {gen_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | dative
| data-accel-col=1 | {dat_m}
| data-accel-col=2 | {dat_f}
| data-accel-col=4 colspan=2 | {dat_p}
|-
! style="background:var(--wikt-palette-lighterblue)" rowspan=2 | accusative
! style="background:var(--wikt-palette-lighterblue)" | animate
| data-accel-col=1 | {acc_m_an}
| data-accel-col=2 rowspan=2 | {acc_f}
| data-accel-col=4 colspan=2 | {acc_p_an}
|-
! style="background:var(--wikt-palette-lighterblue)" | inanimate
| data-accel-col=1 | {acc_m_in}
| data-accel-col=4 | {acc_mp_in}
| data-accel-col=5 | {acc_p_in}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | instrumental
| data-accel-col=1 | {ins_m}
| data-accel-col=2 | {ins_f}
| data-accel-col=4 colspan=2 | {ins_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | prepositional
| data-accel-col=1 | {pre_m}
| data-accel-col=2 | {pre_f}
| data-accel-col=4 colspan=2 | {pre_p}
]===]
-- Used for some old-style templates
surname_full_clause_mp = [===[
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | nominative
| data-accel-col=1 | {nom_m}
| data-accel-col=2 | {nom_f}
| data-accel-col=4 | {nom_mp}
| data-accel-col=5 | {nom_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | genitive
| data-accel-col=1 | {gen_m}
| data-accel-col=2 | {gen_f}
| data-accel-col=4 colspan=2 | {gen_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | dative
| data-accel-col=1 | {dat_m}
| data-accel-col=2 | {dat_f}
| data-accel-col=4 colspan=2 | {dat_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | accusative
| data-accel-col=1 | {acc_m_an}
| data-accel-col=2 | {acc_f}
| data-accel-col=4 colspan=2 | {acc_p_an}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | instrumental
| data-accel-col=1 | {ins_m}
| data-accel-col=2 | {ins_f}
| data-accel-col=4 colspan=2 | {ins_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | prepositional
| data-accel-col=1 | {pre_m}
| data-accel-col=2 | {pre_f}
| data-accel-col=4 colspan=2 | {pre_p}
]===]
-- Used for два and compounds
template_dva = template_prelude("55") .. [===[
! style="width:20%;background:var(--wikt-palette-lightblue)" colspan=2 |
! style="background:var(--wikt-palette-lightblue)" | masculine/neuter
! style="background:var(--wikt-palette-lightblue)" | feminine
]===] .. template_postlude()
-- Used for both new-style and old-style templates of два (only for два itself,
-- which has an animacy distinction; not for compounds)
full_clause_dva = [===[
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | nominative
| data-accel-col=4 | {nom_mp}
| data-accel-col=5 | {nom_fp}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | genitive
| data-accel-col=4 colspan=2 | {gen_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | dative
| data-accel-col=4 colspan=2 | {dat_p}
|-
! style="background:var(--wikt-palette-lighterblue)" rowspan=2 | accusative
! style="background:var(--wikt-palette-lighterblue)" | animate
| data-accel-col=4 colspan=2 | {acc_p_an}
|-
! style="background:var(--wikt-palette-lighterblue)" | inanimate
| data-accel-col=4 | {acc_mp_in}
| data-accel-col=5 | {acc_fp_in}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | instrumental
| data-accel-col=4 colspan=2 | {ins_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | prepositional
| data-accel-col=4 colspan=2 | {pre_p}
]===]
-- Used for both new-style and old-style templates of compounds of два
full_clause_compound_dva = [===[
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | nominative
| data-accel-col=4 | {nom_mp}
| data-accel-col=5 | {nom_fp}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | genitive
| data-accel-col=4 colspan=2 | {gen_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | dative
| data-accel-col=4 colspan=2 | {dat_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | accusative
| data-accel-col=4 | {acc_mp}
| data-accel-col=5 | {acc_fp}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | instrumental
| data-accel-col=4 colspan=2 | {ins_p}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | prepositional
| data-accel-col=4 colspan=2 | {pre_p}
]===]
-- Used for оба
template_oba = template_prelude() .. [===[
! style="width:20%;background:var(--wikt-palette-lightblue)" colspan=2 |
! style="background:var(--wikt-palette-lightblue)" | masculine
! style="background:var(--wikt-palette-lightblue)" | neuter
! style="background:var(--wikt-palette-lightblue)" | feminine
! style="background:var(--wikt-palette-lightblue)" | m./n. plural
! style="background:var(--wikt-palette-lightblue)" | f. plural
]===] .. template_postlude()
-- Used for both new-style and old-style templates of оба
full_clause_oba = [===[
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | nominative
| data-accel-col=1 | {nom_m}
| data-accel-col=3 | {nom_n}
| data-accel-col=2 | {nom_f}
| data-accel-col=4 | {nom_mp}
| data-accel-col=5 | {nom_fp}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | genitive
| data-accel-col=1 colspan=2 | {gen_m}
| data-accel-col=2 | {gen_f}
| data-accel-col=4 | {gen_mp}
| data-accel-col=5 | {gen_fp}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | dative
| data-accel-col=1 colspan=2 | {dat_m}
| data-accel-col=2 | {dat_f}
| data-accel-col=4 | {dat_mp}
| data-accel-col=5 | {dat_fp}
|-
! style="background:var(--wikt-palette-lighterblue)" rowspan=2 | accusative
! style="background:var(--wikt-palette-lighterblue)" | animate
| data-accel-col=1 | {acc_m_an}
| data-accel-col=3 rowspan=2 | {acc_n}
| data-accel-col=2 rowspan=2 | {acc_f}
| data-accel-col=4 | {acc_mp_an}
| data-accel-col=5 | {acc_fp_an}
|-
! style="background:var(--wikt-palette-lighterblue)" | inanimate
| data-accel-col=1 | {acc_m_in}
| data-accel-col=4 | {acc_mp_in}
| data-accel-col=5 | {acc_fp_in}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | instrumental
| data-accel-col=1 colspan=2 | {ins_m}
| data-accel-col=2 | {ins_f}
| data-accel-col=4 | {ins_mp}
| data-accel-col=5 | {ins_fp}
|-
! style="background:var(--wikt-palette-lighterblue)" colspan=2 | prepositional
| data-accel-col=1 colspan=2 | {pre_m}
| data-accel-col=2 | {pre_f}
| data-accel-col=4 | {pre_mp}
| data-accel-col=5 | {pre_fp}
]===]
return export
933luc3r5b2m39cxsyuvgzvif7j6xne
Templat:ru-noun+
10
12170
379772
109322
2026-10-20T08:16:36Z
Hakimi97
2668
Mengemas kini mengikut templat terkini
379772
wikitext
text/x-wiki
{{#invoke:ru-headword|noun_plus|nouns}}<!--
--><noinclude>{{documentation}}</noinclude>
inxq7ymjgstasdgt83t9m9py5ba26qe
Templat:kk-regional
10
14134
379736
112986
2026-10-20T00:09:21Z
Hakimi97
2668
Kemas kini kod templat
379736
wikitext
text/x-wiki
{{kk-alt
|Cyrl={{{1|{{PAGENAME}}}}}
|Arab={{#if:{{{2|}}}|{{{2|}}}|}}
|Latn={{#if:{{{3|}}}|{{{3|}}}|}}}}<noinclude>{{tcat|orthvar}}</noinclude>
ilg2fm2gx7txw0e8nw2rbh0y0cd53m9
teman
0
21052
379740
341275
2026-10-20T00:24:13Z
Hakimi97
2668
Kandungan dipindahkan ke [[Wikikamus:ms/teman]]
379740
wikitext
text/x-wiki
==Bahasa Melayu==
{{:wt:ms/{{FULLPAGENAME}}}}
9ssdqjzbgxl4j8j4xcc8ir7t4eihyyv
Modul:ar-verb
828
22367
379721
375809
2026-10-19T23:33:20Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/93746856|93746856]])
379721
Scribunto
text/plain
local export = {}
--[=[
This module implements {{ar-conj}} and provides the underlying conjugation functions for {{ar-verb}}
(whose actual formatting is done in [[Module:ar-headword]]).
Author: User:Benwing, from an early version (2013-2014) by User:Atitarev, User:ZxxZxxZ.
]=]
--[=[
TERMINOLOGY:
-- "slot" = A particular combination of tense/mood/person/number/etc.
Example slot names for verbs are "past_1s" (past tense first-person singular), "juss_pass_3fp" (non-past jussive
passive third-person feminine plural) "ap" (active participle). Each slot is filled with zero or more forms.
-- "form" = The conjugated Arabic form representing the value of a given slot.
-- "lemma" = The dictionary form of a given Arabic term. For Arabic, normally the third person masculine singular past,
although other forms may be used if this form is missing (e.g. in passive-only verbs or verbs lacking the past).
]=]
--[=[
FIXME:
1. Finish unimplemented conjugation types. Only IX-final-weak left (extremely rare, possibly only one verb اِعْمَايَ
(according to Haywood and Nahmad p. 244, who are very specific about the irregular occurrence of alif + yā instead
of expected اِعْمَيَّ with doubled yā). Not in Hans Wehr. NOTE: Not true about this, cf. form IX اِرْعَوَى "to desist,
to repent, to see the light". Also note form XII اِخْضَوْضَرَ = form IX اِخْضَرَّ "to be or become green".
[DONE except for اِعْمَايَ]
2. Implement irregular verbs as special cases and recognize them, e.g.
-- laysa "to not be"; only exists in the past tense, no non-past, no imperative, no participles, no passive, no
verbal noun. Irregular alternation las-/lays-. [IMPLEMENTABLE USING OVERRIDES]
-- istaḥā yastaḥī "be ashamed of" -- this is complex according to Hans Wehr because there are two verbs, regular
istaḥyā yastaḥyī "to spare (someone)'s life" and irregular istaḥyā yastaḥyī "to be ashamed to face (someone)",
which is irregular because it has the alternate irregular form istaḥā yastaḥī which only applies to this meaning.
Currently we follow Haywood and Nahmad in saying that both varieties can be spelled istaḥyā/istaḥā/istaḥḥā, but we
should instead use a variant= param similar to حَيَّ to distinguish the two possibilities, and maybe not include
istaḥḥā.
-- ʿayya/ʿayiya yaʿayyu/yaʿyā "to not find the right way, be incapable of, stammer, falter, fall ill". This appears
to be a mixture of a geminate and final-weak verb. Unclear what the whole paradigm looks like. Do the
consonant-ending parts in the past follow the final-weak paradigm? Is it the same in the non-past? Or can you
conjugate the non-past fully as either geminate or final-weak?
-- اِنْمَحَى inmaḥā or يمَّحَى immaḥā "to be effaced, obliterated; to disappear, vanish" has irregular assimilation of inm-
to imm- as an alternative. inmalasa "to become smooth; to glide; to slip away; to escape" also has immalasa as an
alternative. The only other form VII verbs in Hans Wehr beginning with -m- are inmalaḵa "to be pulled out, torn
out, wrenched" and inmāʿa "to be melted, to melt, to dissolve", which are not listed with imm- alternatives, but
might have them; if so, we should handle this generally. [DONE]
-- يَرَعَ yaraʕa yariʕu "to be a coward, to be chickenhearted" as an alternative form of يَرِعَ yariʕa yayraʕu (as given in
Wehr). [IMPLEMENTABLE USING OVERRIDES]
3. Implement individual override parameters for each paradigm part. See Module:fro-verb for an example of how to do this
generally. Note that {{temp|ar-conj-I}} and other of the older templates already had such individual override params.
[DONE]
Irregular verbs already implemented:
-- [ḥayya/ḥayiya yaḥyā "live" -- behaves like a normal final-weak verb
(e.g. past first singular ḥayītu) except in the past-tense parts with
vowel-initial endings (all the third person except for the third feminine
plural). The normal singular and dual endings have -yiya- in them, which
compresses to -yya-, with the normal endings the less preferred ones.
In masculine third plural, expected ḥayū is replaced by ḥayyū by
analogy to the -yy- parts, and the regular form is not given as an
alternant in John Mace. Barron's 201 verbs appears to have the regular
ḥayū as the part, however. Note also that final -yā appears with tall
alif. This appears to be a spelling convention of Arabic, also applying
in ḥayyā (form II, "to keep (someone) alive") and 'aḥyā (form IV,
"to animate, revive, give birth to, give new life to").] -- implemented
-- [ittaxadha yattaxidhu "take"] -- implemented
-- [sa'ala yas'alu "ask" with alternative jussive/imperative yasal/sal] -- implemented
-- [ra'ā yarā "see"] -- implemented
-- ['arā yurī "show"] -- implemented
-- ['akala ya'kulu "eat" with imperative kul] -- implemented
-- ['axadha ya'xudhu "take" with imperative xudh] -- implemented
-- ['amara ya'muru "order" with imperative mur] -- implemented
--]=]
local force_cat = false -- set to true for debugging
-- if true, always maintain manual translit during processing, and compare against full translit at the end
local debug_translit = false
local lang = require("Module:languages").getByCode("ar")
local m_links = require("Module:links")
local m_string_utilities = require("Module:string utilities")
local m_table = require("Module:table")
local ar_utilities = require("Module:ar-utilities")
local decorations_module = "Module:decorations"
local iut = require("Module:inflection utilities")
local put = require("Module:parse utilities")
local m_inflection_table = require("Module:inflection-table")
local concat = table.concat
local list_to_text = mw.text.listToText
local rfind = m_string_utilities.find
local rsubn = m_string_utilities.gsub
local rmatch = m_string_utilities.match
local rsplit = m_string_utilities.split
local usub = m_string_utilities.sub
local ulen = m_string_utilities.len
local u = m_string_utilities.char
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local dump = mw.dumpObject
-- Within this module, conjugations are the functions that do the actual
-- conjugating by creating the parts of a basic verb.
-- They are defined further down.
local conjugations = {}
-- hamza variants
local HAMZA = u(0x0621) -- hamza on the line (stand-alone hamza) = ء
local HAMZA_ON_ALIF = u(0x0623)
local HAMZA_ON_W = u(0x0624)
local HAMZA_UNDER_ALIF = u(0x0625)
local HAMZA_ON_Y = u(0x0626)
local HAMZA_ANY = "[" .. HAMZA .. HAMZA_ON_ALIF .. HAMZA_UNDER_ALIF .. HAMZA_ON_W .. HAMZA_ON_Y .. "]"
local HAMZA_PH = u(0xFFF0) -- hamza placeholder
local BAD = u(0xFFF1)
local BORDER = u(0xFFF2)
-- diacritics
local A = u(0x064E) -- fatḥa
local AN = u(0x064B) -- fatḥatān (fatḥa tanwīn)
local U = u(0x064F) -- ḍamma
local UN = u(0x064C) -- ḍammatān (ḍamma tanwīn)
local I = u(0x0650) -- kasra
local IN = u(0x064D) -- kasratān (kasra tanwīn)
local SK = u(0x0652) -- sukūn = no vowel
local SH = u(0x0651) -- šadda = gemination of consonants
local DAGGER_ALIF = u(0x0670)
local DIACRITIC_ANY_BUT_SH = "[" .. A .. I .. U .. AN .. IN .. UN .. SK .. DAGGER_ALIF .. "]"
-- Pattern matching short vowels
local AIU = "[" .. A .. I .. U .. "]"
-- Pattern matching short vowels or sukūn
local AIUSK = "[" .. A .. I .. U .. SK .. "]"
-- Pattern matching any diacritics that may be on a consonant
local DIACRITIC = SH .. "?" .. DIACRITIC_ANY_BUT_SH
-- translit_patterns
local vowels = "aeiouāēīōū"
local NV = "[^" .. vowels .. "]"
local dia = {a = A, i = I, u = U}
local undia = {[A] = "a", [I] = "i", [U] = "u", ["-"] = "-"}
-- various letters and signs
local ALIF = u(0x0627) -- ʾalif = ا
local AMAQ = u(0x0649) -- ʾalif maqṣūra = ى
local AMAD = u(0x0622) -- ʾalif madda = آ
local TAM = u(0x0629) -- tāʾ marbūṭa = ة
local T = u(0x062A) -- tāʾ = ت
local HYPHEN = u(0x0640)
local N = u(0x0646) -- nūn = ن
local W = u(0x0648) -- wāw = و
local Y = u(0x064A) -- yāʾ = ي
local S = "س"
local M = "م"
local LRM = u(0x200e) -- left-to-right mark
-- common combinations
local AH = A .. TAM
local AT = A .. T
local AA = A .. ALIF
local AAMAQ = A .. AMAQ
local AAH = AA .. TAM
local AAT = AA .. T
local II = I .. Y
local UU = U .. W
local AY = A .. Y
local AW = A .. W
local AYSK = AY .. SK
local AWSK = AW .. SK
local NA = N .. A
local NI = N .. I
local AAN = AA .. N
local AANI = AA .. NI
local AYNI = AYSK .. NI
local AWNA = AWSK .. NA
local AYNA = AYSK .. NA
local AYAAT = AY .. AAT
local UNU = "[" .. UN .. U .. "]"
local MA = M .. A
local MU = M .. U
local TA = T .. A
local TU = T .. U
local _I = ALIF .. I
local _U = ALIF .. U
local translit_cache = {
-- hamza variants
[HAMZA] = "ʔ",
[HAMZA_ON_ALIF] = "ʔ",
[HAMZA_ON_W] = "ʔ",
[HAMZA_UNDER_ALIF] = "ʔ",
[HAMZA_ON_Y] = "ʔ",
[HAMZA_PH] = "ʔ",
-- diacritics
[A] = "a",
[AN] = "an",
[U] = "u",
[UN] = "un",
[I] = "i",
[IN] = "in",
[SK] = "",
[SH] = "*", -- handled specially
[DAGGER_ALIF] = "ā",
-- various letters and signs
[""] = "",
[ALIF] = BAD, -- we should never be transliterating ALIF by itself, as its translit in isolation is ambiguous
[AMAQ] = BAD,
[AMAD] = "ʔā",
[TAM] = "",
[T] = "t",
[N] = "n",
[W] = "w",
[Y] = "y",
[S] = "s",
[M] = "m",
[LRM] = "",
-- common combinations
[AH] = "a",
[AT] = "at",
[AA] = "ā",
[AAMAQ] = "ā",
[AAH] = "āh",
[AAT] = "āt",
[II] = "ī",
[UU] = "ū",
[AY] = "ay",
[AW] = "aw",
[AYSK] = "ay",
[AWSK] = "aw",
[NA] = "na",
[NI] = "ni",
[AAN] = "ān",
[AANI] = "āni",
[AYNI] = "ayni",
[AWNA] = "awna",
[AYNA] = "ayna",
[AYAAT] = "ayāt",
[MA] = "ma",
[MU] = "mu",
[TA] = "ta",
[TU] = "tu",
[_I] = "i",
[_U] = "u",
}
local function transliterate(text)
local cached = translit_cache[text]
if cached then
if cached == BAD then
error(("Internal error: Unable to transliterate %s because explicitly marked as BAD"):format(text))
end
return cached
end
local tr = (lang:transliterate(text))
if not tr then
error(("Internal error: Unable to transliterate: %s"):format(text))
end
translit_cache[text] = tr
return tr
end
local all_person_number_list = {
"1s",
"2ms",
"2fs",
"3ms",
"3fs",
"1d",
"2d",
"3md",
"3fd",
"1p",
"2mp",
"2fp",
"3mp",
"3fp"
}
local imp_person_number_list = {}
for _, pn in ipairs(all_person_number_list) do
if pn:find("^2") then
table.insert(imp_person_number_list, pn)
end
end
local passive_types = m_table.listToSet {
"pass", -- verb has both active and passive
"ipass", -- verb is active with impersonal passive
"nopass", -- verb is active-only
"onlypass", -- verb is passive-only
"onlypass-impers", -- verb itself is impersonal, meaning passive-only with impersonal passive
}
local indicator_flags = m_table.listToSet {
"nopast", "no_nonpast", "noimp",
"nocat", -- don't categorize or include annotations about this; useful in suppletive parts of verbs
"reduced", -- verb has assimilation/reduction of initial coronals
"altgem", -- form X with alternative past geminate forms with final-weak endings
}
export.potential_lemma_slots = {"past_3ms", "past_pass_3ms", "ind_3ms", "ind_pass_3ms", "imp_2ms"}
export.unsettable_slots = {}
for _, potential_lemma_slot in ipairs(export.potential_lemma_slots) do
table.insert(export.unsettable_slots, potential_lemma_slot .. "_linked")
end
-- We don't set the active participle directly for form I because we don't want stative verbs (with past vowel i or u)
-- to default to فَاعِل. Instead we set the special slot 'ap1' and later copy it to 'ap' for non-stative verbs. The user
-- meanwhile can explicitly request the فَاعِل form for active participles for stative verbs using `ap:+`.
table.insert(export.unsettable_slots, "ap1") -- primary default فَاعِل for form I active participles
table.insert(export.unsettable_slots, "ap2") -- secondary default فَعِيل for form I active participles (stative I)
table.insert(export.unsettable_slots, "ap3") -- secondary default فَعِل for form I active participles (stative II)
table.insert(export.unsettable_slots, "apcd") -- secondary default أَفْعَل for form I active participles (color/defect)
table.insert(export.unsettable_slots, "apan") -- secondary default فَعْلَان for form I active participles (in -ān)
table.insert(export.unsettable_slots, "pp2") -- secondary default فَعِيل for form I passive participles (same as ap2)
table.insert(export.unsettable_slots, "vn2") -- secondary default فِعَال for form III verbal nouns
export.unsettable_slots_set = m_table.listToSet(export.unsettable_slots)
local default_indicator_to_active_participle_slot = {
["+"] = "ap1",
["++"] = "ap2",
["+++"] = "ap3",
["+cd"] = "apcd",
["+an"] = "apan",
}
local slots_that_may_be_uncertain = {
vn = "verbal noun",
ap = "active participle",
}
-- Initialize all the slots for which we generate forms.
local function add_slots(alternant_multiword_spec)
alternant_multiword_spec.verb_slots = {
{"ap", "act|part"},
{"pp", "pass|part"},
{"vn", "vnoun"},
}
for _, unsettable_slot in ipairs(export.unsettable_slots) do
table.insert(alternant_multiword_spec.verb_slots, {unsettable_slot, "-"})
end
-- Add entries for a slot with person/number variants.
-- `slot_prefix` is the prefix of the slot, typically specifying the tense/aspect.
-- `tag_suffix` is a string listing the set of inflection tags to add after the person/number tags.
-- `person_number_list` is a list of the person/number slot suffixes to add to `slot_prefix`.
local function add_personal_slot(slot_prefix, tag_suffix, person_number_list)
for _, persnum in ipairs(person_number_list) do
local slot = slot_prefix .. "_" .. persnum
local accel = persnum:gsub("(.)", "%1|") .. tag_suffix
table.insert(alternant_multiword_spec.verb_slots, {slot, accel})
end
end
local tenses = {
{"past", "past|%s"},
{"ind", "non-past|%s|ind"},
{"sub", "non-past|%s|sub"},
{"juss", "non-past|%s|juss"},
}
for _, slot_accel in ipairs(tenses) do
local slot, accel = unpack(slot_accel)
for _, voice in ipairs {"act", "pass"} do
add_personal_slot(voice == "act" and slot or slot .. "_pass", accel:format(voice),
all_person_number_list)
end
end
add_personal_slot("imp", "act|imp", imp_person_number_list)
alternant_multiword_spec.verb_slots_map = {}
for _, slot_accel in ipairs(alternant_multiword_spec.verb_slots) do
local slot, accel = unpack(slot_accel)
alternant_multiword_spec.verb_slots_map[slot] = accel
end
end
local overridable_stems = {}
local slot_override_param_mods = {
footnote = {
item_dest = "footnotes",
store = "insert",
},
alt = {},
t = {
-- [[Module:links]] expects the gloss in "gloss".
item_dest = "gloss",
},
gloss = {},
g = {
-- [[Module:links]] expects the genders in "g". `sublist = true` automatically splits on comma (optionally
-- with surrounding whitespace).
item_dest = "genders",
sublist = true,
},
pos = {},
lit = {},
id = {},
-- Qualifiers and labels
q = {
type = "qualifier",
},
qq = {
type = "qualifier",
},
l = {
type = "labels",
},
ll = {
type = "labels",
},
}
local function generate_obj(formval, parse_err, prefix, is_slot_override)
local val, uncertain = formval:match("^(.*)(%?)$")
val = val or formval
uncertain = not not uncertain
local ar, translit = val:match("^(.*)//(.*)$")
if not ar then
ar = val
end
if ar == "" then
if uncertain then
ar = "?"
else
error(("Can't specify blank value for override for %s override '%s'"):format(
is_slot_override and "slot" or "stem", prefix))
end
end
return {form = ar, translit = translit, uncertain = uncertain}
end
local function parse_inline_modifiers(comma_separated_group, parse_err, prefix, is_slot_override)
local function this_generate_obj(formval, parse_err)
return generate_obj(formval, parse_err, prefix, is_slot_override)
end
return put.parse_inline_modifiers_from_segments {
group = comma_separated_group,
props = {
param_mods = slot_override_param_mods,
parse_err = parse_err,
generate_obj = this_generate_obj,
pre_normalize_modifiers = function(data)
local modtext = data.modtext
modtext = modtext:match("^(%[.*%])$")
if modtext then
return ("<footnote:%s>"):format(modtext)
end
return data.modtext
end,
},
}
end
local function allow_multiple_values_for_override(comma_separated_groups, data, is_slot_override)
local retvals = {}
for _, comma_separated_group in ipairs(comma_separated_groups) do
local retval
if is_slot_override then
retval = parse_inline_modifiers(comma_separated_group, data.parse_err)
else
retval = generate_obj(comma_separated_group[1], data.parse_err, data.prefix, is_slot_override)
retval.footnotes = data.fetch_footnotes(comma_separated_group)
end
table.insert(retvals, retval)
end
for _, form in ipairs(retvals) do
if form.form == "+" or default_indicator_to_active_participle_slot[form.form] then
if form.form ~= "+" and default_indicator_to_active_participle_slot[form.form] and not is_slot_override then
error(("Stem override '%s' cannot use %s to request a secondary default"):format(
data.prefix, form.form))
end
data.base.slot_override_uses_default[data.prefix] = true
end
end
for _, form in ipairs(retvals) do
if form.form == "-" then
data.base.slot_explicitly_missing[data.prefix] = true
break
end
end
if data.base.slot_explicitly_missing[data.prefix] then
for _, form in ipairs(retvals) do
if form.form ~= "-" then
data.parse_err(("For slot or stem '%s', saw both - and a value other than -, which isn't allowed"):
format(data.prefix))
end
end
return nil
end
return retvals
end
local function simple_choice(choices)
return function(separated_groups, data)
if #separated_groups > 1 then
data.parse_err("For spec '" .. data.prefix .. ":', only one value currently allowed")
end
if #separated_groups[1] > 1 then
data.parse_err("For spec '" .. data.prefix .. ":', no footnotes currently allowed")
end
local choice = separated_groups[1][1]
if not m_table.contains(choices, choice) then
data.parse_err("For spec '" .. data.prefix .. ":', saw value '" .. choice .. "' but expected one of '" ..
concat(choices, ",") .. "'")
end
return choice
end
end
for _, overridable_stem in ipairs {
"past",
"past_v",
"past_c",
"past_pass",
"past_pass_v",
"past_pass_c",
"nonpast",
"nonpast_v",
"nonpast_c",
"nonpast_pass",
"nonpast_pass_v",
"nonpast_pass_c",
"imp",
"imp_v",
"imp_c",
} do
overridable_stems[overridable_stem] = allow_multiple_values_for_override
end
overridable_stems.past_final_weak_vowel = simple_choice { "ay", "aw", "ī", "ū" }
overridable_stems.past_pass_final_weak_vowel = simple_choice { "ay", "aw", "ī", "ū" }
overridable_stems.nonpast_final_weak_vowel = simple_choice { "ā", "ī", "ū" }
overridable_stems.nonpast_pass_final_weak_vowel = simple_choice { "ā", "ī", "ū" }
-------------------------------------------------------------------------------
-- Utility functions --
-------------------------------------------------------------------------------
-- version of rsubn() that discards all but the first return value
local function rsub(term, foo, bar)
return (rsubn(term, foo, bar))
end
-- Concatenate one or more strings or form objects.
local function q(...)
local not_all_strings = debug_translit
local has_manual_translit = debug_translit
for i = 1, select("#", ...) do
local argt = select(i, ...)
if not argt then
error(("Internal error: Saw nil at index %s: %s"):format(i, dump({...})))
end
if type(argt) ~= "string" then
not_all_strings = true
if argt.translit then
has_manual_translit = true
break
end
end
end
if not not_all_strings then
-- just strings, concatenate directly
return concat{...}
end
local formvals = {}
local translit = has_manual_translit and {} or nil
local footnotes
for i = 1, select("#", ...) do
local argt = select(i, ...)
if type(argt) == "string" then
formvals[i] = argt
if has_manual_translit then
translit[i] = transliterate(argt)
end
else
formvals[i] = argt.form
if has_manual_translit then
translit[i] = argt.translit or transliterate(argt.form)
end
footnotes = iut.combine_footnotes(footnotes, argt.footnotes)
end
end
-- FIXME: Do we want to support other properties?
return {
form = concat(formvals),
translit = has_manual_translit and concat(translit) or nil,
footnotes = footnotes,
}
end
-- Return the formval associated with `rad` (a radical or past/non-past vowel, either a string or form object).
local function rget(rad)
if type(rad) == "string" then
return rad
elseif type(rad) == "table" then
return rad.form
else
error(("Internal error: Unexpected type for radical or past/non-past vowel: %s"):format(dump(rad)))
end
end
export.rget = rget -- for use in [[Module:ar-headword]]
-- Return the footnotes associated with `rad` (a radical or past/non-past vowel, either a string or form object).
local function rget_footnotes(rad)
if type(rad) == "string" then
return nil
elseif type(rad) == "table" then
return rad.footnotes
else
error(("Internal error: Unexpected type for radical or past/non-past vowel: %s"):format(dump(rad)))
end
end
-- Return true if the formval associated with `rad` (a radical or past/non-past vowel, either a string or form object)
-- is `val`.
local function req(rad, val)
return rget(rad) == val
end
-- Map `vow` (a past/non-past vowel, either a string or form object without translit) by passing the formval through
-- `fn`. Don't call this on radicals because they may have manual translit and it isn't clear how to handle that.
local function map_vowel(vow, fn)
if type(vow) == "string" then
return fn(vow)
elseif type(vow) == "table" then
return {form = fn(vow.form), footnotes = vow.footnotes}
else
error(("Internal error: Unexpected type for past/non-past vowel: %s"):format(dump(vow)))
end
end
local function get_radicals_3(vowel_spec)
return vowel_spec.rad1, vowel_spec.rad2, vowel_spec.rad3, vowel_spec.past, vowel_spec.nonpast
end
local function get_radicals_4(vowel_spec)
return vowel_spec.rad1, vowel_spec.rad2, vowel_spec.rad3, vowel_spec.rad4
end
local function is_final_weak(base, vowel_spec)
return vowel_spec.weakness == "final-weak" or base.form == "XV"
end
local function link_term(text, face, id)
return m_links.full_link({lang = lang, term = text, tr = "-", id = id}, face)
end
local function tag_text(text, tag, class)
return m_links.full_link({lang = lang, alt = text, tr = "-"})
end
local function track(page)
require("Module:debug/track")("ar-verb/" .. page)
return true
end
local function track_if_ar_conj(base, page)
if base.alternant_multiword_spec.source_template == "ar-conj" then
require("Module:debug/track")("ar-verb/" .. page)
end
return true
end
-------------------------------------------------------------------------------
-- Basic functions to inflect tenses --
-------------------------------------------------------------------------------
local function skip_slot(base, slot, allow_overrides)
if base.slot_explicitly_missing[slot] then
return true
end
if not allow_overrides and base.slot_overrides[slot] and not base.slot_override_uses_default[slot] then
-- Skip any slots for which there are overrides, except those that request the default value using +, ++, etc.
return true
end
if base.passive == "nopass" and (slot == "pp" or slot:find("_pass")) then
return true
elseif base.passive == "onlypass" and slot ~= "pp" and slot ~= "vn" and not slot:find("_pass") then
return true
elseif base.passive == "ipass" and slot:find("_pass") and not slot:find("3ms") then
return true
elseif base.passive == "onlypass-impers" and slot ~= "pp" and slot ~= "vn" and (not slot:find("_pass") or
slot:find("_pass") and not slot:find("3ms")) then
return true
end
if base.nopast and slot:find("^past_") then
return true
end
if base.noimp and slot:find("^imp_") then
return true
end
if base.no_nonpast and (slot:find("^ind_") or slot:find("^sub_") or slot:find("^juss")) then
return true
end
return false
end
local function basic_combine_stem_ending(stem, ending)
return stem .. ending
end
local function basic_combine_stem_ending_tr(stem, ending)
return stem .. ending
end
-- Concatenate `prefixes`, `stems` and `endings` (any of which may be an abbreviate form list, i.e. strings, form
-- objects or lists of strings or form objects) and store into `slot`. If a user-supplied override exists for the slot,
-- nothing will happen unless `allow_overrides` is provided.
local function add3(base, slot, prefixes, stems, endings, allow_overrides)
if skip_slot(base, slot, allow_overrides) then
return
end
-- Optimization since the prefixes are almost always single strings.
if type(prefixes) == "string" then
local function do_combine_stem_ending(stem, ending)
return prefixes .. stem .. ending
end
local function do_combine_stem_ending_tr(stem, ending)
return transliterate(prefixes) .. stem .. ending
end
iut.add_forms(base.forms, slot, stems, endings, do_combine_stem_ending, transliterate,
do_combine_stem_ending_tr, base.form_footnotes)
else
iut.add_multiple_forms(base.forms, slot, {prefixes, stems, endings}, basic_combine_stem_ending, transliterate,
basic_combine_stem_ending_tr, base.form_footnotes)
end
end
-- Insert one or more forms in `form_or_forms` into `slot`. `form_or_forms` is an abbreviated form list (see comment at
-- top of [[Module:inflection utilities]]). If a user-supplied override exists for the slot, nothing will happen unless
-- `allow_overrides` is provided. BEWARE: One form object should never occur in two different slots, or twice in a given
-- slot; if taking a form object from an existing slot, make sure to shallowCopy() it.
local function insert_form_or_forms(base, slot, form_or_forms, allow_overrides, uncertain)
if not skip_slot(base, slot, allow_overrides) then
-- Some optimizations of the most common case of inserting a single string.
if type(form_or_forms) == "string" and not base.form_footnotes then
form_or_forms = {form = form_or_forms, uncertain = uncertain}
iut.insert_form(base.forms, slot, form_or_forms)
else
local list = iut.convert_to_general_list_form(form_or_forms, base.form_footnotes)
if uncertain then
for _, formobj in ipairs(list) do
formobj.uncertain = true
end
end
iut.insert_forms(base.forms, slot, list)
end
end
end
-- Insert `string_or_form` into both the ap2 and pp2 slots, shallowCopying a form object to make sure no form objects
-- occur in two slots.
local function insert_ap2_pp2(base, string_or_form)
insert_form_or_forms(base, "ap2", string_or_form)
if type(string_or_form) == "table" then
string_or_form = m_table.shallowCopy(string_or_form)
end
insert_form_or_forms(base, "pp2", string_or_form)
end
-- Convert `stemforms` (a string, a form object, or a list of strings and/or form objects) into "general form" (a list
-- of form objects) and map `fn` over the list of objects. `fn` is passed two arguments (form value and translit) and
-- should likewise return the new form value and translit. Footnotes will be preserved. FIXME: Preserve other metadata.
local function map_general(stemforms, fn)
return iut.map_forms(iut.convert_to_general_list_form(stemforms), fn)
end
-- Similar to map_general() except that `fn` should return a single value (one or more strings or form objects), instead
-- of two values (form value and translit), and the resulting value(s) from all calls to `fn` will be flattened to
-- construct the overall return value. Footnotes will be preserved. FIXME: Preserve other metadata.
local function flatmap_general(stemforms, fn)
return iut.flatmap_forms(iut.convert_to_general_list_form(stemforms), fn)
end
-- Given user-supplied stem overrides in `base`, construct any derived stem overrides (e.g. vowel-specific or
-- consonant-specific variants), and truncate initial y-/ي- in any non-past overrides.
local function construct_stems(base)
local stems = base.stem_overrides
stems.past_v = stems.past_v or stems.past
stems.past_c = stems.past_c or stems.past
stems.past_pass_v = stems.past_pass_v or stems.past_pass
stems.past_pass_c = stems.past_pass_c or stems.past_pass
stems.nonpast_v = stems.nonpast_v or stems.nonpast
stems.nonpast_c = stems.nonpast_c or stems.nonpast
stems.nonpast_pass_v = stems.nonpast_pass_v or stems.nonpast_pass
stems.nonpast_pass_c = stems.nonpast_pass_c or stems.nonpast_pass
stems.imp_v = stems.imp_v or stems.imp
stems.imp_c = stems.imp_c or stems.imp
local function truncate_nonpast_initial_cons(stem_type, form, translit)
if form == "+" then
return form, translit
end
if not form:find("^" .. Y) then
error(("Form value %s for stem type '%s' should begin with ي"):format(form, stem_type))
end
form = form:gsub("^" .. Y, "")
if translit then
if not translit:find("^y") then
error(("Translit value %s for stem type '%s' should begin with y"):format(translit, stem_type))
end
translit = translit:gsub("^y", "")
end
return form, translit
end
for _, nonpast_stem_type in ipairs { "nonpast_v", "nonpast_c", "nonpast_pass_v", "nonpast_pass_c" } do
if stems[nonpast_stem_type] then
stems[nonpast_stem_type] = map_general(stems[nonpast_stem_type], function(form, translit)
return truncate_nonpast_initial_cons(nonpast_stem_type, form, translit)
end)
end
end
end
-- Given user-specified overrides for stem `stemname`, return overrides with occurrences of + replaced by
-- `default_stem`. If no overrides, return `default_stem`, or {} if no default.
local function override_stem_if_needed(base, stemname, default_stem)
local overrides = base.stem_overrides[stemname]
if not overrides then
return default_stem or {}
end
return map_general(overrides, function(form, translit)
if form ~= "+" and default_indicator_to_active_participle_slot[form] then
error(("Stem overrides cannot use secondary default indicators but saw %s in stem override '%s'"):format(
form, stemname))
end
if form == "+" then
if translit then
error(("Cannot supply manual translit along with + for stem override '%s'"):format(stemname))
end
if not default_stem then
error(("Cannot use + for stem override '%s' because no default is available"):format(stemname))
end
if type(default_stem) ~= "string" then
error(("Internal error: Default stem for '%s' is not a string: %s"):format(stemname, dump(default_stem)))
end
return default_stem
end
return form, translit
end)
end
-------------------------------------------------------------------------------
-- Properties of different verbal forms --
-------------------------------------------------------------------------------
local allowed_vforms = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX",
"X", "XI", "XII", "XIII", "XIV", "XV", "Iq", "IIq", "IIIq", "IVq"}
local allowed_vforms_set = m_table.listToSet(allowed_vforms)
local allowed_vforms_with_weakness = m_table.shallowCopy(allowed_vforms)
-- The user needs to be able to explicitly specify that a form-I verb (specifically one whose initial radical is و) is
-- sound. Cf. wajiʕa yawjaʕu (not #yajaʕu) "to ache, to hurt". In general, i~a and u~u verbs whose initial radical is و
-- seem to not assimilate the first radical; cf. وقح "to be shameless", variously waqaḥa~yaqiḥu, waquḥa~yawquḥu and
-- waqiḥa~yawqaḥu, whereas a~i verbs (wafaḍa~yafiḍu "to rush"), i~i verbs (wafiqa~yafiqu "to be proper, to be suitable")
-- and a~a verbs (waḍaʕa~yaḍaʕu "to set down, to place") do assimilate. But there are naturally exceptions, e.g.
-- waṭiʔa~yaṭaʔu "to tread, to trample"; wasiʕa~yasaʕu "to be spacious; to be well-off"; waṯiʔa~yaṯaʔu "to get bruised,
-- to be sprained". Also beware of waniya~yawnā "to be faint; to languish", which is sound in the first radical and
-- final-weak in the last radical. Nonetheless, the regularity of the patterns mentioned above suggest we should provide
-- them as defaults.
-- Note that there are other cases of unexpectedly sound verbs, e.g. izdawaja~yazdawiju "to be in pairs", layisa~yalyasu
-- "to be valiant, to be brave", ʔaḥwaja~yuḥwiju "to need", istahwana~yastahwinu "to consider easy", sawisa~yaswasu "to
-- be or become moth-eaten or worm-eaten" (vs. sāsa~yasūsu "to govern, to rule" from the same radicals), ʕawira~yaʕwaru
-- "to be one-eyed", istajwaba~yastajwibu "to interrogate", etc. But in these cases there is no need for explicit user
-- specification as the lemma itself specifies the unexpected soundness.
for _, form_with_weakness in ipairs { "I-sound", "I-assimilated", "none-sound", "none-hollow", "none-geminate",
"none-final-weak" } do
table.insert(allowed_vforms_with_weakness, form_with_weakness)
end
local allowed_vforms_with_weakness_set = m_table.listToSet(allowed_vforms_with_weakness)
local function vform_supports_final_weak(vform)
return vform ~= "XI" and vform ~= "XV" and vform ~= "IVq"
end
local function vform_supports_geminate(vform)
return vform == "I" or vform == "III" or vform == "IV" or vform == "VI" or vform == "VII" or vform == "VIII" or
vform == "X"
end
local function vform_supports_hollow(vform)
return vform == "I" or vform == "IV" or vform == "VII" or vform == "VIII" or vform == "X"
end
local function vform_probably_impersonal_passive(vform, weakness, past_vowel, nonpast_vowel)
return vform == "I" and req(past_vowel, I) or vform == "V" or vform == "VI" or vform == "X" or vform == "IIq"
end
local function vform_probably_full_passive(vform)
return vform == "II" or vform == "III" or vform == "IV" or vform == "Iq"
end
local function vform_probably_no_passive(vform, weakness, past_vowel, nonpast_vowel)
return vform == "I" and req(past_vowel, U) or vform == "VII" or vform == "IX" or
vform == "XI" or vform == "XII" or vform == "XIII" or vform == "XIV" or vform == "XV" or
vform == "IIIq" or vform == "IVq"
end
-- Active vforms II, III, IV, Iq use non-past prefixes in -u- instead of -a-.
local function prefix_vowel_from_vform(vform)
if vform == "II" or vform == "III" or vform == "IV" or vform == "Iq" then
return "u"
else
return "a"
end
end
-- True if the active non-past takes a-vocalization rather than i-vocalization in its last syllable.
local function vform_nonpast_a_vowel(vform)
return vform == "V" or vform == "VI" or vform == "XV" or vform == "IIq"
end
-- True if the `passive` spec indicates a passive-only verb.
local function is_passive_only(passive)
return passive == "onlypass" or passive == "onlypass-impers"
end
export.is_passive_only = is_passive_only -- for use in [[Module:ar-headword]]
-------------------------------------------------------------------------------
-- Properties of specific sounds --
-------------------------------------------------------------------------------
-- Is radical wāw (و) or yāʾ (ي)?
local function is_waw_ya(rad)
return req(rad, W) or req(rad, Y)
end
-- Check that radical is wāw (و) or yāʾ (ي), error if not
local function check_waw_ya(rad)
if not is_waw_ya(rad) then
error("Expecting weak radical: '" .. rget(rad) .. "' should be " .. W .. " or " .. Y)
end
end
-- Form-I verb حيّ or حيي and form-X verb استحيا or استحى
local function hayy_radicals(rad1, rad2, rad3)
return req(rad1, "ح") and req(rad2, Y) and is_waw_ya(rad3)
end
-- FUCK ME HARD. "Lua error at line 1514: main function has more than 200 local variables".
local function create_conjugations()
-------------------------------------------------------------------------------
-- Radicals associated with various irregular verbs --
-------------------------------------------------------------------------------
-- Form-I verb أخذ or form-VIII verb اتخذ
local function axadh_radicals(rad1, rad2, rad3)
return req(rad1, HAMZA) and req(rad2, "خ") and req(rad3, "ذ")
end
-- Form-I verb whose imperative has a reduced form: أكل and أخذ and أمر. Return "shortonly" if only
-- short-form imperatives exist (أكل and أخذ) or "shortlong" if long-form imperatives also exist (أمر);
-- they are used after a clitic like فَ and وَ.
local function reduced_imperative_verb(rad1, rad2, rad3)
return axadh_radicals(rad1, rad2, rad3) and "shortonly" or
req(rad1, HAMZA) and req(rad2, "ك") and req(rad3, "ل") and "shortonly" or
req(rad1, HAMZA) and req(rad2, "م") and req(rad3, "ر") and "shortlong"
end
-- Form-I verb رأى and form-IV verb أرى
local function raa_radicals(rad1, rad2, rad3)
return req(rad1, "ر") and req(rad2, HAMZA) and is_waw_ya(rad3)
end
-- Form-I verb سأل
local function saal_radicals(rad1, rad2, rad3)
return req(rad1, "س") and req(rad2, HAMZA) and req(rad3, "ل")
end
-- Form-I verb كان
local function kaan_radicals(rad1, rad2, rad3)
return req(rad1, "ك") and req(rad2, W) and req(rad3, N)
end
-------------------------------------------------------------------------------
-- Sets of past endings --
-------------------------------------------------------------------------------
-- The 14 endings of the sound/hollow/geminate past tense.
local past_endings = {
-- singular
SK .. TU, SK .. TA, SK .. "تِ", A, A .. "تْ",
--dual
SK .. "نَا", SK .. "تُمَا", AA, A .. "تَا",
-- plural
SK .. "نَا", SK .. "تُمْ",
-- shadda + vowel diacritic ends up in the wrong order due to Unicode
-- bug, so keep them separate to avoid this
SK .. "تُن" .. SH .. A, UU .. ALIF, SK .. "نَ"
}
-- Make endings for final-weak past in -aytu or -awtu. AYAW is AY or AW as appropriate. Note that AA and AW are
-- global variables.
local function make_past_endings_ay_aw(ayaw, third_sg_masc)
return {
-- singular
ayaw .. SK .. TU, ayaw .. SK .. TA, ayaw .. SK .. "تِ", third_sg_masc, A .. "تْ",
--dual
ayaw .. SK .. "نَا", ayaw .. SK .. "تُمَا", ayaw .. AA, A .. "تَا",
-- plural
ayaw .. SK .. "نَا", ayaw .. SK .. "تُمْ",
-- shadda + vowel diacritic ends up in the wrong order due to Unicode
-- bug, so keep them separate to avoid this
ayaw .. SK .. "تُن" .. SH .. A, AW .. SK .. ALIF, ayaw .. SK .. "نَ"
}
end
-- past final-weak -aytu endings
local past_endings_ay = make_past_endings_ay_aw(AY, AAMAQ)
-- past final-weak -awtu endings
local past_endings_aw = make_past_endings_ay_aw(AW, AA)
-- used for alternative endings for form-X geminate verbs like اِسْتَمَرَّ
local past_endings_ay_12_person_only = {
-- singular
AY .. SK .. TU, AY .. SK .. TA, AY .. SK .. "تِ", {}, {},
--dual
AY .. SK .. "نَا", AY .. SK .. "تُمَا", {}, {},
-- plural
AY .. SK .. "نَا", AY .. SK .. "تُمْ",
-- shadda + vowel diacritic ends up in the wrong order due to Unicode
-- bug, so keep them separate to avoid this
AY .. SK .. "تُن" .. SH .. A, {}, {},
}
-- Make endings for final-weak past in -ītu or -ūtu. IIUU is ī or ū as appropriate. Note that AA and UU are global
-- variables.
local function make_past_endings_ii_uu(iiuu)
return {
-- singular
iiuu .. TU, iiuu .. TA, iiuu .. "تِ", iiuu .. A, iiuu .. A .. "تْ",
--dual
iiuu .. "نَا", iiuu .. "تُمَا", iiuu .. AA, iiuu .. A .. "تَا",
-- plural
iiuu .. "نَا", iiuu .. "تُمْ",
-- shadda + vowel diacritic ends up in the wrong order due to Unicode
-- bug, so keep them separate to avoid this
iiuu .. "تُن" .. SH .. A, UU .. ALIF, iiuu .. "نَ"
}
end
-- past final-weak -ītu endings
local past_endings_ii = make_past_endings_ii_uu(II)
-- past final-weak -ūtu endings
local past_endings_uu = make_past_endings_ii_uu(UU)
-------------------------------------------------------------------------------
-- Sets of non-past prefixes and endings --
-------------------------------------------------------------------------------
local nonpast_prefix_consonants = {
-- singular
HAMZA, T, T, Y, T,
-- dual
N, T, Y, T,
-- plural
N, T, T, Y, Y
}
-- There are only five distinct endings in all non-past verbs. Make any set of non-past endings given these five
-- distinct endings.
local function make_nonpast_endings(null, fem, dual, pl, fempl)
return {
-- singular
null, null, fem, null, null,
-- dual
null, dual, dual, dual,
-- plural
null, pl, fempl, pl, fempl
}
end
-- endings for non-past indicative
local ind_endings = make_nonpast_endings(
U,
II .. NA,
AANI,
UU .. NA,
SK .. NA
)
-- Make the endings for non-past subjunctive/jussive, given the vowel diacritic used in "null" endings
-- (1s/2ms/3ms/3fs/1p).
local function make_sub_juss_endings(dia_null)
return make_nonpast_endings(
dia_null,
II,
AA,
UU .. ALIF,
SK .. NA
)
end
-- endings for non-past subjunctive
local sub_endings = make_sub_juss_endings(A)
-- endings for non-past jussive
local juss_endings = make_sub_juss_endings(SK)
-- endings for alternative geminate non-past jussive in -a; same as subjunctive
local juss_endings_alt_a = sub_endings
-- endings for alternative geminate non-past jussive in -i
local juss_endings_alt_i = make_sub_juss_endings(I)
-- Endings for final-weak non-past indicative in -ā. Note that AY, AW and AAMAQ are global variables.
local ind_endings_aa = make_nonpast_endings(
AAMAQ,
AYSK .. NA,
AY .. AANI,
AWSK .. NA,
AYSK .. NA
)
-- Make endings for final-weak non-past indicative in -ī or -ū; IIUU is ī or ū as appropriate. Note that II and UU
-- are global variables.
local function make_ind_endings_ii_uu(iiuu)
return make_nonpast_endings(
iiuu,
II .. NA,
iiuu .. AANI,
UU .. NA,
iiuu .. NA
)
end
-- endings for final-weak non-past indicative in -ī
local ind_endings_ii = make_ind_endings_ii_uu(II)
-- endings for final-weak non-past indicative in -ū
local ind_endings_uu = make_ind_endings_ii_uu(UU)
-- Endings for final-weak non-past subjunctive in -ā. Note that AY, AW, ALIF, AAMAQ are global variables.
local sub_endings_aa = make_nonpast_endings(
AAMAQ,
AYSK,
AY .. AA,
AWSK .. ALIF,
AYSK .. NA
)
-- Make endings for final-weak non-past subjunctive in -ī or -ū. IIUU is ī or ū as appropriate. Note that AA, II,
-- UU, ALIF are global variables.
local function make_sub_endings_ii_uu(iiuu)
return make_nonpast_endings(
iiuu .. A,
II,
iiuu .. AA,
UU .. ALIF,
iiuu .. NA
)
end
-- endings for final-weak non-past subjunctive in -ī
local sub_endings_ii = make_sub_endings_ii_uu(II)
-- endings for final-weak non-past subjunctive in -ū
local sub_endings_uu = make_sub_endings_ii_uu(UU)
-- endings for final-weak non-past jussive in -ā
local juss_endings_aa = make_nonpast_endings(
A,
AYSK,
AY .. AA,
AWSK .. ALIF,
AYSK .. NA
)
-- Make endings for final-weak non-past jussive in -ī or -ū. IU is short i or u, IIUU is long ī or ū as appropriate.
-- Note that AA, II, UU, ALIF are global variables.
local function make_juss_endings_ii_uu(iu, iiuu)
return make_nonpast_endings(
iu,
II,
iiuu .. AA,
UU .. ALIF,
iiuu .. NA
)
end
-- endings for final-weak non-past jussive in -ī
local juss_endings_ii = make_juss_endings_ii_uu(I, II)
-- endings for final-weak non-past jussive in -ū
local juss_endings_uu = make_juss_endings_ii_uu(U, UU)
-------------------------------------------------------------------------------
-- Sets of imperative endings --
-------------------------------------------------------------------------------
-- Extract the second person jussive endings to get corresponding imperative endings.
local function imperative_endings_from_jussive(endings)
return {endings[2], endings[3], endings[7], endings[11], endings[12]}
end
-- normal imperative endings
local imp_endings = imperative_endings_from_jussive(juss_endings)
-- alternative geminate imperative endings in -a
local imp_endings_alt_a = imperative_endings_from_jussive(juss_endings_alt_a)
-- alternative geminate imperative endings in -i
local imp_endings_alt_i = imperative_endings_from_jussive(juss_endings_alt_i)
-- final-weak imperative endings in -ā
local imp_endings_aa = imperative_endings_from_jussive(juss_endings_aa)
-- final-weak imperative endings in -ī
local imp_endings_ii = imperative_endings_from_jussive(juss_endings_ii)
-- final-weak imperative endings in -ū
local imp_endings_uu = imperative_endings_from_jussive(juss_endings_uu)
-------------------------------------------------------------------------------
-- Basic functions to inflect tenses --
-------------------------------------------------------------------------------
-- Add to `base` the inflections for the tense indicated by `tense` (the prefix in the slot names, e.g. 'past'
-- or 'juss_pass'), formed by combining the `prefixes`, `stems` and `endings`. Each of `prefixes`, `stems` and
-- `endings` is either a sequence of 5 (for the imperative) or 13 (for other tenses) abbreviated form lists (each of
-- which is either a string, a form object, or a list of strings and/or form objects; see
-- [[Module:inflection utilities]] for more info). Alternatively, any of `prefixes`, `stems` or `endings` can be a
-- single-element list containing an abbreviated form list, with an additional key `all_same` set to true, or (as a
-- special case) a single string; in the latter cases, the same value is used for all 5 or 13 slots. If existing
-- inflections already exist, they will be added to, not overridden. `pnums` is the list of person/number slot name
-- suffixes, which must match up with the elements in `prefixes`, `stems` and `endings` (i.e. 5 for imperative, 13
-- otherwise).
local function inflect_tense_1(base, tense, prefixes, stems, endings, pnums)
if not prefixes or not stems or not endings then
return
end
local function verify_affixes(affixname, affixes)
local function interr(msg)
error(("Internal error: For tense '%s', '%s' %s: %s"):format(tense, affixname, msg, dump(affixes)))
end
if type(affixes) == "string" then
-- do nothing
elseif type(affixes) ~= "table" then
interr("is not a table or string")
elseif affixes.all_same then
if #affixes ~= 1 then
interr(("with all_same = true should have length 1 but has length %s"):format(#affixes))
end
else
if #affixes ~= #pnums then
interr(("should have length %s but has length %s"):format(#pnums, #affixes))
end
end
end
verify_affixes("prefixes", prefixes)
verify_affixes("stems", stems)
verify_affixes("endings", endings)
local function get_affix(affixes, i)
if type(affixes) == "string" then
return affixes
elseif affixes.all_same then
return affixes[1]
else
return affixes[i]
end
end
for i, pnum in ipairs(pnums) do
local prefix = get_affix(prefixes, i)
local stem = get_affix(stems, i)
local ending = get_affix(endings, i)
local slot = tense .. "_" .. pnum
add3(base, slot, prefix, stem, ending)
end
end
-- Add to `base` the inflections for the tense indicated by `tense` (the prefix in the slot names, e.g. 'past'
-- or 'juss_pass'), formed by combining the `prefixes`, `stems` and `endings`. This is a simple wrapper around
-- inflect_tense_1() that applies to all tenses other than the imperative; see inflect_tense_1() for more
-- information about the parameters.
local function inflect_tense(base, tense, prefixes, stems, endings)
inflect_tense_1(base, tense, prefixes, stems, endings, all_person_number_list)
end
-- Like inflect_tense() but for the imperative, which has only five parts instead of 13 and no prefixes.
local function inflect_tense_imp(base, stems, endings)
inflect_tense_1(base, "imp", "", stems, endings, imp_person_number_list)
end
-------------------------------------------------------------------------------
-- Functions to inflect the past tense --
-------------------------------------------------------------------------------
-- Generate past verbs using specified vowel and consonant stems; works for sound, assimilated, hollow, and geminate
-- verbs, active and passive.
local function past_2stem_conj(base, tense, v_stem, c_stem, footnote_12)
local passive = tense:find("_pass") and "_pass" or ""
-- Override stems with user-specified stems if available.
v_stem = override_stem_if_needed(base, "past" .. passive .. "_v", v_stem)
local c_stem_12 = c_stem
if footnote_12 then
c_stem_12 = iut.combine_form_and_footnotes(c_stem_12, footnote_12)
end
c_stem_12 = override_stem_if_needed(base, "past" .. passive .. "_c", c_stem_12)
local c_stem_3 = override_stem_if_needed(base, "past" .. passive .. "_c", c_stem)
inflect_tense(base, tense, "", {
-- singular
c_stem_12, c_stem_12, c_stem_12, v_stem, v_stem,
--dual
c_stem_12, c_stem_12, v_stem, v_stem,
-- plural
c_stem_12, c_stem_12, c_stem_12, v_stem, c_stem_3
}, past_endings)
end
-- Generate past verbs using single specified stem; works for sound and assimilated verbs, active and passive.
local function past_1stem_conj(base, tense, stem)
past_2stem_conj(base, tense, stem, stem)
end
-------------------------------------------------------------------------------
-- Functions to inflect non-past tenses --
-------------------------------------------------------------------------------
-- Generate non-past conjugation, with two stems, for vowel-initial and consonant-initial endings, respectively.
-- Useful for active and passive; for all forms; for all weaknesses (sound, assimilated, hollow, final-weak and
-- geminate) and for all types of non-past (indicative, subjunctive, jussive) except for the imperative. (There is a
-- separate wrapper function below for geminate jussives because they have three alternants.) Both stems may be the
-- same, e.g. for sound verbs.
-- `prefix_vowel` will be either "a" or "u". `endings` should be an array of 13 items. If `endings` is nil or
-- omitted, infer the endings from the tense. If `jussive` is true, or `endings` is nil and `tense` indicatives
-- jussive, use the jussive pattern of vowel/consonant stems (different from the normal ones).
local function nonpast_2stem_conj(base, tense, prefix_vowel, v_stem, c_stem, endings, jussive)
local passive = tense:find("_pass") and "_pass" or ""
-- Override stems with user-specified stems if available.
v_stem = override_stem_if_needed(base, "nonpast" .. passive .. "_v",
v_stem and q(dia[prefix_vowel], v_stem) or nil)
c_stem = override_stem_if_needed(base, "nonpast" .. passive .. "_c",
c_stem and q(dia[prefix_vowel], c_stem) or nil)
if not endings then
if tense:find("^ind") then
endings = ind_endings
elseif tense:find("^sub") then
endings = sub_endings
elseif tense:find("^juss") then
jussive = true
endings = juss_endings
else
error("Internal error: Unrecognized tense '" .. tense .."'")
end
end
if not jussive then
inflect_tense(base, tense, nonpast_prefix_consonants, {
-- singular
v_stem, v_stem, v_stem, v_stem, v_stem,
--dual
v_stem, v_stem, v_stem, v_stem,
-- plural
v_stem, v_stem, c_stem, v_stem, c_stem
}, endings)
else
inflect_tense(base, tense, nonpast_prefix_consonants, {
-- singular
-- 'adlul, tadlul, tadullī, yadlul, tadlul
c_stem, c_stem, v_stem, c_stem, c_stem,
--dual
-- nadlul, tadullā, yadullā, tadullā
c_stem, v_stem, v_stem, v_stem,
-- plural
-- nadlul, tadullū, tadlulna, yadullū, yadlulna
c_stem, v_stem, c_stem, v_stem, c_stem
}, endings)
end
end
-- Generate non-past conjugation with one stem (no distinct stems for vowel-initial and consonant-initial endings).
-- See nonpast_2stem_conj().
local function nonpast_1stem_conj(base, tense, prefix_vowel, stem, endings, jussive)
nonpast_2stem_conj(base, tense, prefix_vowel, stem, stem, endings, jussive)
end
-- Generate active/passive jussive geminative. There are three alternants, two with terminations -a and -i and one
-- in a null termination with a distinct pattern of vowel/consonant stem usage. See nonpast_2stem_conj() for a
-- description of the arguments.
local function jussive_gem_conj(base, tense, prefix_vowel, v_stem, c_stem)
-- alternative in -a
nonpast_2stem_conj(base, tense, prefix_vowel, v_stem, c_stem, juss_endings_alt_a)
-- alternative in -i
nonpast_2stem_conj(base, tense, prefix_vowel, v_stem, c_stem, juss_endings_alt_i)
-- alternative in -null; requires different combination of v_stem and
-- c_stem since the null endings require the c_stem (e.g. "tadlul" here)
-- whereas the corresponding endings above in -a or -i require the v_stem
-- (e.g. "tadulla, tadulli" above)
nonpast_2stem_conj(base, tense, prefix_vowel, v_stem, c_stem, juss_endings, "jussive")
end
-------------------------------------------------------------------------------
-- Functions to inflect the imperative --
-------------------------------------------------------------------------------
-- Generate imperative conjugation, with two stems, for vowel-initial and consonant-initial endings, respectively.
-- Useful for all forms, and for all weaknesses other than final-weak. Note that the two stems may be the same
-- (specifically for sound and assimilated verbs). If `endings` is nil or omitted, use `imp_endings`. If `alt_gem`
-- is specified, use the pattern of vowel and consonant stems appropriate for the alternative geminate imperatives
-- that use a null ending of -a or -i instead of an empty ending.
local function make_2stem_imperative(base, v_stem, c_stem, endings, alt_gem)
endings = endings or imp_endings
-- Override stems with user-specified stems if available.
v_stem = override_stem_if_needed(base, "imp_v", v_stem)
c_stem = override_stem_if_needed(base, "imp_c", c_stem)
if alt_gem then
inflect_tense_imp(base, {v_stem, v_stem, v_stem, v_stem, c_stem}, endings)
else
inflect_tense_imp(base, {c_stem, v_stem, v_stem, v_stem, c_stem}, endings)
end
end
-- Generate imperative parts for sound or assimilated verbs.
local function make_1stem_imperative(base, stem)
make_2stem_imperative(base, stem, stem)
end
-- Generate imperative parts for geminate verbs form I (also IV, VII, VIII, X).
local function make_gem_imperative(base, v_stem, c_stem)
make_2stem_imperative(base, v_stem, c_stem, imp_endings_alt_a, "alt gem")
make_2stem_imperative(base, v_stem, c_stem, imp_endings_alt_i, "alt gem")
make_2stem_imperative(base, v_stem, c_stem)
end
-------------------------------------------------------------------------------
-- Functions to inflect entire verbs --
-------------------------------------------------------------------------------
-- Generate finite parts of a sound verb (also works for assimilated verbs) from five stems (past and non-past,
-- active and passive, plus imperative) plus the prefix vowel in the active non-past ("a" or "u").
local function make_sound_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem, imp_stem,
prefix_vowel)
past_1stem_conj(base, "past", past_stem)
past_1stem_conj(base, "past_pass", past_pass_stem)
nonpast_1stem_conj(base, "ind", prefix_vowel, nonpast_stem)
nonpast_1stem_conj(base, "sub", prefix_vowel, nonpast_stem)
nonpast_1stem_conj(base, "juss", prefix_vowel, nonpast_stem)
nonpast_1stem_conj(base, "ind_pass", "u", nonpast_pass_stem)
nonpast_1stem_conj(base, "sub_pass", "u", nonpast_pass_stem)
nonpast_1stem_conj(base, "juss_pass", "u", nonpast_pass_stem)
make_1stem_imperative(base, imp_stem)
end
local function past_final_weak_endings_from_vowel(vowel)
if vowel == "ay" then
return past_endings_ay
elseif vowel == "aw" then
return past_endings_aw
elseif vowel == "ī" then
return past_endings_ii
elseif vowel == "ū" then
return past_endings_uu
elseif not vowel then
return nil
else
error(("Internal error: Unrecognized past final-weak vowel spec '%s'"):format(vowel))
end
end
local function nonpast_final_weak_endings_from_vowel(vowel)
if vowel == "ā" then
return ind_endings_aa, sub_endings_aa, juss_endings_aa, imp_endings_aa
elseif vowel == "ī" then
return ind_endings_ii, sub_endings_ii, juss_endings_ii, imp_endings_ii
elseif vowel == "ū" then
return ind_endings_uu, sub_endings_uu, juss_endings_uu, imp_endings_uu
elseif not vowel then
return nil
else
error(("Internal error: Unrecognized non-past final-weak vowel spec '%s'"):format(vowel))
end
end
-- Generate finite parts of a final-weak verb from five stems (past and non-past, active and passive, plus
-- imperative), the past active ending vowel (ay, aw, ī or ū), the non-past active ending vowel (ā, ī or ū) and the
-- prefix vowel in the active non-past (a or u).
local function make_final_weak_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem, imp_stem,
past_ending_vowel, nonpast_ending_vowel, prefix_vowel)
past_stem = override_stem_if_needed(base, "past", past_stem)
past_pass_stem = override_stem_if_needed(base, "past_pass", past_pass_stem)
-- Don't call override_stem_if_needed() here for non-past stems; it's called in nonpast_2stem_conj().
imp_stem = override_stem_if_needed(base, "imp", imp_stem)
-- + not supported for ending vowel overrides
past_ending_vowel = base.stem_overrides.past_final_weak_vowel or past_ending_vowel
local past_pass_ending_vowel = base.stem_overrides.past_pass_final_weak_vowel or "ī"
nonpast_ending_vowel = base.stem_overrides.nonpast_final_weak_vowel or nonpast_ending_vowel
local nonpast_pass_ending_vowel = base.stem_overrides.nonpast_pass_final_weak_vowel or "ā"
local past_endings = past_final_weak_endings_from_vowel(past_ending_vowel)
local past_pass_endings = past_final_weak_endings_from_vowel(past_pass_ending_vowel)
local ind_endings, sub_endings, juss_endings, imp_endings =
nonpast_final_weak_endings_from_vowel(nonpast_ending_vowel)
local ind_pass_endings, sub_pass_endings, juss_pass_endings =
nonpast_final_weak_endings_from_vowel(nonpast_pass_ending_vowel)
inflect_tense(base, "past", "", {past_stem, all_same = 1}, past_endings)
inflect_tense(base, "past_pass", "", {past_pass_stem, all_same = 1}, past_pass_endings)
nonpast_1stem_conj(base, "ind", prefix_vowel, nonpast_stem, ind_endings)
nonpast_1stem_conj(base, "sub", prefix_vowel, nonpast_stem, sub_endings)
nonpast_1stem_conj(base, "juss", prefix_vowel, nonpast_stem, juss_endings)
nonpast_1stem_conj(base, "ind_pass", "u", nonpast_pass_stem, ind_pass_endings)
nonpast_1stem_conj(base, "sub_pass", "u", nonpast_pass_stem, sub_pass_endings)
nonpast_1stem_conj(base, "juss_pass", "u", nonpast_pass_stem, juss_pass_endings)
inflect_tense_imp(base, {imp_stem, all_same = 1}, imp_endings)
end
-- Generate finite parts of an augmented (form II+) final-weak verb from five stems (past and non-past, active and
-- passive, plus imperative) plus the prefix vowel in the active non-past ("a" or "u") and a flag indicating if it
-- behaves like a form V/VI verb in taking non-past endings in -ā instead of -ī.
local function make_augmented_final_weak_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem,
imp_stem, prefix_vowel, form56)
make_final_weak_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem, imp_stem, "ay",
form56 and "ā" or "ī", prefix_vowel)
end
-- Generate finite parts of an augmented (form II+) sound or final-weak verb, given:
-- * `base` (conjugation data structure);
-- * `vowel_spec` (radicals, weakness);
-- * `past_stem_base` (active past stem minus last syllable (= -al or -ā));
-- * `nonpast_stem_base` (non-past stem minus last syllable (= -al/-il or -ā/-ī);
-- * `past_pass_stem_base` (passive past stem minus last syllable (= -il or -ī));
-- * `vn` (verbal noun).
local function make_augmented_sound_final_weak_verb(base, vowel_spec, past_stem_base, nonpast_stem_base,
past_pass_stem_base, vn)
insert_form_or_forms(base, "vn", vn)
local lastrad = base.quadlit and vowel_spec.rad4 or vowel_spec.rad3
local final_weak = is_final_weak(base, vowel_spec)
local prefix_vowel = prefix_vowel_from_vform(base.verb_form)
local form56 = vform_nonpast_a_vowel(base.verb_form)
local a_base_suffix = final_weak and "" or q(A, lastrad)
local i_base_suffix = final_weak and "" or q(I, lastrad)
-- past and non-past stems, active and passive
local past_stem = q(past_stem_base, a_base_suffix)
-- In forms 5 and 6, non-past has /a/ as last stem vowel in the non-past
-- in both active and passive, but /i/ in the active participle and /a/
-- in the passive participle. Elsewhere, consistent /i/ in active non-past
-- and participle, consistent /a/ in passive non-past and participle.
-- Hence, forms 5 and 6 differ only in the non-past active (but not
-- active participle), so we have to split the finite non-past stem and
-- active participle stem.
local nonpast_stem = q(nonpast_stem_base, form56 and a_base_suffix or i_base_suffix)
local ap_stem = q(nonpast_stem_base, i_base_suffix)
local past_pass_stem = q(past_pass_stem_base, i_base_suffix)
local nonpast_pass_stem = q(nonpast_stem_base, a_base_suffix)
-- imperative stem
local imp_stem = q(past_stem_base, form56 and a_base_suffix or i_base_suffix)
-- make parts
if final_weak then
make_augmented_final_weak_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem, imp_stem,
prefix_vowel, form56)
else
make_sound_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem, imp_stem, prefix_vowel)
end
-- active and passive participle
if final_weak then
insert_form_or_forms(base, "ap", q(MU, ap_stem, IN))
insert_form_or_forms(base, "pp", q(MU, nonpast_pass_stem, AN, AMAQ))
else
insert_form_or_forms(base, "ap", q(MU, ap_stem))
insert_form_or_forms(base, "pp", q(MU, nonpast_pass_stem))
end
end
-- Generate finite parts of a hollow or geminate verb from ten stems (vowel and consonant stems for each of past and
-- non-past, active and passive, plus imperative) plus the prefix vowel in the active non-past ("a" or "u"), plus a
-- flag indicating if we are a geminate verb.
local function make_hollow_geminate_verb(base, geminate, past_v_stem, past_c_stem, past_pass_v_stem,
past_pass_c_stem, nonpast_v_stem, nonpast_c_stem, nonpast_pass_v_stem, nonpast_pass_c_stem, imp_v_stem,
imp_c_stem, prefix_vowel, altgem_note)
past_2stem_conj(base, "past", past_v_stem, past_c_stem, altgem_note)
past_2stem_conj(base, "past_pass", past_pass_v_stem, past_pass_c_stem)
nonpast_2stem_conj(base, "ind", prefix_vowel, nonpast_v_stem, nonpast_c_stem)
nonpast_2stem_conj(base, "sub", prefix_vowel, nonpast_v_stem, nonpast_c_stem)
nonpast_2stem_conj(base, "ind_pass", "u", nonpast_pass_v_stem, nonpast_pass_c_stem)
nonpast_2stem_conj(base, "sub_pass", "u", nonpast_pass_v_stem, nonpast_pass_c_stem)
if geminate then
jussive_gem_conj(base, "juss", prefix_vowel, nonpast_v_stem, nonpast_c_stem)
jussive_gem_conj(base, "juss_pass", "u", nonpast_pass_v_stem, nonpast_pass_c_stem)
make_gem_imperative(base, imp_v_stem, imp_c_stem)
else
nonpast_2stem_conj(base, "juss", prefix_vowel, nonpast_v_stem, nonpast_c_stem)
nonpast_2stem_conj(base, "juss_pass", "u", nonpast_pass_v_stem, nonpast_pass_c_stem)
make_2stem_imperative(base, imp_v_stem, imp_c_stem)
end
end
-- Generate finite parts of an augmented (form II+) hollow verb, given:
-- * `base` (conjugation data structure);
-- * `vowel_spec` (radicals, weakness);
-- * `past_stem_base` (invariable part of active past stem);
-- * `nonpast_stem_base` (invariable part of nonpast stem);
-- * `past_pass_stem_base` (invariable part of passive past stem);
-- * `vn` (verbal noun).
local function make_augmented_hollow_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base,
vn)
insert_form_or_forms(base, "vn", vn)
local lastrad = base.quadlit and vowel_spec.rad4 or vowel_spec.rad3
local form410 = base.verb_form == "IV" or base.verb_form == "X"
local prefix_vowel = prefix_vowel_from_vform(base.verb_form)
local a_base_suffix_v, a_base_suffix_c
local i_base_suffix_v, i_base_suffix_c
a_base_suffix_v = q(AA, lastrad) -- 'af-āl-a, inf-āl-a
a_base_suffix_c = q(A, lastrad) -- 'af-al-tu, inf-al-tu
i_base_suffix_v = q(II, lastrad) -- 'uf-īl-a, unf-īl-a
i_base_suffix_c = q(I, lastrad) -- 'uf-il-tu, unf-il-tu
-- past and non-past stems, active and passive, for vowel-initial and
-- consonant-initial endings
local past_v_stem = q(past_stem_base, a_base_suffix_v)
local past_c_stem = q(past_stem_base, a_base_suffix_c)
-- yu-f-īl-u, ya-staf-īl-u but yanf-āl-u, yaft-āl-u
local nonpast_v_stem = q(nonpast_stem_base, form410 and i_base_suffix_v or a_base_suffix_v)
local nonpast_c_stem = q(nonpast_stem_base, form410 and i_base_suffix_c or a_base_suffix_c)
local past_pass_v_stem = q(past_pass_stem_base, i_base_suffix_v)
local past_pass_c_stem = q(past_pass_stem_base, i_base_suffix_c)
local nonpast_pass_v_stem = q(nonpast_stem_base, a_base_suffix_v)
local nonpast_pass_c_stem = q(nonpast_stem_base, a_base_suffix_c)
-- imperative stem
local imp_v_stem = q(past_stem_base, form410 and i_base_suffix_v or a_base_suffix_v)
local imp_c_stem = q(past_stem_base, form410 and i_base_suffix_c or a_base_suffix_c)
-- make parts
make_hollow_geminate_verb(base, false, past_v_stem, past_c_stem, past_pass_v_stem,
past_pass_c_stem, nonpast_v_stem, nonpast_c_stem, nonpast_pass_v_stem,
nonpast_pass_c_stem, imp_v_stem, imp_c_stem, prefix_vowel)
-- active participle
insert_form_or_forms(base, "ap", q(MU, nonpast_v_stem))
-- passive participle
insert_form_or_forms(base, "pp", q(MU, nonpast_pass_v_stem))
end
-- Generate finite parts of an augmented (form II+) geminate verb, given:
-- * `base` (conjugation data structure);
-- * `vowel_spec` (radicals, weakness);
-- * `past_stem_base` (invariable part of active past stem; this and the stem bases below will end with a consonant
-- for forms IV, X, IVq, and a short vowel for the others);
-- * `nonpast_stem_base` (invariable part of nonpast stem);
-- * `past_pass_stem_base` (invariable part of passive past stem);
-- * `vn` (verbal noun);
-- * `altgem_note` (footnote to add to active past 1/2-person forms, when alternative forms are supplied [form X]).
local function make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base,
past_pass_stem_base, vn, altgem_note)
insert_form_or_forms(base, "vn", vn)
local vform = base.verb_form
local lastrad = base.quadlit and vowel_spec.rad4 or vowel_spec.rad3
local prefix_vowel = prefix_vowel_from_vform(vform)
local a_base_suffix_v, a_base_suffix_c
local i_base_suffix_v, i_base_suffix_c
if vform == "IV" or vform == "X" or vform == "IVq" then
a_base_suffix_v = q(A, lastrad, SH) -- 'af-all
a_base_suffix_c = q(SK, lastrad, A, lastrad) -- 'af-lal
i_base_suffix_v = q(I, lastrad, SH) -- yuf-ill
i_base_suffix_c = q(SK, lastrad, I, lastrad) -- yuf-lil
else
a_base_suffix_v = q(lastrad, SH) -- fā-ll, infa-ll
a_base_suffix_c = q(lastrad, A, lastrad) -- fā-lal, infa-lal
i_base_suffix_v = q(lastrad, SH) -- yufā-ll, yanfa-ll
i_base_suffix_c = q(lastrad, I, lastrad) -- yufā-lil, yanfa-lil
end
-- past and non-past stems, active and passive, for vowel-initial and
-- consonant-initial endings
local past_v_stem = q(past_stem_base, a_base_suffix_v)
local past_c_stem = q(past_stem_base, a_base_suffix_c)
local nonpast_v_stem = q(nonpast_stem_base, vform_nonpast_a_vowel(vform) and a_base_suffix_v or i_base_suffix_v)
local nonpast_c_stem = q(nonpast_stem_base, vform_nonpast_a_vowel(vform) and a_base_suffix_c or i_base_suffix_c)
-- NOTE: Formerly had a comment that "vform III and VI passive past do not have contracted parts, only
-- uncontracted parts, which are added separately by those functions". This is based on Mace
-- "Arabic Verbs and Essential Grammar" (1999) entry 63 (continued), which shows passive ḥūjija but no ḥūjja;
-- but that is apparently a mistake, as (1) verb tables in other books do show contracted passive parts for
-- these forms; (2) there is no mention of such an exception on p. 99, which explains how geminate ("doubled")
-- verbs work (on the contrary, it says "The contracted and uncontracted pairs (see above) are found all
-- over Forms III and VI of the doubled verbs").
local past_pass_v_stem = q(past_pass_stem_base, i_base_suffix_v)
local past_pass_c_stem = q(past_pass_stem_base, i_base_suffix_c)
local nonpast_pass_v_stem = q(nonpast_stem_base, a_base_suffix_v)
local nonpast_pass_c_stem = q(nonpast_stem_base, a_base_suffix_c)
-- imperative stem
local imp_v_stem = q(past_stem_base, vform_nonpast_a_vowel(vform) and a_base_suffix_v or i_base_suffix_v)
local imp_c_stem = q(past_stem_base, vform_nonpast_a_vowel(vform) and a_base_suffix_c or i_base_suffix_c)
-- make parts
make_hollow_geminate_verb(base, "geminate", past_v_stem, past_c_stem, past_pass_v_stem,
past_pass_c_stem, nonpast_v_stem, nonpast_c_stem, nonpast_pass_v_stem,
nonpast_pass_c_stem, imp_v_stem, imp_c_stem, prefix_vowel, altgem_note)
-- active participle
insert_form_or_forms(base, "ap", q(MU, nonpast_v_stem))
-- passive participle
insert_form_or_forms(base, "pp", q(MU, nonpast_pass_v_stem))
end
-------------------------------------------------------------------------------
-- Conjugation functions for specific conjugation types --
-------------------------------------------------------------------------------
local function form_i_imp_stem_through_rad1(base, nonpast_vowel, rad1)
local imp_vowel = map_vowel(nonpast_vowel, function(vow)
if vow == A or vow == I then
return I
elseif vow == U then
return U
elseif not skip_slot(base, "imp_2ms") then
error(("Internal error: Non-past vowel %s isn't a, i, or u, should have been caught earlier"):format(
dump(nonpast_vowel)))
else
-- Passive-only; imperative won't ever be displayed so it doesn't matter.
return I
end
end)
-- Mace ("Arabic Verbs and Essentials of Grammar" p. 63: [https://archive.org/details/arabicverbsessen00john/page/62/mode/2up])
-- claims that initial hamza is assimilated/elided into a long vowel in the form-I imperative, but apparently
-- this isn't corrrect.
local vowel_on_alif = map_vowel(imp_vowel, function(vow)
return ALIF .. vow
end)
return q(vowel_on_alif, rad1, SK)
end
-- Implement form-I sound or assimilated verb. ASSIMILATED is true for assimilated verbs.
local function make_form_i_sound_assimilated_verb(base, vowel_spec, assimilated)
local rad1, rad2, rad3, past_vowel, nonpast_vowel = get_radicals_3(vowel_spec)
-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied
-- past and non-past stems, active and passive
local past_stem = q(rad1, A, rad2, past_vowel, rad3)
local nonpast_stem = assimilated and q(rad2, nonpast_vowel, rad3) or
q(rad1, SK, rad2, nonpast_vowel, rad3)
local past_pass_stem = q(rad1, U, rad2, I, rad3)
local nonpast_pass_stem = q(rad1, SK, rad2, A, rad3)
-- imperative stem
-- check for irregular verb with reduced imperative (أَخَذَ or أَكَلَ or أَمَرَ)
local reducedimp = reduced_imperative_verb(rad1, rad2, rad3)
if reducedimp then
base.irregular = true
end
local imp_stem_suffix = q(rad2, nonpast_vowel, rad3)
local long_imp_stem_base = form_i_imp_stem_through_rad1(base, nonpast_vowel, rad1)
local short_imp_stem_base = ""
local imp_stem = q((assimilated or reducedimp) and "" or long_imp_stem_base, imp_stem_suffix)
-- make parts
make_sound_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem, imp_stem, "a")
if reducedimp == "shortlong" then
make_1stem_imperative(base, iut.combine_form_and_footnotes(q(long_imp_stem_base, imp_stem_suffix),
mw.getCurrentFrame():preprocess("[used especially with a clitic such as {{m|ar|فَ}} or {{m|ar|وَ}}]")))
end
-- Check for irregular verb سَأَلَ with alternative jussive and imperative. Calling this after make_sound_verb()
-- adds additional entries to the paradigm parts.
if saal_radicals(rad1, rad2, rad3) then
base.irregular = true
nonpast_1stem_conj(base, "juss", "a", "سَل")
nonpast_1stem_conj(base, "juss_pass", "u", "سَل")
make_1stem_imperative(base, "سَل")
end
-- Active participle.
insert_form_or_forms(base, "ap1", q(rad1, AA, rad2, I, rad3))
-- Insert alternative active participle (stative type I) فَعِيل. Since not all verbs have this, we require that
-- verbs that do have it specify it explicitly; a shortcut ++ is provided to make this easier (e.g. <ap:++> to
-- indicate that the alternative form should be used for the active participle, <ap:+,++> to indicate that both
-- forms can be used, and <ap:-> to indicate that there is no active participle). The same form is used for
-- secondary default passive participle.
insert_ap2_pp2(base, q(rad1, A, rad2, II, rad3))
-- Active participle, stative type II فَعِل (+++).
insert_form_or_forms(base, "ap3", q(rad1, A, rad2, I, rad3))
-- Active participle, color/defect أَفْعَل (+cd).
insert_form_or_forms(base, "apcd", q(HAMZA, A, rad1, SK, rad2, A, rad3))
-- Active participle, -ān فَعْلَان (+an).
insert_form_or_forms(base, "apan", q(rad1, A, rad2, SK, rad3, AAN))
-- Passive participle.
insert_form_or_forms(base, "pp", q(MA, rad1, SK, rad2, UU, rad3))
end
conjugations["I-sound"] = function(base, vowel_spec)
make_form_i_sound_assimilated_verb(base, vowel_spec, false)
end
conjugations["none-sound"] = function(base, vowel_spec)
-- All default stems are nil.
make_sound_verb(base)
end
conjugations["none-hollow"] = function(base, vowel_spec)
-- All default stems are nil.
make_hollow_geminate_verb(base, false)
end
conjugations["none-geminate"] = function(base, vowel_spec)
-- All default stems are nil.
make_hollow_geminate_verb(base, "geminate")
end
conjugations["none-final-weak"] = function(base, vowel_spec)
-- All default stems are nil.
make_final_weak_verb(base)
end
conjugations["I-assimilated"] = function(base, vowel_spec)
make_form_i_sound_assimilated_verb(base, vowel_spec, "assimilated")
end
local function make_form_i_hayy_verb(base, vowel_spec)
-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied
base.irregular = true
-- past and non-past stems, active and passive, and imperative stem
local past_c_stem = "حَيِي"
local past_v_stem_long = past_c_stem
local past_v_stem_short = "حَيّ"
local past_pass_c_stem = "حُيِي"
local past_pass_v_stem_long = past_pass_c_stem
local past_pass_v_stem_short = "حُيّ"
local nonpast_stem = "حْي"
local nonpast_pass_stem = nonpast_stem
local imp_stem = _I .. nonpast_stem
-- make parts
past_2stem_conj(base, "past", {}, past_c_stem)
past_2stem_conj(base, "past_pass", {}, past_pass_c_stem)
local variant = vowel_spec.variant or "both"
if variant == "short" or variant == "both" then
past_2stem_conj(base, "past", past_v_stem_short, {})
past_2stem_conj(base, "past_pass", past_pass_v_stem_short, {})
end
local function inflect_long_variant(tense, long_stem, short_stem)
inflect_tense_1(base, tense, "",
{long_stem, long_stem, long_stem, long_stem, short_stem},
{past_endings[4], past_endings[5], past_endings[8], past_endings[9],
past_endings[13]},
{"3ms", "3fs", "3md", "3fd", "3mp"})
end
if variant == "long" or variant == "both" then
inflect_long_variant("past", past_v_stem_long, past_v_stem_short)
inflect_long_variant("past_pass", past_pass_v_stem_long, past_pass_v_stem_short)
end
nonpast_1stem_conj(base, "ind", "a", nonpast_stem, ind_endings_aa)
nonpast_1stem_conj(base, "sub", "a", nonpast_stem, sub_endings_aa)
nonpast_1stem_conj(base, "juss", "a", nonpast_stem, juss_endings_aa)
nonpast_1stem_conj(base, "ind_pass", "u", nonpast_pass_stem, ind_endings_aa)
nonpast_1stem_conj(base, "sub_pass", "u", nonpast_pass_stem, sub_endings_aa)
nonpast_1stem_conj(base, "juss_pass", "u", nonpast_pass_stem, juss_endings_aa)
inflect_tense_imp(base, {imp_stem, all_same = 1}, imp_endings_aa)
-- active and passive participles apparently do not exist for this verb
end
-- Implement form-I final-weak assimilated+final-weak verb. ASSIMILATED is true for assimilated verbs.
local function make_form_i_final_weak_verb(base, vowel_spec, assimilated)
local rad1, rad2, rad3, past_vowel, nonpast_vowel = get_radicals_3(vowel_spec)
-- حَيَّ or حَيِيَ is weird enough that we handle it as a separate function.
if hayy_radicals(rad1, rad2, rad3) then
make_form_i_hayy_verb(base, vowel_spec)
return
end
-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied.
-- Past and non-past stems, active and passive, and imperative stem.
local past_stem = q(rad1, A, rad2)
local past_pass_stem = q(rad1, U, rad2)
local nonpast_stem, nonpast_pass_stem, imp_stem
if raa_radicals(rad1, rad2, rad3) then
base.irregular = true
nonpast_stem = rad1
nonpast_pass_stem = rad1
imp_stem = rad1
else
nonpast_pass_stem = q(rad1, SK, rad2)
if assimilated then
nonpast_stem = rad2
imp_stem = rad2
else
nonpast_stem = nonpast_pass_stem
imp_stem = q(form_i_imp_stem_through_rad1(base, nonpast_vowel, rad1), rad2)
end
end
-- Make parts.
local past_ending_vowel =
req(rad3, Y) and req(past_vowel, A) and "ay" or
req(rad3, W) and req(past_vowel, A) and "aw" or
req(past_vowel, I) and "ī" or "ū"
-- Try to preserve footnotes attached to the third radical and/or past and/or non-past vowels.
local past_footnotes = iut.combine_footnotes(rget_footnotes(rad3), rget_footnotes(past_vowel))
local nonpast_ending_vowel = req(nonpast_vowel, A) and "ā" or req(nonpast_vowel, I) and "ī" or "ū"
local nonpast_footnotes = iut.combine_footnotes(rget_footnotes(rad3), rget_footnotes(nonpast_vowel))
make_final_weak_verb(base,
iut.combine_form_and_footnotes(past_stem, past_footnotes),
iut.combine_form_and_footnotes(past_pass_stem, past_footnotes),
iut.combine_form_and_footnotes(nonpast_stem, nonpast_footnotes),
iut.combine_form_and_footnotes(nonpast_pass_stem, nonpast_footnotes),
iut.combine_form_and_footnotes(imp_stem, nonpast_footnotes),
past_ending_vowel, nonpast_ending_vowel, "a")
-- Active participle.
insert_form_or_forms(base, "ap1", q(rad1, AA, rad2, IN))
-- Active participle, stative type I فَعِيّ (++). FIXME: Is this correct when rad3 is W?
insert_ap2_pp2(base, q(rad1, A, rad2, II, SH))
-- Active participle, stative type II فَعٍ (+++). FIXME: Any examples of this to verify it's correct?
insert_form_or_forms(base, "ap3", q(rad1, A, rad2, IN))
-- Active participle, color/defect أَفْعَى (+cd).
insert_form_or_forms(base, "apcd", q(HAMZA, A, rad1, SK, rad2, AAMAQ))
-- Active participle, -ān فَعْيَان or فَعْوَان (+an).
-- FIXME: Any examples of this for both rad3 = W and y to verify it's correct?
insert_form_or_forms(base, "apan", q(rad1, A, rad2, SK, rad3, AAN))
-- Passive participle.
insert_form_or_forms(base, "pp", q(MA, rad1, SK, rad2, req(rad3, Y) and II or UU, SH))
end
conjugations["I-final-weak"] = function(base, vowel_spec)
make_form_i_final_weak_verb(base, vowel_spec, false)
end
conjugations["I-assimilated+final-weak"] = function(base, vowel_spec)
make_form_i_final_weak_verb(base, vowel_spec, "assimilated")
end
conjugations["I-hollow"] = function(base, vowel_spec)
local rad1, rad2, rad3, past_vowel, nonpast_vowel = get_radicals_3(vowel_spec)
-- In some sense, hollow vowels i~i and u~u are more "correct" than a~i and a~u, but the latter follow the
-- pattern of other form-I verbs, so we map i~i to a~i and u~u to a~u in infer_radicals(). Now however we have
-- to undo this to get the actual past vowel based on the non-past vowel.
if req(past_vowel, A) then
past_vowel = map_vowel(past_vowel, function(vow)
return req(nonpast_vowel, A) and I or rget(nonpast_vowel)
end)
end
local lengthened_nonpast = map_vowel(nonpast_vowel, function(vow)
return vow == U and UU or vow == I and II or AA
end)
-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied.
-- active past stems - vowel (v) and consonant (c)
local past_v_stem = q(rad1, AA, rad3)
local past_c_stem = q(rad1, past_vowel, rad3)
-- active non-past stems - vowel (v) and consonant (c)
local nonpast_v_stem = q(rad1, lengthened_nonpast, rad3)
local nonpast_c_stem = q(rad1, nonpast_vowel, rad3)
-- passive past stems - vowel (v) and consonant (c)
-- 'ufīla, 'ufiltu
local past_pass_v_stem = q(rad1, II, rad3)
local past_pass_c_stem = q(rad1, I, rad3)
-- passive non-past stems - vowel (v) and consonant (c)
-- yufāla/yufalna
-- stem is built differently but conjugation is identical to sound verbs
local nonpast_pass_v_stem = q(rad1, AA, rad3)
local nonpast_pass_c_stem = q(rad1, A, rad3)
-- imperative stem
local imp_v_stem = nonpast_v_stem
local imp_c_stem = nonpast_c_stem
-- make parts
make_hollow_geminate_verb(base, false, past_v_stem, past_c_stem, past_pass_v_stem,
past_pass_c_stem, nonpast_v_stem, nonpast_c_stem, nonpast_pass_v_stem,
nonpast_pass_c_stem, imp_v_stem, imp_c_stem, "a")
if kaan_radicals(rad1, rad2, rad3) then
local endings = make_nonpast_endings(U, {}, {}, {}, {})
inflect_tense(base, "juss", nonpast_prefix_consonants, q(A, rad1), endings)
base.irregular = true
end
-- Active participle.
insert_form_or_forms(base, "ap1", req(rad3, HAMZA) and q(rad1, AA, HAMZA, IN) or
q(rad1, AA, HAMZA, I, rad3))
-- Active participle, stative type I فَيِّد (++). FIXME: Any examples of this to verify it's correct?
insert_ap2_pp2(base, q(rad1, A, Y, SH, I, rad3))
-- Active participle, stative type II فَيِد (+++). FIXME: Any examples of this to verify it's correct?
insert_form_or_forms(base, "ap3", q(rad1, A, Y, I, rad3))
-- Active participle, color/defect أَفّيَد or أَفّوَد (+cd). FIXME: Any examples of this to verify it's correct?
insert_form_or_forms(base, "apcd", q(HAMZA, A, rad1, SK, rad2, A, rad3))
-- Active participle, -ān فَيْدَان or فَوْدَان (+an). Example: جَاعَ "to be hungry", act part جَوْعَان
insert_form_or_forms(base, "apan", q(rad1, A, rad2, SK, rad3, AAN))
-- Passive participle.
insert_form_or_forms(base, "pp", q(MA, rad1, req(rad2, Y) and II or UU, rad3))
end
conjugations["I-geminate"] = function(base, vowel_spec)
local rad1, rad2, rad3, past_vowel, nonpast_vowel = get_radicals_3(vowel_spec)
-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied.
-- active past stems - vowel (v) and consonant (c)
local past_v_stem = q(rad1, A, rad2, SH)
local past_c_stem = q(rad1, A, rad2, past_vowel, rad2)
-- active non-past stems - vowel (v) and consonant (c)
local nonpast_v_stem = q(rad1, nonpast_vowel, rad2, SH)
local nonpast_c_stem = q(rad1, SK, rad2, nonpast_vowel, rad2)
-- passive past stems - vowel (v) and consonant (c)
-- dulla/dulilta
local past_pass_v_stem = q(rad1, U, rad2, SH)
local past_pass_c_stem = q(rad1, U, rad2, I, rad2)
-- passive non-past stems - vowel (v) and consonant (c)
--yudallu/yudlalna
-- stem is built differently but conjugation is identical to sound verbs
local nonpast_pass_v_stem = q(rad1, A, rad2, SH)
local nonpast_pass_c_stem = q(rad1, SK, rad2, A, rad2)
-- imperative stem
local imp_v_stem = q(rad1, nonpast_vowel, rad2, SH)
local imp_c_stem = q(form_i_imp_stem_through_rad1(base, nonpast_vowel, rad1), rad2, nonpast_vowel, rad2)
-- make parts
make_hollow_geminate_verb(base, "geminate", past_v_stem, past_c_stem, past_pass_v_stem,
past_pass_c_stem, nonpast_v_stem, nonpast_c_stem, nonpast_pass_v_stem,
nonpast_pass_c_stem, imp_v_stem, imp_c_stem, "a")
-- Active participle.
insert_form_or_forms(base, "ap1", q(rad1, AA, rad2, SH))
-- Active participle, stative type I فَعِيع (++). FIXME: Any examples of this to verify it's correct?
insert_ap2_pp2(base, q(rad1, A, rad2, II, rad2))
-- Active participle, stative type II فَعّ (+++). Example: بَرَّ "to be pious", active participle بَرّ
insert_form_or_forms(base, "ap3", q(rad1, A, rad2, SH))
-- Active participle, color/defect أَفَعّ (+cd).
-- Example: لَصَّ "to be thievish, to steal repeatedly", active participle أَلَصّ.
insert_form_or_forms(base, "apcd", q(HAMZA, A, rad1, A, rad2, SH))
-- Active participle, -ān فَعَّان (+an). FIXME: Any examples of this to verify it's correct?
insert_form_or_forms(base, "apan", q(rad1, A, rad2, SH, AAN))
-- Passive participle.
insert_form_or_forms(base, "pp", q(MA, rad1, SK, rad2, UU, rad2))
end
-- Return the ta- (active, past and non-past) and tu- (passive past) prefixes for a form II/III/V/VI verb.
-- Form V and VI verbs normally use ta- and tu-, but reduced (base.reduced) verbs use different prefixes. Form II
-- and III verbs have no prefix.
local function form_ii_iii_v_vi_ta_tu_prefix(base, rad1)
local vform = base.verb_form
if vform == "V" or vform == "VI" then
if base.reduced then
-- To simplify the code, we generate two rad1's with a sukūn between them, which is cleaned up in
-- postprocessing.
return q(_I, rad1, SK), q(rad1, SK), q(_U, rad1, SK)
else
return TA, TA, TU
end
else
return "", "", ""
end
end
-- Make form II or V sound or final-weak verb.
local function make_form_ii_v_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local final_weak = is_final_weak(base, vowel_spec)
local vform = base.verb_form
local ta_past_prefix, ta_nonpast_prefix, tu_past_prefix = form_ii_iii_v_vi_ta_tu_prefix(base, rad1)
local vn = vform == "V" and
q(ta_past_prefix, rad1, A, rad2, SH, final_weak and IN or q(U, rad3)) or
q(TA, rad1, SK, rad2, II, final_weak and AH or rad3)
-- various stem bases
local past_stem_base = q(ta_past_prefix, rad1, A, rad2, SH)
local nonpast_stem_base = q(ta_nonpast_prefix, rad1, A, rad2, SH)
local past_pass_stem_base = q(tu_past_prefix, rad1, U, rad2, SH)
-- make parts
make_augmented_sound_final_weak_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base,
vn)
end
conjugations["II-sound"] = function(base, vowel_spec)
make_form_ii_v_sound_final_weak_verb(base, vowel_spec)
end
conjugations["II-final-weak"] = function(base, vowel_spec)
make_form_ii_v_sound_final_weak_verb(base, vowel_spec)
end
local function make_form_iii_alt_vn(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local final_weak = is_final_weak(base, vowel_spec)
-- Insert alternative verbal noun فِعَال. Since not all verbs have this, we require that verbs that do have it
-- specify it explicitly; a shortcut ++ is provided to make this easier (e.g. <vn:+,++> to indicate that
-- both the normal verbal noun مُفَاعَلَة and secondary verbal noun فِعَال are available).
insert_form_or_forms(base, "vn2", q(rad1, I, rad2, AA, final_weak and HAMZA or rad3))
end
-- Make form III or VI sound or final-weak verb.
local function make_form_iii_vi_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local final_weak = is_final_weak(base, vowel_spec)
local vform = base.verb_form
local ta_past_prefix, ta_nonpast_prefix, tu_past_prefix = form_ii_iii_v_vi_ta_tu_prefix(base, rad1)
local vn = vform == "VI" and
q(ta_past_prefix, rad1, AA, rad2, final_weak and IN or q(U, rad3)) or
q(MU, rad1, AA, rad2, final_weak and AAH or q(A, rad3, AH))
-- various stem bases
local past_stem_base = q(ta_past_prefix, rad1, AA, rad2)
local nonpast_stem_base = q(ta_nonpast_prefix, rad1, AA, rad2)
local past_pass_stem_base = q(tu_past_prefix, rad1, UU, rad2)
-- make parts
make_augmented_sound_final_weak_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base,
vn)
if vform == "III" then
make_form_iii_alt_vn(base, vowel_spec)
end
end
conjugations["III-sound"] = function(base, vowel_spec)
make_form_iii_vi_sound_final_weak_verb(base, vowel_spec)
end
conjugations["III-final-weak"] = function(base, vowel_spec)
make_form_iii_vi_sound_final_weak_verb(base, vowel_spec)
end
-- Make form III or VI geminate verb.
local function make_form_iii_vi_geminate_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vform = base.verb_form
local ta_past_prefix, ta_nonpast_prefix, tu_past_prefix = form_ii_iii_v_vi_ta_tu_prefix(base, rad1)
-- Alternative verbal noun فِعَال will be inserted when we add sound parts below.
local vn = vform == "VI" and q(ta_past_prefix, rad1, AA, rad2, SH) or q(MU, rad1, AA, rad2, SH, AH)
-- Various stem bases.
local past_stem_base = q(ta_past_prefix, rad1, AA)
local nonpast_stem_base = q(ta_nonpast_prefix, rad1, AA)
local past_pass_stem_base = q(tu_past_prefix, rad1, UU)
-- Make parts.
local variant = vowel_spec.variant or "short"
if variant == "short" or variant == "both" then
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
-- Also add alternative sound (non-compressed) parts. This will lead to some duplicate entries, but they are
-- removed during addition.
if variant == "long" or variant == "both" then
make_form_iii_vi_sound_final_weak_verb(base, vowel_spec)
elseif vform == "III" then
-- Still need to add the alternative form-III verbal noun.
make_form_iii_alt_vn(base, vowel_spec)
end
end
conjugations["III-geminate"] = function(base, vowel_spec)
make_form_iii_vi_geminate_verb(base, vowel_spec)
end
-- Make form IV sound or final-weak verb.
local function make_form_iv_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local final_weak = is_final_weak(base, vowel_spec)
-- core of stem base, minus stem prefixes
local stem_core
-- check for irregular verb أَرَى
local is_raa = raa_radicals(rad1, rad2, rad3)
if is_raa then
base.irregular = true
stem_core = rad1
else
stem_core = q(rad1, SK, rad2)
end
-- verbal noun
local vn = is_raa and
q(HAMZA, I, stem_core, AA, HAMZA, AH) or
q(HAMZA, I, stem_core, AA, final_weak and HAMZA or rad3)
-- various stem bases
local past_stem_base = q(HAMZA, A, stem_core)
local nonpast_stem_base = stem_core
local past_pass_stem_base = q(HAMZA, U, stem_core)
-- make parts
make_augmented_sound_final_weak_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base,
vn)
end
conjugations["IV-sound"] = function(base, vowel_spec)
make_form_iv_sound_final_weak_verb(base, vowel_spec)
end
conjugations["IV-final-weak"] = function(base, vowel_spec)
make_form_iv_sound_final_weak_verb(base, vowel_spec)
end
conjugations["IV-hollow"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
-- verbal noun
local vn = q(HAMZA, I, rad1, AA, rad3, AH)
-- various stem bases
local past_stem_base = q(HAMZA, A, rad1)
local nonpast_stem_base = rad1
local past_pass_stem_base = q(HAMZA, U, rad1)
-- make parts
make_augmented_hollow_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["IV-geminate"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = q(HAMZA, I, rad1, SK, rad2, AA, rad2)
-- various stem bases
local past_stem_base = q(HAMZA, A, rad1)
local nonpast_stem_base = rad1
local past_pass_stem_base = q(HAMZA, U, rad1)
-- make parts
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["V-sound"] = function(base, vowel_spec)
make_form_ii_v_sound_final_weak_verb(base, vowel_spec)
end
conjugations["V-final-weak"] = function(base, vowel_spec)
make_form_ii_v_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VI-sound"] = function(base, vowel_spec)
make_form_iii_vi_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VI-final-weak"] = function(base, vowel_spec)
make_form_iii_vi_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VI-geminate"] = function(base, vowel_spec)
make_form_iii_vi_geminate_verb(base, vowel_spec)
end
-- Make a verbal noun of the general form that applies to forms VII and above. RAD12 is the first consonant cluster
-- (after initial اِ) and RAD34 is the second consonant cluster. RAD5 is the final consonant.
local function high_form_verbal_noun(rad12, rad34, rad5)
return q(_I, rad12, I, rad34, AA, rad5)
end
-- Populate a sound or final-weak verb for any of the various high-numbered augmented forms (form VII and up) that
-- have up to 5 consonants in two clusters in the stem and the same pattern of vowels between. Some of these
-- consonants in certain verb parts are w's, which leads to apparent anomalies in certain stems of these parts, but
-- these anomalies are handled automatically in postprocessing, where we resolve sequences of iwC -> īC, uwC -> ūC,
-- w + sukūn + w -> w + shadda.
-- RAD12 is the first consonant cluster (after initial اِ) and RAD34 is the second consonant cluster. RAD5 is the
-- final consonant.
local function make_high_form_sound_final_weak_verb(base, vowel_spec, rad12, rad34, rad5)
local final_weak = is_final_weak(base, vowel_spec)
local vn = high_form_verbal_noun(rad12, rad34, final_weak and HAMZA or rad5)
-- various stem bases
local nonpast_stem_base = q(rad12, A, rad34)
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, rad12, U, rad34)
-- make parts
make_augmented_sound_final_weak_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base,
vn)
end
local function form_vii_nrad1(base, rad1)
if base.reduced then
if not req(rad1, M) then
error(("Internal error: Form VII first radical %s is not م but .reduced specified; should have been caught earlier"):
format(rget(rad1)))
end
return M .. SH
else
return q("نْ", rad1)
end
end
-- Make form VII sound or final-weak verb.
local function make_form_vii_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
make_high_form_sound_final_weak_verb(base, vowel_spec, form_vii_nrad1(base, rad1), rad2, rad3)
end
conjugations["VII-sound"] = function(base, vowel_spec)
make_form_vii_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VII-final-weak"] = function(base, vowel_spec)
make_form_vii_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VII-hollow"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local nrad1 = form_vii_nrad1(base, rad1)
local vn = high_form_verbal_noun(nrad1, Y, rad3)
-- various stem bases
local nonpast_stem_base = nrad1
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, nrad1)
-- make parts
make_augmented_hollow_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["VII-geminate"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local nrad1 = form_vii_nrad1(base, rad1)
local vn = high_form_verbal_noun(nrad1, rad2, rad2)
-- various stem bases
local nonpast_stem_base = q(nrad1, A)
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, nrad1, U)
-- make parts
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
-- Return Form VIII verbal noun.
local function form_viii_verbal_noun(base, vowel_spec, rad1, rad2, rad3)
local final_weak = is_final_weak(base, vowel_spec)
rad3 = final_weak and HAMZA or rad3
return {high_form_verbal_noun(vowel_spec.form_viii_assim, rad2, rad3)}
end
-- Make form VIII sound or final-weak verb.
local function make_form_viii_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
-- check for irregular verb اِتَّخَذَ
if axadh_radicals(rad1, rad2, rad3) then
base.irregular = true
rad1 = T
end
make_high_form_sound_final_weak_verb(base, vowel_spec, vowel_spec.form_viii_assim, rad2, rad3)
end
conjugations["VIII-sound"] = function(base, vowel_spec)
make_form_viii_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VIII-final-weak"] = function(base, vowel_spec)
make_form_viii_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VIII-hollow"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = form_viii_verbal_noun(base, vowel_spec, rad1, Y, rad3)
-- various stem bases
local nonpast_stem_base = vowel_spec.form_viii_assim
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, nonpast_stem_base)
-- make parts
make_augmented_hollow_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["VIII-geminate"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = form_viii_verbal_noun(base, vowel_spec, rad1, rad2, rad2)
-- various stem bases
local nonpast_stem_base = q(vowel_spec.form_viii_assim, A)
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, vowel_spec.form_viii_assim, U)
-- make parts
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["IX-sound"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = q(_I, rad1, SK, rad2, I, rad3, AA, rad3)
-- various stem bases
local nonpast_stem_base = q(rad1, SK, rad2, A)
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, rad1, SK, rad2, U)
-- make parts
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["IX-final-weak"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
make_high_form_sound_final_weak_verb(base, vowel_spec, q(rad1, SK, rad2), rad3, rad3)
end
-- Populate a sound or final-weak verb for any of the various high-numbered
-- augmented forms that have 5 consonants in the stem and the same pattern of
-- vowels. Some of these consonants in certain verb parts are w's, which leads to
-- apparent anomalies in certain stems of these parts, but these anomalies
-- are handled automatically in postprocessing, where we resolve sequences of
-- iwC -> īC, uwC -> ūC, w + sukūn + w -> w + shadda.
local function make_high5_form_sound_final_weak_verb(base, vowel_spec, rad1, rad2, rad3, rad4, rad5)
make_high_form_sound_final_weak_verb(base, vowel_spec, q(rad1, SK, rad2), q(rad3, SK, rad4), rad5)
end
-- Make form X sound or final-weak verb.
local function make_form_x_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
-- check for irregular verb اِسْتَحْيَا (also اِسْتَحَى)
local is_hayy = hayy_radicals(rad1, rad2, rad3)
local variant = vowel_spec.variant or "both"
if not is_hayy or variant == "long" or variant == "both" then
make_high5_form_sound_final_weak_verb(base, vowel_spec, S, T, rad1, rad2, rad3)
end
if is_hayy and (variant == "short" or variant == "both") then
base.irregular = true
-- Add alternative entries to the verbal paradigms. Any duplicates are removed during addition.
make_high_form_sound_final_weak_verb(base, vowel_spec, S .. SK .. T, rad1, rad3)
end
end
conjugations["X-sound"] = function(base, vowel_spec)
make_form_x_sound_final_weak_verb(base, vowel_spec)
end
conjugations["X-final-weak"] = function(base, vowel_spec)
make_form_x_sound_final_weak_verb(base, vowel_spec)
end
conjugations["X-hollow"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = q(base.reduced and "اِسْ" or "اِسْتِ", rad1, AA, rad3, AH)
-- various stem bases
local past_stem_base = q(base.reduced and "اِسْ" or "اِسْتَ", rad1)
local nonpast_stem_base = q(base.reduced and "سْ" or "سْتَ", rad1)
local past_pass_stem_base = q(base.reduced and "اُسْ" or "اُسْتُ", rad1)
-- make parts
make_augmented_hollow_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["X-geminate"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = q("اِسْتِ", rad1, SK, rad2, AA, rad2)
-- various stem bases
local past_stem_base = q("اِسْتَ", rad1)
local nonpast_stem_base = q("سْتَ", rad1)
local past_pass_stem_base = q("اُسْتُ", rad1)
-- make parts
if base.altgem then
inflect_tense(base, "past", "", {q(past_stem_base, A, rad2, SH), all_same = 1},
past_endings_ay_12_person_only)
end
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn,
base.altgem and "[uncommon]" or nil)
end
conjugations["XI-sound"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = q(_I, rad1, SK, rad2, II, rad3, AA, rad3)
-- various stem bases
local nonpast_stem_base = q(rad1, SK, rad2, AA)
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, rad1, SK, rad2, UU)
-- make parts
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
-- Probably no form XI final-weak, since already geminate in form; would behave as XI-sound.
-- Make form XII sound or final-weak verb.
local function make_form_xii_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
make_high5_form_sound_final_weak_verb(base, vowel_spec, rad1, rad2, W, rad2, rad3)
end
conjugations["XII-sound"] = function(base, vowel_spec)
make_form_xii_sound_final_weak_verb(base, vowel_spec)
end
conjugations["XII-final-weak"] = function(base, vowel_spec)
make_form_xii_sound_final_weak_verb(base, vowel_spec)
end
-- Make form XIII sound or final-weak verb.
local function make_form_xiii_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
make_high5_form_sound_final_weak_verb(base, vowel_spec, rad1, rad2, W, W, rad3)
end
conjugations["XIII-sound"] = function(base, vowel_spec)
make_form_xiii_sound_final_weak_verb(base, vowel_spec)
end
conjugations["XIII-final-weak"] = function(base, vowel_spec)
make_form_xiii_sound_final_weak_verb(base, vowel_spec)
end
-- Make a form XIV or XV sound or final-weak verb. Last radical appears twice (if`anlala / yaf`anlilu) so if it were
-- w or y you'd get if`anwā / yaf`anwī or if`anyā / yaf`anyī, i.e. unlike for most augmented verbs, the identity of
-- the radical matters.
local function make_form_xiv_xv_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local lastrad = base.verb_form == "XV" and Y or rad3
make_high5_form_sound_final_weak_verb(base, vowel_spec, rad1, rad2, N, rad3, lastrad)
end
conjugations["XIV-sound"] = function(base, vowel_spec)
make_form_xiv_xv_sound_final_weak_verb(base, vowel_spec)
end
conjugations["XIV-final-weak"] = function(base, vowel_spec)
make_form_xiv_xv_sound_final_weak_verb(base, vowel_spec)
end
conjugations["XV-sound"] = function(base, vowel_spec)
make_form_xiv_xv_sound_final_weak_verb(base, vowel_spec)
end
-- Probably no form XV final-weak, since already final-weak in form; would behave as XV-sound.
-- Make form Iq or IIq sound or final-weak verb.
local function make_form_iq_iiq_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3, rad4 = get_radicals_4(vowel_spec)
local final_weak = is_final_weak(base, vowel_spec)
local vform = base.verb_form
local vn = vform == "IIq" and
q(TA, rad1, A, rad2, SK, rad3, (final_weak and IN or q(U, rad4))) or
q(rad1, A, rad2, SK, rad3, (final_weak and AAH or q(A, rad4, AH)))
local ta_pref = vform == "IIq" and TA or ""
local tu_pref = vform == "IIq" and TU or ""
-- various stem bases
local past_stem_base = q(ta_pref, rad1, A, rad2, SK, rad3)
local nonpast_stem_base = past_stem_base
local past_pass_stem_base = q(tu_pref, rad1, U, rad2, SK, rad3)
-- make parts
make_augmented_sound_final_weak_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base,
vn)
end
conjugations["Iq-sound"] = function(base, vowel_spec)
make_form_iq_iiq_sound_final_weak_verb(base, vowel_spec)
end
conjugations["Iq-final-weak"] = function(base, vowel_spec)
make_form_iq_iiq_sound_final_weak_verb(base, vowel_spec)
end
conjugations["IIq-sound"] = function(base, vowel_spec)
make_form_iq_iiq_sound_final_weak_verb(base, vowel_spec)
end
conjugations["IIq-final-weak"] = function(base, vowel_spec)
make_form_iq_iiq_sound_final_weak_verb(base, vowel_spec)
end
-- Make form IIIq sound or final-weak verb.
local function make_form_iiiq_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3, rad4 = get_radicals_4(vowel_spec)
make_high5_form_sound_final_weak_verb(base, vowel_spec, rad1, rad2, N, rad3, rad4)
end
conjugations["IIIq-sound"] = function(base, vowel_spec)
make_form_iiiq_sound_final_weak_verb(base, vowel_spec)
end
conjugations["IIIq-final-weak"] = function(base, vowel_spec)
make_form_iiiq_sound_final_weak_verb(base, vowel_spec)
end
conjugations["IVq-sound"] = function(base, vowel_spec)
local rad1, rad2, rad3, rad4 = get_radicals_4(vowel_spec)
local vn = q(_I, rad1, SK, rad2, I, rad3, SK, rad4, AA, rad4)
-- various stem bases
local past_stem_base = q(_I, rad1, SK, rad2, A, rad3)
local nonpast_stem_base = q(rad1, SK, rad2, A, rad3)
local past_pass_stem_base = q(_U, rad1, SK, rad2, U, rad3)
-- make parts
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
-- Probably no form IVq final-weak, since already geminate in form; would behave as IVq-sound.
end
create_conjugations()
-------------------------------------------------------------------------------
-- Guts of main conjugation function --
-------------------------------------------------------------------------------
-- Given form, weakness and radicals, check to make sure the radicals present are allowable for the weakness. Hamzas on
-- alif/wāw/yāʾ seats are never allowed (should always appear as hamza-on-the-line), and various weaknesses have various
-- strictures on allowable consonants.
local function check_radicals(form, weakness, rad1, rad2, rad3, rad4)
local function hamza_check(index, rad)
if rad == HAMZA_ON_ALIF or rad == HAMZA_UNDER_ALIF or
rad == HAMZA_ON_W or rad == HAMZA_ON_Y then
error("Radical " .. index .. " is " .. rad .. " but should be ء (hamza on the line)")
end
end
local function check_waw_ya(index, rad)
if not is_waw_ya(rad) then
error("Radical " .. index .. " is " .. rad .. " but should be و or ي")
end
end
local function check_not_waw_ya(index, rad)
if is_waw_ya(rad) then
error("In a sound verb, radical " .. index .. " should not be و or ي")
end
end
hamza_check(rad1)
hamza_check(rad2)
hamza_check(rad3)
hamza_check(rad4)
if weakness == "assimilated" or weakness == "assimilated+final-weak" then
if rad1 ~= W then
error("Radical 1 is " .. rad1 .. " but should be و")
end
-- don't check that non-assimilated form I verbs don't have wāw as their
-- first radical because some form-I verbs exist where a first-radical wāw
-- behaves as sound, e.g. wajuha yawjuhu "to be distinguished".
end
if weakness == "final-weak" or weakness == "assimilated+final-weak" then
if rad4 then
check_waw_ya(4, rad4)
else
check_waw_ya(3, rad3)
end
elseif vform_supports_final_weak(form) then
-- non-final-weak verbs cannot have weak final radical if there's a corresponding
-- final-weak verb category. I think this is safe. We may have problems with
-- ḥayya/ḥayiya yaḥyā if we treat it as a geminate verb.
if rad4 then
check_not_waw_ya(4, rad4)
else
check_not_waw_ya(3, rad3)
end
end
if weakness == "hollow" then
check_waw_ya(2, rad2)
-- don't check that non-hollow verbs in forms that support hollow verbs
-- don't have wāw or yāʾ as their second radical because some verbs exist
-- where a middle-radical wāw/yāʾ behaves as sound, e.g. form-VIII izdawaja
-- "to be in pairs".
end
if weakness == "geminate" then
if rad4 then
error("Internal error: No geminate quadrilaterals, should not be seen")
end
if rad2 ~= rad3 then
error("Weakness is geminate; radical 3 is " .. rad3 .. " but should be same as radical 2 " .. rad2)
end
elseif vform_supports_geminate(form) then
-- non-geminate verbs cannot have second and third radical same if there's
-- a corresponding geminate verb category. I think this is safe. We
-- don't fuss over double wāw or double yāʾ because this could legitimately
-- be a final-weak verb with middle wāw/yāʾ, treated as sound.
if rad4 then
error("Internal error: No quadrilaterals should support geminate verbs")
end
if rad2 == rad3 and not is_waw_ya(rad2) then
error("Weakness is '" .. weakness .. "'; radical 2 and 3 are same at " .. rad2 .. " but should not be; consider making weakness 'geminate'")
end
end
end
-- array of substitutions; each element is a 2-entry array FROM, TO; do it
-- this way so the concatenations only get evaluated once
local postprocess_subs = {
-- reorder short-vowel + shadda -> shadda + short-vowel for easier processing
{"(" .. AIU .. ")" .. SH, SH .. "%1"},
----------same letter separated by sukūn should instead use shadda---------
------------happens e.g. in kun-nā "we were".-----------------
{"(.)" .. SK .. "%1", "%1" .. SH},
---------------------------- assimilated verbs ----------------------------
-- iw, iy -> ī (assimilated verbs)
{I .. W .. SK, II},
{I .. Y .. SK, II},
-- uw, uy -> ū (assimilated verbs)
{U .. W .. SK, UU},
{U .. Y .. SK, UU},
-------------- final -yā uses tall alif not alif maqṣūra ------------------
{"(" .. Y .. SH .. "?" .. A .. ")" .. AMAQ, "%1" .. ALIF},
----------------------- handle hamza assimilation -------------------------
-- initial hamza + short-vowel + hamza + sukūn -> hamza + long vowel
{HAMZA .. A .. HAMZA .. SK, HAMZA .. A .. ALIF},
{HAMZA .. I .. HAMZA .. SK, HAMZA .. I .. Y},
{HAMZA .. U .. HAMZA .. SK, HAMZA .. U .. W}
}
local postprocess_tr_subs = {
{"ī([" .. vowels .. "y*])", "iy%1"},
{"ū([" .. vowels .. "w*])", "uw%1"},
{"(.)%*", "%1%1"}, -- implement shadda
---------------------------- assimilated verbs ----------------------------
-- iw, iy -> ī (assimilated verbs)
{"iw([^" .. vowels .. "w])", "ī%1"},
{"iy([^" .. vowels .. "y])", "ī%1"},
-- uw, uy -> ū (assimilated verbs)
{"uw([^" .. vowels .. "w])", "ū%1"},
{"uy([^" .. vowels .. "y])", "ū%1"},
----------------------- handle hamza assimilation -------------------------
-- initial hamza + short-vowel + hamza + sukūn -> hamza + long vowel
{"ʔaʔ(" .. NV .. ")", "ʔā%1"},
{"ʔiʔ(" .. NV .. ")", "ʔī%1"},
{"ʔuʔ(" .. NV .. ")", "ʔū%1"},
}
-- Post-process verb parts to eliminate phonological anomalies. Many of the changes, particularly the tricky ones,
-- involve converting hamza to have the proper seat. The rules for this are complicated and are documented on the
-- [[w:Hamza]] Wikipedia page. In some cases there are alternatives allowed, and we handle them below by returning
-- multiple possibilities.
--
-- `incidental_final`: a word-final hamza is final only because a weak third radical was dropped; see
-- ar_utilities.seat_hamza().
local function postprocess_term(term, incidental_final)
if term == "?" then
return "?"
end
-- Add BORDER at text boundaries.
term = BORDER .. term .. BORDER
-- Do the main post-processing, based on the pattern substitutions in postprocess_subs.
for _, sub in ipairs(postprocess_subs) do
term = rsub(term, sub[1], sub[2])
end
term = term:gsub(BORDER, "")
if not rfind(term, HAMZA) then
return term
end
local spellings = ar_utilities.seat_hamza((term:gsub(HAMZA, HAMZA_PH)), {incidental_final = incidental_final})
if #spellings == 1 then
return spellings[1].form
end
local forms = {}
for _, spelling in ipairs(spellings) do
table.insert(forms, spelling.form)
end
return forms
end
local function postprocess_translit(translit)
if translit == "?" then
return "?"
end
-- Add BORDER at text boundaries.
translit = BORDER .. translit .. BORDER
-- Do the main post-processing, based on the pattern substitutions in postprocess_tr_subs.
for _, sub in ipairs(postprocess_tr_subs) do
translit = rsub(translit, sub[1], sub[2])
end
translit = translit:gsub(BORDER, "")
return translit
end
local function postprocess_forms(base)
-- Final-weak verbs with hamza as second radical.
local incidental_final = false
for _, vowel_spec in ipairs(base.conj_vowels) do
if vowel_spec.weakness and rfind(vowel_spec.weakness, "final%-weak") and vowel_spec.rad2 and
req(vowel_spec.rad2, HAMZA) then
incidental_final = true
end
end
local converted_values = {}
for slot, forms in pairs(base.forms) do
local need_dedup = false
for i, form in ipairs(forms) do
local term = postprocess_term(form.form, incidental_final)
local translit = form.translit and postprocess_translit(form.translit) or nil
if term ~= form.form or translit ~= form.translit then
need_dedup = true
end
converted_values[i] = {term, translit}
end
if need_dedup then
local temp_dedup = {}
for i = 1, #forms do
local new_term, new_translit = unpack(converted_values[i])
if type(new_term) == "table" then
for _, nt in ipairs(new_term) do
local new_formobj = {
form = nt,
translit = new_translit,
footnotes = forms[i].footnotes,
}
iut.insert_form(temp_dedup, "temp", new_formobj)
end
else
local new_formobj = {
form = new_term,
translit = new_translit,
footnotes = forms[i].footnotes,
}
iut.insert_form(temp_dedup, "temp", new_formobj)
end
end
base.forms[slot] = temp_dedup.temp
end
end
end
local function process_slot_overrides(base)
for slot, forms in pairs(base.slot_overrides) do
local existing_values = base.forms[slot]
base.forms[slot] = nil
for _, form in ipairs(forms) do
-- + in active participle for form I requests slot ap1
if form.form == "+" and (base.verb_form ~= "I" or slot ~= "ap") then
if not existing_values then
error(("Slot '%s' requested the default value but no such value available"):format(slot))
end
-- We maintain an invariant that no two slots share a form object (although they may share the footnote
-- lists inside the form objects). However, there is no need to copy the form objects here because there
-- is a one-to-one correspondence between slots and slot overrides, i.e. you can't have a default value
-- go into two slots.
insert_form_or_forms(base, slot, existing_values, "allow overrides", form.uncertain)
elseif default_indicator_to_active_participle_slot[form.form] then
if form.form == "++" then
if slot ~= "vn" and slot ~= "ap" and slot ~= "pp" then
error(("Secondary default value request '++' only applicable to verbal nouns and pariciples, but found in slot '%s'"):
format(slot))
end
else
if slot ~= "ap" then
error(("Secondary default value request '%s' only applicable to active pariciples, but found in slot '%s'"):
format(form.form, slot))
end
end
local secondary_default_slot =
slot == "vn" and "vn2" or slot == "pp" and "pp2" or
default_indicator_to_active_participle_slot[form.form]
local existing_values = base.forms[secondary_default_slot]
if not existing_values then
error(("Slot '%s' requested a secondary default value using '%s' but no such value available"):
format(slot, form.form))
end
-- See comment above about the lack of need to copy the form objects.
insert_form_or_forms(base, slot, existing_values, "allow overrides", form.uncertain)
-- To make sure there aren't shared form objects.
base.forms[secondary_default_slot] = nil
else
insert_form_or_forms(base, slot, form, "allow overrides", form.uncertain)
end
end
end
-- Now, for non-stative form-I verbs, fill the active participle slot from ap1 unless it should be missing (e.g.
-- passive-only or user specified 'ap:-').
if base.verb_form == "I" and not base.forms.ap and base.forms.ap1 and not skip_slot(base, "ap") then
local saw_non_stative = false
for _, vowel_spec in ipairs(base.conj_vowels) do
if req(vowel_spec.past, A) then
saw_non_stative = true
break
end
end
if saw_non_stative then
base.forms.ap = base.forms.ap1
-- To make sure there aren't shared form objects.
base.forms.ap1 = nil
end
end
end
local function handle_lemma_linked(base)
-- Compute linked versions of potential lemma slots, for use in {{ar-verb}}. We substitute the original lemma
-- (before removing links) for forms that are the same as the lemma, if the original lemma has links.
for _, slot in ipairs(export.potential_lemma_slots) do
if base.forms[slot] then
insert_form_or_forms(base, slot .. "_linked", iut.map_forms(base.forms[slot], function(form)
if form == base.lemma and rfind(base.linked_lemma, "%[%[") then
return base.linked_lemma
else
return form
end
end))
end
end
end
-- Process specs given by the user using 'addnote[SLOTSPEC][FOOTNOTE][FOOTNOTE][...]'.
local function process_addnote_specs(base)
for _, spec in ipairs(base.addnote_specs) do
for _, slot_spec in ipairs(spec.slot_specs) do
slot_spec = "^" .. slot_spec .. "$"
for slot, forms in pairs(base.forms) do
if rfind(slot, slot_spec) then
-- To save on memory, side-effect the existing forms.
for _, form in ipairs(forms) do
form.footnotes = iut.combine_footnotes(form.footnotes, spec.footnotes)
end
end
end
end
end
end
local function add_missing_links_to_forms(base)
-- Any forms without links should get them now. Redundant ones will be stripped later.
for slot, forms in pairs(base.forms) do
for _, form in ipairs(forms) do
if not form.form:find("%[%[") then
form.form = "[[" .. form.form .. "]]"
end
end
end
end
local function conjugate_verb(base)
construct_stems(base)
for _, vowel_spec in ipairs(base.conj_vowels) do
-- Reconstruct conjugation type from verb form and (possibly inferred) weakness.
conj_type = base.verb_form .. "-" .. vowel_spec.weakness
-- Check that the conjugation type is recognized.
if not conjugations[conj_type] then
error("Unknown conjugation type '" .. conj_type .. "'")
end
-- The way the conjugation functions work is they always add entries to the appropriate parts of the paradigm
-- (each of which is an array), rather than setting the values. This makes it possible to call more than one
-- conjugation function and essentially get a paradigm of the "either A or B" kind. Doing this may insert
-- duplicate entries into a particular paradigm part, but this is not a problem because we check for duplicate
-- entries when adding them, and don't insert in that case.
conjugations[conj_type](base, vowel_spec)
end
postprocess_forms(base)
process_slot_overrides(base)
-- This should happen before add_missing_links_to_forms() so that the comparison `form == base.lemma` in
-- handle_lemma_linked() works correctly and compares unlinked forms to unlinked forms.
handle_lemma_linked(base)
process_addnote_specs(base)
if not base.alternant_multiword_spec.args.noautolinkverb then
add_missing_links_to_forms(base)
end
end
local function parse_indicator_spec(angle_bracket_spec)
-- Store the original angle bracket spec so we can reconstruct the overall conj spec with the lemma(s) in them.
local base = {
angle_bracket_spec = angle_bracket_spec,
conj_vowels = {},
root_consonants = {},
user_stem_overrides = {},
user_slot_overrides = {},
slot_explicitly_missing = {},
slot_uncertain = {},
slot_override_uses_default = {},
addnote_specs = {},
}
local function parse_err(msg)
error(msg .. ": " .. angle_bracket_spec)
end
local function fetch_footnotes(separated_group)
local footnotes
for j = 2, #separated_group - 1, 2 do
if separated_group[j + 1] ~= "" then
parse_err("Extraneous text after bracketed footnotes: '" .. concat(separated_group) .. "'")
end
if not footnotes then
footnotes = {}
end
table.insert(footnotes, separated_group[j])
end
return footnotes
end
local inside = angle_bracket_spec:match("^<(.*)>$")
assert(inside)
local segments = put.parse_multi_delimiter_balanced_segment_run(inside, {{"[", "]"}, {"<", ">"}})
local dot_separated_groups = put.split_alternating_runs_and_strip_spaces(segments, "%.")
-- The first dot-separated element must specify the verb form, e.g. IV or IIq. If the form is I, it needs to include
-- the the past and non-past vowels, e.g. I/a~u for kataba ~ yaktubu. More than one vowel can be given,
-- comma-separated, and more than one past~non-past pair can be given, slash-separated, e.g. I/a,u~u/i~a for form I
-- كمل, which can be conjugated as kamala/kamula ~ yakmulu or kamila ~ yakmalu. An individual vowel spec must be one
-- of a, i or u and in general (a) at least one past~non-past pair most be given, and (b) both past and non-past
-- vowels must be given even though sometimes the vowel can be determined from the unvocalized form. An exception is
-- passive-only verbs, where the vowels can't in general be determined (except indirectly in some cases by looking
-- at an associated non-passive verb); in that case, the vowel~vowel spec can left out.
local slash_separated_groups = put.split_alternating_runs_and_strip_spaces(dot_separated_groups[1], "/")
local form_spec = slash_separated_groups[1]
base.form_footnotes = fetch_footnotes(form_spec)
if form_spec[1] == "" then
parse_err("Missing verb form")
end
if not allowed_vforms_with_weakness_set[form_spec[1]] then
parse_err(("Unrecognized verb form '%s', should be one of %s"):format(
form_spec[1], list_to_text(allowed_vforms, nil, " or ")))
end
if form_spec[1]:find("%-") then
base.verb_form, base.explicit_weakness = form_spec[1]:match("^(.-)%-(.*)$")
else
base.verb_form = form_spec[1]
end
if #slash_separated_groups > 1 then
if base.verb_form ~= "I" then
parse_err(("Past~non-past vowels can only be specified when verb form is I, but saw form '%s'"):format(
base.verb_form))
end
for i = 2, #slash_separated_groups do
local slash_separated_group = slash_separated_groups[i]
local tilde_separated_groups = put.split_alternating_runs_and_strip_spaces(slash_separated_group, "~")
if #tilde_separated_groups ~= 2 then
parse_err(("Expected two tilde-separated vowel specs: %s"):format(concat(slash_separated_group)))
end
local function parse_conj_vowels(tilde_separated_group, vtype)
local conj_vowel_objects = {}
local comma_separated_groups = put.split_alternating_runs_and_strip_spaces(tilde_separated_group, ",")
for _, comma_separated_group in ipairs(comma_separated_groups) do
local conj_vowel = comma_separated_group[1]
if conj_vowel ~= "a" and conj_vowel ~= "i" and conj_vowel ~= "u" then
parse_err(("Expected %s conjugation vowel '%s' to be one of a, i or u in %s"):format(
vtype, conj_vowel, concat(slash_separated_group)))
end
conj_vowel = dia[conj_vowel]
local conj_vowel_footnotes = fetch_footnotes(comma_separated_group)
-- Try to use strings when possible as it makes q() significantly more efficient.
if conj_vowel_footnotes then
table.insert(conj_vowel_objects, {form = conj_vowel, footnotes = conj_vowel_footnotes})
else
table.insert(conj_vowel_objects, conj_vowel)
end
end
return conj_vowel_objects
end
local conj_vowel_spec = {
past = parse_conj_vowels(tilde_separated_groups[1], "past"),
nonpast = parse_conj_vowels(tilde_separated_groups[2], "non-past"),
}
table.insert(base.conj_vowels, conj_vowel_spec)
end
end
for i = 2, #dot_separated_groups do
local dot_separated_group = dot_separated_groups[i]
local first_element = dot_separated_group[1]
if first_element == "addnote" then
local spec_and_footnotes = fetch_footnotes(dot_separated_group)
if #spec_and_footnotes < 2 then
parse_err("Spec with 'addnote' should be of the form 'addnote[SLOTSPEC][FOOTNOTE][FOOTNOTE][...]'")
end
local slot_spec = table.remove(spec_and_footnotes, 1)
local slot_spec_inside = rmatch(slot_spec, "^%[(.*)%]$")
if not slot_spec_inside then
parse_err("Internal error: slot_spec " .. slot_spec .. " should be surrounded with brackets")
end
local slot_specs = rsplit(slot_spec_inside, ",")
-- FIXME: Here, [[Module:it-verb]] called strip_spaces(). Generally we don't do this. Should we?
table.insert(base.addnote_specs, {slot_specs = slot_specs, footnotes = spec_and_footnotes})
elseif first_element:find("^var:") then
if #dot_separated_group > 1 then
parse_err(("Can't attach footnotes to 'var:' spec '%s'"):format(first_element))
end
base.variant = first_element:match("^var:(.*)$")
elseif first_element:find("^I+V?:") then
local root_cons, root_cons_value = first_element:match("^(I+V?):(.*)$")
local root_index
if root_cons == "I" then
root_index = 1
elseif root_cons == "II" then
root_index = 2
elseif root_cons == "III" then
root_index = 3
elseif root_cons == "IV" then
root_index = 4
if not base.verb_form:find("q$") then
parse_err(("Can't specify root consonant IV for non-quadriliteral verb form '%s': %s"):format(
base.verb_form, first_element))
end
end
local cons, translit = root_cons_value:match("^(.*)//(.*)$")
if not cons then
cons = root_cons_value
end
local root_footnotes = fetch_footnotes(dot_separated_group)
if not translit and not root_footnotes then
base.root_consonants[root_index] = cons
else
base.root_consonants[root_index] = {form = cons, translit = translit, footnotes = root_footnotes}
end
elseif first_element:find("^[a-z][a-z0-9_]*:") then
local slot_or_stem, remainder = first_element:match("^(.-):(.*)$")
dot_separated_group[1] = remainder
local comma_separated_groups = put.split_alternating_runs_and_strip_spaces(dot_separated_group, "[,،]")
if overridable_stems[slot_or_stem] then
if base.user_stem_overrides[slot_or_stem] then
parse_err("Overridable stem '" .. slot_or_stem .. "' specified twice")
end
base.user_stem_overrides[slot_or_stem] = overridable_stems[slot_or_stem](comma_separated_groups,
{prefix = slot_or_stem, base = base, parse_err = parse_err, fetch_footnotes = fetch_footnotes})
else -- assume a form override; we validate further later when the possible slots are available
if base.user_slot_overrides[slot_or_stem] then
parse_err("Form override '" .. slot_or_stem .. "' specified twice")
end
base.user_slot_overrides[slot_or_stem] = allow_multiple_values_for_override(comma_separated_groups,
{prefix = slot_or_stem, base = base, parse_err = parse_err, fetch_footnotes = fetch_footnotes},
"is form override")
end
elseif indicator_flags[first_element] then
if #dot_separated_group > 1 then
parse_err("No footnotes allowed with '" .. first_element .. "' spec")
end
if base[first_element] then
parse_err("Spec '" .. first_element .. "' specified twice")
end
base[first_element] = true
else
local passive, uncertain = first_element:match("^(.*)(%?)$")
passive = passive or first_element
uncertain = not not uncertain
if passive_types[passive] then
if #dot_separated_group > 1 then
parse_err("No footnotes allowed with '" .. passive .. "' spec")
end
if base.passive then
parse_err("Value for passive type specified twice")
end
base.passive = passive
base.passive_uncertain = uncertain
else
parse_err("Unrecognized spec '" .. first_element .. "'")
end
end
end
return base
end
-- Normalize all lemmas, substituting the pagename for blank lemmas and adding links to multiword lemmas.
local function normalize_all_lemmas(alternant_multiword_spec, head)
-- (1) Add links to all before and after text. Remember the original text so we can reconstruct the verb spec later.
if not alternant_multiword_spec.args.noautolinktext then
iut.add_links_to_before_and_after_text(alternant_multiword_spec, "remember original")
end
-- (2) Remove any links from the lemma, but remember the original form so we can use it below in the 'lemma_linked'
-- form.
iut.map_word_specs(alternant_multiword_spec, function(base)
if base.lemma == "" then
base.lemma = head
end
base.user_specified_lemma = base.lemma
base.lemma = m_links.remove_links(base.lemma)
base.user_specified_verb = base.lemma
base.verb = base.user_specified_verb
local linked_lemma
if alternant_multiword_spec.args.noautolinkverb or base.user_specified_lemma:find("%[%[") then
linked_lemma = base.user_specified_lemma
else
-- Add links to the lemma so the user doesn't specifically need to, since we preserve
-- links in multiword lemmas and include links in non-lemma forms rather than allowing
-- the entire form to be a link.
linked_lemma = iut.add_links(base.user_specified_lemma)
end
base.linked_lemma = linked_lemma
end)
end
-- Determine weakness from radicals. Used when root given in place of lemma (e.g. for {{ar-verb forms}}).
local function weakness_from_radicals(form, rad1, rad2, rad3, rad4)
local weakness = nil
local quadlit = form:find("q$")
-- If weakness unspecified, derive from radicals.
if not quadlit then
if is_waw_ya(rad3) and rad1 == W and form == "I" then
weakness = "assimilated+final-weak"
elseif is_waw_ya(rad3) and vform_supports_final_weak(form) then
weakness = "final-weak"
elseif rad2 == rad3 and vform_supports_geminate(form) then
weakness = "geminate"
elseif is_waw_ya(rad2) and vform_supports_hollow(form) then
weakness = "hollow"
elseif rad1 == W and form == "I" then
weakness = "assimilated"
else
weakness = "sound"
end
else
if is_waw_ya(rad4) then
weakness = "final-weak"
else
weakness = "sound"
end
end
return weakness
end
-- Join the infixed tāʔ (ت) to the first radical in form VIII verbs. This may cause assimilation of the tāʔ to the
-- radical or in some cases the radical to the tāʔ. Used when a root is supplied instead of a lemma (which already has
-- the appropriate assimilation in it).
local function form_viii_join_ta(rad)
if rad == W or rad == Y or rad == "ت" then return "تّ"
elseif rad == "د" then return "دّ"
elseif rad == "ث" then return "ثّ"
elseif rad == "ذ" then return "ذّ"
elseif rad == "ز" then return "زْد"
elseif rad == "ص" then return "صْط"
elseif rad == "ض" then return "ضْط"
elseif rad == "ط" then return "طّ"
elseif rad == "ظ" then return "ظّ"
else return rad .. SK .. "ت"
end
end
local function detect_indicator_spec(base)
base.forms = {}
base.stem_overrides = {}
base.slot_overrides = {}
if not base.conj_vowels[1] then
-- These may be converted to inferred vowels. If not, we throw an error if form I and not passive-only.
base.conj_vowels = {{
past = "-",
nonpast = "-",
}}
else
-- If multiple vowels specified for a given vowel type (e.g. a,u~u), expand so that each spec in
local expansion = {}
for _, spec in ipairs(base.conj_vowels) do
for _, past in ipairs(spec.past) do
for _, nonpast in ipairs(spec.nonpast) do
table.insert(expansion, {past = past, nonpast = nonpast})
end
end
end
base.conj_vowels = expansion
end
local vform = base.verb_form
-- check for quadriliteral form (Iq, IIq, IIIq, IVq)
base.quadlit = not not vform:find("q$")
-- Infer radicals as necessary. We infer a separate set of radicals for each past~non-past vowel combination because
-- they may be different (particularly with form-I hollow verbs).
for _, vowel_spec in ipairs(base.conj_vowels) do
-- NOTE: rad1, rad2, etc. refer to user-specified radicals, which are formobj tables that optionally specify an
-- explicit manual translit, whereas ir1, ir2, etc. refer to inferred radicals, which are either strings or
-- lists of possible radicals.
local rads = base.root_consonants
local rad1, rad2, rad3, rad4 = rads[1], rads[2], rads[3], rads[4]
-- Default any unspecified radicals to radicals determined from the headword. The returned radicals may be
-- lists of possible radicals, where the first radical should be chosen if the user didn't explicitly specify a
-- radical but all are allowed. If `ambig = true` is set in the table, the radical is considered ambiguous and
-- categories won't be created for weak radicals.
local weakness, ir1, ir2, ir3, ir4
if vform ~= "none" then
ir1, ir2, ir3 = rmatch(base.lemma, "^([^_])_([^_])_([^_])$")
if not ir1 then
ir1, ir2, ir3, ir4 = rmatch(base.lemma, "^([^_])_([^_])_([^_])_([^_])$")
end
if ir1 then
-- root given instead of lemma
weakness = weakness_from_radicals(vform, ir1, ir2, ir3, ir4)
if vform == "VIII" then
vowel_spec.form_viii_assim = form_viii_join_ta(ir1)
end
else
local ret = export.infer_radicals {
headword = base.lemma,
vform = vform,
passive = base.passive,
past_vowel = vowel_spec.past,
nonpast_vowel = vowel_spec.nonpast,
is_reduced = base.reduced,
}
weakness, ir1, ir2, ir3, ir4 = ret.weakness, ret.rad1, ret.rad2, ret.rad3, ret.rad4
vowel_spec.form_viii_assim = ret.form_viii_assim
vowel_spec.past = ret.past_vowel
vowel_spec.nonpast = ret.nonpast_vowel
vowel_spec.variant = base.variant or ret.variant
end
end
-- For most ambiguous radicals, the choice of radical doesn't matter because it doesn't affect the conjugation
-- one way or another. For form I hollow verbs, however, it definitely does. In fact, the choice of radical is
-- critical even beyond the past and non-past vowels because it affects the form of the passive participle. So,
-- check for this and signal an error if the radical could not be inferred and is not given explicitly.
if vform == "I" and type(ir2) == "table" and ir2.need_radical and not rad2 then
error("Unable to guess middle radical of hollow form I verb; need to specify radical explicitly")
end
if vform == "I" and not is_passive_only(base.passive) and (
rget(vowel_spec.past) == "-" or rget(vowel_spec.nonpast) == "-") then
error("Form I verb that isn't passive-only or final-weak must have past~non-past vowels specified")
end
-- Convert ambiguous radicals.
local function regularize_inferred_radical(rad)
if type(rad) == "table" then
if rad.ambig then
return {form = rad[1], ambig = true}
else
return rad[1]
end
else
return rad
end
end
-- Return the appropriate radical at index `index` (1 through 4), based either on the user-specified radical
-- `user_radical` or (if unspecified) `inferred_radical`, inferred from the unvocalized lemma. Two values are
-- returned, the "regularized" version of the radical (where ambiguous inferred radicals are converted to their
-- most likely actual radical) and the non-regularized version. The returned values are form objects rather than
-- strings.
local function fetch_radical(user_radical, inferred_radical, index)
if not user_radical then
return regularize_inferred_radical(inferred_radical), inferred_radical
else
local rad_formval = rget(user_radical)
if type(inferred_radical) == "table" then
local allowed_radical_set = m_table.listToSet(inferred_radical)
if not allowed_radical_set[rad_formval] then
error(("For lemma %s, radical %s ambiguously inferred as %s but user radical incompatibly given as %s"):
format(base.lemma, index,
list_to_text(inferred_radical, nil, " or "), rad_formval))
end
elseif rad_formval ~= inferred_radical then
error(("For lemma %s, radical %s inferred as %s but user radical incompatibly given as %s"):
format(base.lemma, index, inferred_radical, rad_formval))
end
return user_radical, user_radical
end
end
if vform ~= "none" then
vowel_spec.rad1, vowel_spec.unreg_rad1 = fetch_radical(rad1, ir1, 1)
vowel_spec.rad2, vowel_spec.unreg_rad2 = fetch_radical(rad2, ir2, 2)
vowel_spec.rad3, vowel_spec.unreg_rad3 = fetch_radical(rad3, ir3, 3)
if base.quadlit then
vowel_spec.rad4, vowel_spec.unreg_rad4 = fetch_radical(rad4, ir4, 4)
end
end
if vform == "I" then
-- If explicit weakness given using 'I-sound' or 'I-assimilated', we may need to adjust the inferred weakness.
if base.explicit_weakness == "sound" then
if weakness == "assimilated" then
weakness = "sound"
elseif weakness == "assimilated+final-weak" then
-- Verbs like waniya~yawnā "to be faint; to languish" (although the defaults should handle this
-- correctly)
weakness = "final-weak"
else
error(("Can't specify form 'I-sound' when inferred weakness is '%s' for lemma %s"):format(
weakness, base.lemma))
end
elseif base.explicit_weakness == "assimilated" then
if weakness == "sound" then
-- i~a verbs like waṭiʔa~yaṭaʔu "to tread, to trample"; wasiʕa~yasaʕu "to be spacious; to be well-off";
-- waṯiʔa~yaṯaʔu "to get bruised, to be sprained", which would default to sound.
weakness = "assimilated"
elseif weakness == "final-weak" then
-- For completeness; not clear if any verbs occur where this is needed. (There are plenty of
-- assimilated+final-weak verbs but the defaults should take care of them.)
weakness = "assimilated+final-weak"
else
error(("Can't specify form 'I-assimilated' when inferred weakness is '%s' for lemma %s"):format(
weakness, base.lemma))
end
elseif base.explicit_weakness then
error(("Internal error: Unrecognized value '%s' for base.explicit_weakness"):format(base.explicit_weakness))
end
elseif vform == "none" then
weakness = base.explicit_weakness
elseif base.explicit_weakness then
error(("Internal error: Explicit weakness should not be specifiable except with forms I and none, but saw explicit weakness '%s' with verb form '%s'"):
format(base.explicit_weakness, vform))
end
vowel_spec.weakness = weakness
if vform ~= "none" then
-- Error if radicals are wrong given the weakness. More likely to happen if the weakness is explicitly given
-- rather than inferred. Will also happen if certain incorrect letters are included as radicals e.g. hamza on
-- top of various letters, alif maqṣūra, tā' marbūṭa.
check_radicals(vform, weakness, rget(vowel_spec.rad1), rget(vowel_spec.rad2), rget(vowel_spec.rad3),
base.quadlit and rget(vowel_spec.rad4) or nil)
end
-- Check the variant value.
local form_iii_vi_geminate = (vform == "III" or vform == "VI") and rget(vowel_spec.rad2) == rget(vowel_spec.rad3) and
not req(vowel_spec.rad2, Y)
local hayy_i_x = hayy_radicals(vowel_spec.rad1, vowel_spec.rad2, vowel_spec.rad3) and (vform == "I" or vform == "X")
if form_iii_vi_geminate or hayy_i_x then
if vowel_spec.variant and vowel_spec.variant ~= "long" and vowel_spec.variant ~= "short" and vowel_spec.variant ~= "both" then
error(("For form-III/VI geminate verb or form-I/X verb with ح-ي-ي radicals, saw unrecognized 'var:%s' value; should be 'var:long', 'var:short' or 'var:both'"):format(
vowel_spec.variant))
end
elseif vowel_spec.variant then
error(("Variant value 'var:%s' not allowed in this context"):format(vowel_spec.variant))
end
end
-- If form I, regroup expanded vowels for display purposes.
if vform == "I" then
local group_by_past = {}
for _, vowel_spec in ipairs(base.conj_vowels) do
m_table.insertIfNot(group_by_past, {
past = undia[rget(vowel_spec.past)],
nonpasts = {undia[rget(vowel_spec.nonpast)]},
}, {
key = function(obj) return obj.past end,
combine = function(obj1, obj2)
for _, nonpast in ipairs(obj2.nonpasts) do
m_table.insertIfNot(obj1.nonpasts, nonpast)
end
end,
})
end
local group_by_nonpast = {}
for _, vowel_spec in ipairs(group_by_past) do
m_table.insertIfNot(group_by_nonpast, {
pasts = {vowel_spec.past},
nonpasts = vowel_spec.nonpasts,
}, {
key = function(obj) return obj.nonpasts end,
combine = function(obj1, obj2)
for _, past in ipairs(obj2.pasts) do
m_table.insertIfNot(obj1.pasts, past)
end
end,
})
end
base.grouped_conj_vowels = group_by_nonpast
end
-- Set value of passive. If not specified, default is yes for forms II, III, IV and Iq; no but uncertainly for
-- forms VII, IX, XI - XV and IIIq - IVq, as well as form I with past vowel u; impersonal but uncertainly for form
-- V, VI, X and IIq, as well as form I with past vowel i; and yes but uncertainly for the remainder (form I with
-- past vowel only a and form VIII).
if not base.passive then
base.passive_defaulted = true
-- Temporary tracking for defaulted passives by verb form, weakness and (for form I) past/non-past vowels.
track_if_ar_conj(base, "passive-defaulted/" .. vform)
for _, vowel_spec in ipairs(base.conj_vowels) do
track_if_ar_conj(base, "passive-defaulted/" .. vform.. "/" .. vowel_spec.weakness)
if vform == "I" then
local past_nonpast = ("%s~%s"):format(undia[vowel_spec.past], undia[vowel_spec.nonpast])
track_if_ar_conj(base, "passive-defaulted/I/" .. past_nonpast)
track_if_ar_conj(base, "passive-defaulted/I/" .. vowel_spec.weakness .. "/" .. past_nonpast)
end
end
if vform_probably_full_passive(vform) then
base.passive = "pass"
else
base.passive_uncertain = true
for _, vowel_spec in ipairs(base.conj_vowels) do
if vform_probably_no_passive(vform, vowel_spec.weakness, vowel_spec.past, vowel_spec.nonpast) then
base.passive = "nopass"
break
elseif vform_probably_impersonal_passive(vform, vowel_spec.weakness, vowel_spec.past,
vowel_spec.nonpast) then
base.passive = "ipass"
break
end
end
base.passive = base.passive or "pass"
end
end
-- NOTE: Currently there are no built-in stems or form overrides for Arabic; this code is inherited from
-- [[Module:ca-verb]], where such things do exist, and is kept for generality in case we decide in the future to
-- implement such things.
-- Override built-in verb stems and overrides with user-specified ones.
for stem, values in pairs(base.user_stem_overrides) do
base.stem_overrides[stem] = values
end
for slot, values in pairs(base.user_slot_overrides) do
if not base.alternant_multiword_spec.verb_slots_map[slot] then
error("Unrecognized override slot '" .. slot .. "': " .. base.angle_bracket_spec)
end
if export.unsettable_slots_set[slot] then
error("Slot '" .. slot .. "' cannot be set using an override: " .. base.angle_bracket_spec)
end
if skip_slot(base, slot, "allow overrides") then
error("Override slot '" .. slot ..
"' would be skipped based on the passive, 'noimp' and/or 'no_nonpast' settings: " ..
base.angle_bracket_spec)
end
base.slot_overrides[slot] = values
end
if base.verb_form == "none-final-weak" then
for _, stem_type in ipairs { "past", "past_pass", "nonpast", "nonpast_pass" } do
if base.stem_overrides[stem_type .. "_c"] or base.stem_overrides[stem_type .. "_v"] then
error(("Specify past stem for verb type 'none-final-weak' using '%s:...' not '%s_c:...' or '%s_v:...'"):
format(stem_type, stem_type, stem_type))
end
end
for _, stem_type in ipairs { "past", "nonpast" } do
if base.stem_overrides[stem_type] or not base.stem_overrides[stem_type .. "_final_weak_vowel"] then
error(("For verb type 'none-final-weak', if '%s:...' specified, so must '%s_final_weak_vowel:...'"):
format(stem_type, stem_type))
end
end
end
end
local function detect_all_indicator_specs(alternant_multiword_spec)
add_slots(alternant_multiword_spec)
alternant_multiword_spec.verb_forms = {}
-- This means at least one individual base had the slot marked as explicitly missing. Another base (e.g. when
-- there are multiple alternants) might have a value for the slot. In practice, we only respect this when there are
-- no overall values in the slot and `slot_uncertain` isn't set; in this case, we display "no ..." for the slot
-- instead of simply not displaying anything for the slot.
alternant_multiword_spec.slot_explicitly_missing = {}
-- This means at least one individual base had no values for the slot and the slot marked as explicitly uncertain.
-- Note that this is different from a value being present but marked as uncertain (e.g. if an override was given
-- with a ? after it); this causes the form object for the value to have `uncertain = true` set. If there are no
-- overall values in the slot and `slot_uncertain` is set, we display this in the headword.
alternant_multiword_spec.slot_uncertain = {}
iut.map_word_specs(alternant_multiword_spec, function(base)
-- So arguments, etc. can be accessed. WARNING: Creates circular reference.
base.alternant_multiword_spec = alternant_multiword_spec
detect_indicator_spec(base)
if not base.nocat then
m_table.insertIfNot(alternant_multiword_spec.verb_forms, base.verb_form)
end
if base.passive_uncertain then
alternant_multiword_spec.passive_uncertain = true
end
for slot, _ in pairs(base.slot_explicitly_missing) do
alternant_multiword_spec.slot_explicitly_missing[slot] = true
end
end)
end
local function determine_slot_uncertainty_from_forms(alternant_multiword_spec)
iut.map_word_specs(alternant_multiword_spec, function(base)
-- If no verbal noun and verb form is not 'none' (manually-specified stems) — which currently only happens for
-- form I — and the verbal noun wasn't explicitly indicated as missing using <vn:->, we assume it's just
-- unknown/unspecified rather than missing. Same with active participles.
local vform = base.verb_form
for uncertain_slot, _ in pairs(slots_that_may_be_uncertain) do
if not base.forms[uncertain_slot] and vform ~= "none" and not skip_slot(base, uncertain_slot) then
base.slot_uncertain[uncertain_slot] = true
end
end
-- Propagate slot uncertainty up. Currently only the verbal noun can have this set but we write the code
-- generally.
for slot, _ in pairs(base.slot_uncertain) do
alternant_multiword_spec.slot_uncertain[slot] = true
end
end)
-- If slot is uncertain and has no value, explicitly set its value to "?".
for uncertain_slot, _ in pairs(slots_that_may_be_uncertain) do
if not alternant_multiword_spec.forms[uncertain_slot] and
alternant_multiword_spec.slot_uncertain[uncertain_slot] then
alternant_multiword_spec.forms[uncertain_slot] = {{form = "?"}}
end
end
end
-- Determine certain properties of the verb from the overall forms, such as whether the verb is active-only or
-- passive-only, is impersonal, lacks an imperative, etc.
local function determine_verb_properties_from_forms(alternant_multiword_spec)
alternant_multiword_spec.has_active = false
alternant_multiword_spec.has_passive = false
alternant_multiword_spec.has_non_impers_active = false
alternant_multiword_spec.has_non_impers_passive = false
alternant_multiword_spec.has_imp = false
alternant_multiword_spec.has_past = false
alternant_multiword_spec.has_nonpast = false
for slot, _ in pairs(alternant_multiword_spec.forms) do
if slot == "ap" or slot:find("[123]") and not slot:find("_pass") then
alternant_multiword_spec.has_active = true
end
if slot == "pp" or slot:find("[123]") and slot:find("_pass") then
alternant_multiword_spec.has_passive = true
end
if slot:find("[123]") and not slot:find("pass_[123]") and not slot:find("3ms") then
alternant_multiword_spec.has_non_impers_active = true
end
if slot:find("pass_[123]") and not slot:find("3ms") then
alternant_multiword_spec.has_non_impers_passive = true
end
if slot:find("^imp_") then
alternant_multiword_spec.has_imp = true
end
if slot:find("^past_") then
alternant_multiword_spec.has_past = true
end
if slot:find("^ind_") or slot:find("^sub_") or slot:find("^juss_") then
alternant_multiword_spec.has_nonpast = true
end
end
end
local function add_categories_and_annotation(alternant_multiword_spec, base, multiword_lemma, insert_ann, insert_cat)
-- Useful e.g. in constructing suppletive verbs out of parts. For a verb like جاء or أتى whose imperative comes
-- from the unrelated verb تعالى, we don't want the latter verb showing up in categories or annotations.
if base.nocat then
return
end
local vform = base.verb_form
if vform ~= "none" then
insert_ann("form", vform)
insert_cat("form-" .. vform .. " verbs")
end
if base.reduced then
insert_ann("reduced", "reduced")
if vform ~= "none" then
insert_cat("form-" .. vform .. " reduced verbs")
end
end
if base.quadlit then
insert_cat("verbs with quadriliteral roots")
end
if base.passive_defaulted then
insert_cat("verbs with defaulted passive")
end
for _, vowel_spec in ipairs(base.conj_vowels) do
local rad1, rad2, rad3, rad4 = get_radicals_4(vowel_spec)
local final_weak = is_final_weak(base, vowel_spec)
local weakness = vowel_spec.weakness
-- We have to distinguish weakness by form and weakness by conjugation. Weakness by form merely indicates the
-- presence of weak letters in certain positions in the radicals. Weakness by conjugation is related to how the
-- verbs are conjugated. For example, form-II verbs that are "hollow by form" (middle radical is wāw or yāʾ) are
-- conjugated as sound verbs. Another example: form-I verbs with initial wāw are "assimilated by form" and most
-- are assimilated by conjugation as well, but a few are sound by conjugation, e.g. wajuha yawjuhu "to be
-- distinguished" (rather than wajuha yajuhu); similarly for some hollow-by-form verbs in various forms, e.g.
-- form VIII izdawaja yazdawiju "to be in pairs" (rather than izdāja yazdāju). Categories referring to weakness
-- always refer to weakness by conjugation; weakness by form is distinguished only by categories such as
-- [[:Category:Arabic form-III verbs with و as second radical]].
insert_ann("weakness", weakness)
if vform ~= "none" then
insert_cat(("%s form-%s verbs"):format(weakness, vform))
end
local function radical_is_ambiguous(rad)
return type(rad) == "table" and rad.ambig
end
local function radical_is_unambiguous_weak(rad)
return not radical_is_ambiguous(rad) and (is_waw_ya(rad) or req(rad, HAMZA))
end
if vform ~= "none" then
local ur1, ur2, ur3, ur4 =
vowel_spec.unreg_rad1, vowel_spec.unreg_rad2, vowel_spec.unreg_rad3, vowel_spec.unreg_rad4
-- Create headword categories based on the radicals. Do the following before
-- converting the Latin radicals into Arabic ones so we distinguish
-- between ambiguous and non-ambiguous radicals.
if radical_is_ambiguous(ur1) or radical_is_ambiguous(ur2) or radical_is_ambiguous(ur3) or
ur4 and radical_is_ambiguous(ur4) then
insert_cat("verbs with ambiguous radicals")
end
if radical_is_unambiguous_weak(ur1) then
insert_cat("form-" .. vform .. " verbs with " .. rget(ur1) .. " as first radical")
end
if radical_is_unambiguous_weak(ur2) then
insert_cat("form-" .. vform .. " verbs with " .. rget(ur2) .. " as second radical")
end
if radical_is_unambiguous_weak(ur3) then
insert_cat("form-" .. vform .. " verbs with " .. rget(ur3) .. " as third radical")
end
if ur4 and radical_is_unambiguous_weak(ur4) then
insert_cat("form-" .. vform .. " verbs with " .. rget(ur4) .. " as fourth radical")
end
end
end
if vform == "I" and not is_passive_only(base.passive) then
for _, vowel_spec in ipairs(base.grouped_conj_vowels) do
insert_ann("vowels",
("%s ~ %s"):format(concat(vowel_spec.pasts, "/"), concat(vowel_spec.nonpasts, "/")))
for _, past in ipairs(vowel_spec.pasts) do
for _, nonpast in ipairs(vowel_spec.nonpasts) do
if past == "-" or nonpast == "-" then
error("Internal error: Saw form I past vowel %s and non-past vowel %s but - in place of vowel should have triggered an error earlier")
end
insert_cat(("form-I verbs with past vowel %s and non-past vowel %s"):format(past, nonpast))
end
end
end
end
for slot, name in pairs(slots_that_may_be_uncertain) do
if base.slot_uncertain[slot] then
-- An unspecified and non-defaulted verbal noun (form I) is considered uncertain rather than explicitly
-- missing. Use <vn:-> to explicitly indicate the lack of verbal noun. Same for form-I stative active
-- participles.
insert_cat(("verbs with unknown or uncertain %ss"):format(name))
end
end
if base.irregular then
insert_ann("irreg", "irregular")
insert_cat("irregular verbs")
end
end
-- Compute the categories to add the verb to, as well as the annotation to display in the conjugation title bar. We
-- combine the code to do these functions as both categories and title bar contain similar information.
local function compute_categories_and_annotation(alternant_multiword_spec)
alternant_multiword_spec.categories = {}
local ann = {}
alternant_multiword_spec.annotation = ann
ann.form = {}
ann.weakness = {}
ann.vowels = {}
ann.passive = nil
ann.reduced = {}
ann.irreg = {}
ann.defective = {}
local multiword_lemma = false
for _, slot in ipairs(export.potential_lemma_slots) do
if alternant_multiword_spec.forms[slot] then
for _, formobj in ipairs(alternant_multiword_spec.forms[slot]) do
if formobj.form:find(" ") then
multiword_lemma = true
break
end
end
break
end
end
local function insert_ann(anntype, value)
m_table.insertIfNot(alternant_multiword_spec.annotation[anntype], value)
end
local function insert_cat(cat, also_when_multiword)
-- Don't place multiword terms in categories like 'Arabic form-II verbs' to avoid spamming the categories with
-- such terms.
if also_when_multiword or not multiword_lemma then
m_table.insertIfNot(alternant_multiword_spec.categories, "Arabic " .. cat)
end
end
iut.map_word_specs(alternant_multiword_spec, function(base)
add_categories_and_annotation(alternant_multiword_spec, base, multiword_lemma, insert_ann, insert_cat)
end)
for slot, name in pairs(slots_that_may_be_uncertain) do
if alternant_multiword_spec.forms[slot] then
for _, form in ipairs(alternant_multiword_spec.forms[slot]) do
if form.uncertain then
if form.form == "?" then
insert_cat(("verbs with explicitly unknown %ss"):format(name))
else
insert_cat(("verbs needing %s checked"):format(name))
end
break
end
end
end
end
if alternant_multiword_spec.has_active then
if alternant_multiword_spec.has_passive and alternant_multiword_spec.has_non_impers_passive then
insert_cat("verbs with full passive")
ann.passive = "full passive"
elseif alternant_multiword_spec.has_passive then
insert_cat("verbs with impersonal passive")
ann.passive = "impersonal passive"
else
insert_cat("verbs lacking passive forms")
ann.passive = "no passive"
end
else
if alternant_multiword_spec.has_non_impers_passive then
insert_cat("passive verbs")
insert_cat("verbs with full passive")
ann.passive = "passive-only"
else
insert_cat("passive verbs")
insert_cat("impersonal verbs")
insert_cat("verbs with impersonal passive")
ann.passive = "impersonal (passive-only)"
end
end
if alternant_multiword_spec.passive_uncertain then
insert_cat("verbs needing passive checked")
ann.passive = ann.passive .. ' <abbr title="passive status uncertain">(?)</abbr>'
end
if alternant_multiword_spec.has_active and not alternant_multiword_spec.has_imp then
insert_ann("defective", "no imperative")
insert_cat("verbs lacking imperative forms")
end
if not alternant_multiword_spec.has_past then
insert_ann("defective", "no past")
insert_cat("verbs lacking past forms")
end
if not alternant_multiword_spec.has_nonpast then
insert_ann("defective", "no non-past")
insert_cat("verbs lacking non-past forms")
end
local ann_parts = {}
local function insert_ann_part(part, conj)
local val = concat(ann[part], conj or " or ")
if val ~= "" and val ~= "regular" then
table.insert(ann_parts, val)
end
end
insert_ann_part("form")
insert_ann_part("weakness")
insert_ann_part("reduced")
insert_ann_part("vowels")
if ann.passive then
table.insert(ann_parts, ann.passive)
end
insert_ann_part("irreg")
insert_ann_part("defective", ", ")
alternant_multiword_spec.annotation = concat(ann_parts, ", ")
end
local function show_forms(alternant_multiword_spec)
local lemmas = {}
for _, slot in ipairs(export.potential_lemma_slots) do
if alternant_multiword_spec.forms[slot] then
for _, formobj in ipairs(alternant_multiword_spec.forms[slot]) do
table.insert(lemmas, formobj)
end
break
end
end
alternant_multiword_spec.lemmas = lemmas -- save for later use in make_table()
alternant_multiword_spec.vn = alternant_multiword_spec.forms.vn -- save for later use in make_table()
-- Reconstruct the original verb spec without overrides for verbal nouns and participles, since those specific slots
-- are ignored by {{ar-verb form}}. Compute this once beforehand; `transform_accel_obj` is called repeatedly on each
-- form and we don't want to compute this repeatedly.
local reconstructed_verb_spec = iut.reconstruct_original_spec(alternant_multiword_spec, {
preprocess_angle_bracket_spec = function(spec)
spec = spec:match("^<(.*)>$")
assert(spec)
local segments = put.parse_multi_delimiter_balanced_segment_run(spec, {{"[", "]"}, {"<", ">"}})
local dot_separated_groups = put.split_alternating_runs_and_strip_spaces(segments, "%.")
-- Rejoin each dot-separated group into a single string, since we aren't actually going to do any parsing
-- of bracket-bounded textual runs; then filter out overrides for verbal nouns and participles.
local filtered_indicators = {}
for _, dot_separated_group in ipairs(dot_separated_groups) do
local indicator = concat(dot_separated_group)
-- FIXME: Do we want to filter out any other indicators?
if not (indicator:find("^vn:") or indicator:find("^[ap]p:")) then
table.insert(filtered_indicators, indicator)
end
end
return ("<%s>"):format(concat(filtered_indicators, "."))
end,
})
-- If we're dealing with a single word, no alternants and a single verb form, use the auto-conjugation-fetching
-- variant.
local reconstructed_lemma, inside = reconstructed_verb_spec:match("^([^ <>()]+)(%b<>)$")
if inside and alternant_multiword_spec.verb_forms[1] and not alternant_multiword_spec.verb_forms[2] then
reconstructed_verb_spec = ("+%s<%s>"):format(reconstructed_lemma, alternant_multiword_spec.verb_forms[1])
end
local function transform_accel_obj(slot, formobj, accel_obj)
if not accel_obj then
return accel_obj
end
if slot == "ap" or slot == "pp" or slot == "vn" then
-- FIXME: [[Module:accel]] can't correctly handle more than one verb form for participles and verbal nouns
accel_obj.form = slot .. "-" .. concat(alternant_multiword_spec.verb_forms, ",")
else
accel_obj.form = "verb-form-" .. reconstructed_verb_spec
end
return accel_obj
end
local function generate_link(data)
local form = data.form
local term = form.formval_for_link
local alt = form.alt
if term == "?" then
term = nil
alt = "?"
end
local link = m_links.full_link {
lang = lang, term = term, tr = "-", accel = form.accel_obj,
alt = alt, gloss = form.gloss, genders = form.genders, pos = form.pos, lit = form.lit, id = form.id,
} .. iut.get_footnote_text(form.footnotes, data.footnote_obj)
if form.q and form.q[1] or form.qq and form.qq[1] or form.l and form.l[1] or form.ll and form.ll[1] then
link = require(decorations_module).format_decorations {
lang = lang,
text = link,
q = form.q,
qq = form.qq,
l = form.l,
ll = form.ll,
}
end
return link
end
local props = {
lang = lang,
lemmas = lemmas,
transform_accel_obj = transform_accel_obj,
generate_link = generate_link,
slot_list = alternant_multiword_spec.verb_slots,
include_translit = true,
}
iut.show_forms(alternant_multiword_spec.forms, props)
end
-------------------------------------------------------------------------------
-- Functions to create inflection tables --
-------------------------------------------------------------------------------
-- Make the conjugation table. Called from export.show().
local function make_table(alternant_multiword_spec)
local text = m_inflection_table.make_top{
title = 'Pengkonjugatan bagi {title}',
tall = 'yes',
palette = "green",
category = 'Pengkonjugatan',
class = 'tr-alongside', -- temp hack to prevent extra line break
}
text = text .. [=[
! colspan="6" | kata nama kerjaan<br /><<الْمَصْدَر>>
| colspan="7" | {vn}
]=]
if alternant_multiword_spec.has_active then
text = text .. [=[
|-
! colspan="6" | active participle<br /><<اِسْم الْفَاعِل>>
| colspan="7" | {ap}
]=]
end
if alternant_multiword_spec.has_passive then
text = text .. [=[
|-
! colspan="6" | passive participle<br /><<اِسْم الْمَفْعُول>>
| colspan="7" | {pp}
]=]
end
text = text .. [=[
|-
! colspan="999" class="separator" |
]=]
if alternant_multiword_spec.has_active then
text = text .. [=[
|-
! colspan="13" class="outer" | active voice<br /><<الْفِعْل الْمَعْلُوم>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="12" class="separator" |
! colspan="3" | dual<br /><<الْمُثَنَّى>>
! rowspan="12" class="separator" |
! colspan="3"| plural<br /><<الْجَمْع>>
|-
! colspan="2"|
! 1<sup>st</sup> person<br /><<الْمُتَكَلِّم>>
! 2<sup>nd</sup> person<br /><<الْمُخَاطَب>>
! 3<sup>rd</sup> person<br /><<الْغَائِب>>
! 1<sup>st</sup> person<br /><<الْمُتَكَلِّم>>
! 2<sup>nd</sup> person<br /><<الْمُخَاطَب>>
! 3<sup>rd</sup> person<br /><<الْغَائِب>>
! 1<sup>st</sup> person<br /><<الْمُتَكَلِّم>>
! 2<sup>nd</sup> person<br /><<الْمُخَاطَب>>
! 3<sup>rd</sup> person<br /><<الْغَائِب>>
|-
! rowspan="2" | past indicative<br />(perfect)<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_1s}
| {past_2ms}
| {past_3ms}
| rowspan="2" | {past_1d}
| rowspan="2" | {past_2d}
| {past_3md}
| rowspan="2" | {past_1p}
| {past_2mp}
| {past_3mp}
|-
! class="secondary" | f
| {past_2fs}
| {past_3fs}
| {past_3fd}
| {past_2fp}
| {past_3fp}
|-
! rowspan="2" | non-past indicative<br />(imperfect)<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_1s}
| {ind_2ms}
| {ind_3ms}
| rowspan="2" | {ind_1d}
| rowspan="2" | {ind_2d}
| {ind_3md}
| rowspan="2" | {ind_1p}
| {ind_2mp}
| {ind_3mp}
|-
! class="secondary" | f
| {ind_2fs}
| {ind_3fs}
| {ind_3fd}
| {ind_2fp}
| {ind_3fp}
|-
! rowspan="2" | subjunctive<br /><<الْمُضَارِع الْمَنْصُوب>>
! class="secondary" | m
| rowspan="2" | {sub_1s}
| {sub_2ms}
| {sub_3ms}
| rowspan="2" | {sub_1d}
| rowspan="2" | {sub_2d}
| {sub_3md}
| rowspan="2" | {sub_1p}
| {sub_2mp}
| {sub_3mp}
|-
! class="secondary" | f
| {sub_2fs}
| {sub_3fs}
| {sub_3fd}
| {sub_2fp}
| {sub_3fp}
|-
! rowspan="2" | jussive<br /><<الْمُضَارِع الْمَجْزُوم>>
! class="secondary" | m
| rowspan="2" | {juss_1s}
| {juss_2ms}
| {juss_3ms}
| rowspan="2" | {juss_1d}
| rowspan="2" | {juss_2d}
| {juss_3md}
| rowspan="2" | {juss_1p}
| {juss_2mp}
| {juss_3mp}
|-
! class="secondary" | f
| {juss_2fs}
| {juss_3fs}
| {juss_3fd}
| {juss_2fp}
| {juss_3fp}
|-
! rowspan="2" | imperative<br /><<الْأَمْر>>
! class="secondary" | m
| rowspan="2" |
| {imp_2ms}
| rowspan="2" |
| rowspan="2" |
| rowspan="2" | {imp_2d}
| rowspan="2" |
| rowspan="2" |
| {imp_2mp}
| rowspan="2" |
|-
! class="secondary" | f
| {imp_2fs}
| {imp_2fp}
]=]
end
if alternant_multiword_spec.has_passive then
text = text .. [=[
|-
! colspan="999" class="separator" |
|-
! colspan="13" class="outer" | passive voice<br /><<الْفِعْل الْمَجْهُول>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="10" class="separator" |
! colspan="3" | dual<br /><<الْمُثَنَّى>>
! rowspan="10" class="separator" |
! colspan="3" | plural<br /><<الْجَمْع>>
|-
! colspan="2" |
! 1<sup>st</sup> person<br /><<الْمُتَكَلِّم>>
! 2<sup>nd</sup> person<br /><<الْمُخَاطَب>>
! 3<sup>rd</sup> person<br /><<الْغَائِب>>
! 1<sup>st</sup> person<br /><<الْمُتَكَلِّم>>
! 2<sup>nd</sup> person<br /><<الْمُخَاطَب>>
! 3<sup>rd</sup> person<br /><<الْغَائِب>>
! 1<sup>st</sup> person<br /><<الْمُتَكَلِّم>>
! 2<sup>nd</sup> person<br /><<الْمُخَاطَب>>
! 3<sup>rd</sup> person<br /><<الْغَائِب>>
|-
! rowspan="2" | past indicative<br />(perfect)<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_pass_1s}
| {past_pass_2ms}
| {past_pass_3ms}
| rowspan="2" | {past_pass_1d}
| rowspan="2" | {past_pass_2d}
| {past_pass_3md}
| rowspan="2" | {past_pass_1p}
| {past_pass_2mp}
| {past_pass_3mp}
|-
! class="secondary" | f
| {past_pass_2fs}
| {past_pass_3fs}
| {past_pass_3fd}
| {past_pass_2fp}
| {past_pass_3fp}
|-
! rowspan="2" | non-past indicative<br />(imperfect)<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_pass_1s}
| {ind_pass_2ms}
| {ind_pass_3ms}
| rowspan="2" | {ind_pass_1d}
| rowspan="2" | {ind_pass_2d}
| {ind_pass_3md}
| rowspan="2" | {ind_pass_1p}
| {ind_pass_2mp}
| {ind_pass_3mp}
|-
! class="secondary" | f
| {ind_pass_2fs}
| {ind_pass_3fs}
| {ind_pass_3fd}
| {ind_pass_2fp}
| {ind_pass_3fp}
|-
! rowspan="2" | subjunctive<br /><<الْمُضَارِع الْمَنْصُوب>>
! class="secondary" | m
| rowspan="2" | {sub_pass_1s}
| {sub_pass_2ms}
| {sub_pass_3ms}
| rowspan="2" | {sub_pass_1d}
| rowspan="2" | {sub_pass_2d}
| {sub_pass_3md}
| rowspan="2" | {sub_pass_1p}
| {sub_pass_2mp}
| {sub_pass_3mp}
|-
! class="secondary" | f
| {sub_pass_2fs}
| {sub_pass_3fs}
| {sub_pass_3fd}
| {sub_pass_2fp}
| {sub_pass_3fp}
|-
! rowspan="2" | jussive<br /><<الْمُضَارِع الْمَجْزُوم>>
! class="secondary" | m
| rowspan="2" | {juss_pass_1s}
| {juss_pass_2ms}
| {juss_pass_3ms}
| rowspan="2" | {juss_pass_1d}
| rowspan="2" | {juss_pass_2d}
| {juss_pass_3md}
| rowspan="2" | {juss_pass_1p}
| {juss_pass_2mp}
| {juss_pass_3mp}
|-
! class="secondary" | f
| {juss_pass_2fs}
| {juss_pass_3fs}
| {juss_pass_3fd}
| {juss_pass_2fp}
| {juss_pass_3fp}
]=]
end
text = text .. m_inflection_table.make_bottom{
notes = '{footnote}',
}
local forms = alternant_multiword_spec.forms
if not alternant_multiword_spec.lemmas then
forms.title = "—"
else
local linked_lemmas = {}
for _, form in ipairs(alternant_multiword_spec.lemmas) do
table.insert(linked_lemmas, link_term(form.form, "term"))
end
forms.title = concat(linked_lemmas, ", ")
end
local ann_parts = {}
if alternant_multiword_spec.annotation ~= "" then
table.insert(ann_parts, alternant_multiword_spec.annotation)
end
if alternant_multiword_spec.vn then
local linked_vns = {}
for _, form in ipairs(alternant_multiword_spec.vn) do
table.insert(linked_vns, link_term(form.form, "term"))
end
table.insert(ann_parts, (#linked_vns > 1 and "verbal nouns" or "verbal noun") .. " " ..
concat(linked_vns, ", "))
end
local annotation = concat(ann_parts, ", ")
if annotation ~= "" then
forms.title = forms.title .. " (" .. annotation .. ")"
end
-- Format the table.
local tagged_table = rsub(text, "<<(.-)>>", tag_text)
return m_string_utilities.format(tagged_table, forms)
end
-------------------------------------------------------------------------------
-- External entry points --
-------------------------------------------------------------------------------
-- Append two lists `l1` and `l2`, removing duplicates. If either is {nil}, just return the other.
local function combine_lists(l1, l2)
-- combine_footnotes() does exactly what we want.
return iut.combine_footnotes(l1, l2)
end
local function combine_metadata(data)
local src1 = data.form1
local src2 = data.form2
local dest = data.dest_form
dest.uncertain = src1.uncertain or src2.uncertain
if src1.genders and src2.genders and not m_table.deepEquals(src1.genders, src2.genders) then
-- do nothing
else
dest.genders = src1.genders or src2.genders
end
if src1.pos and src2.pos and src1.pos ~= src2.pos then
-- do nothing
else
dest.pos = src1.pos or src2.pos
end
-- Don't copy .alt, .gloss, .lit, .id, which describe a single term and don't extend to multiword terms.
dest.q = combine_lists(src1.q, src2.q)
dest.qq = combine_lists(src1.qq, src2.qq)
dest.l = combine_lists(src1.l, src2.l)
dest.ll = combine_lists(src1.ll, src2.ll)
end
-- Externally callable function to parse and conjugate a verb given user-specified arguments.
-- Return value is WORD_SPEC, an object where the conjugated forms are in `WORD_SPEC.forms`
-- for each slot. If there are no values for a slot, the slot key will be missing. The value
-- for a given slot is a list of objects {form=FORM, footnotes=FOOTNOTES}.
function export.do_generate_forms(args, source_template, headword_head)
local PAGENAME = mw.loadData("Module:headword/data").pagename
local function in_template_space()
return mw.title.getCurrentTitle().nsText == "Templat"
end
-- Determine the verb spec we're being asked to generate the conjugation of. This may be taken from the current page
-- title or the value of |pagename=; but not when called from {{ar-verb form}}, where the page title is a
-- non-lemma form. Note that the verb spec may omit the lemma; e.g. it may be "<II>". For this reason, we use the
-- value of `pagename` computed here down below, when calling normalize_all_lemmas().
local pagename = source_template ~= "ar-verb form" and args.pagename or PAGENAME
local head = headword_head or pagename
local arg1 = args[1]
if not arg1 then
if (pagename == "ar-conj" or pagename == "ar-verb" or pagename == "ar-verb form") and in_template_space() then
arg1 = "كتب<I/a~u.pass>"
else
arg1 = "<>"
end
end
-- When called from {{ar-verb form}}, determine the non-lemma form whose inflections we're being asked to
-- determine. This normally comes from the page title or the value of |pagename=.
local verb_form_of_form
if source_template == "ar-verb form" then
verb_form_of_form = args.pagename
if not verb_form_of_form then
if PAGENAME == "ar-verb form" and in_template_space() then
verb_form_of_form = "كتبت"
else
verb_form_of_form = PAGENAME
end
end
end
local incorporated_headword_head_into_lemma = false
if arg1:find("^<.*>$") then -- missing lemma
if head:find(" ") then
-- If multiword lemma, try to add arg spec after the first word.
-- Try to preserve the brackets in the part after the verb, but don't do it
-- if there aren't the same number of left and right brackets in the verb
-- (which means the verb was linked as part of a larger expression).
local first_word, post = rmatch(head, "^(.-)( .*)$")
local left_brackets = rsub(first_word, "[^%[]", "")
local right_brackets = rsub(first_word, "[^%]]", "")
if #left_brackets == #right_brackets then
arg1 = iut.remove_redundant_links(first_word) .. arg1 .. post
incorporated_headword_head_into_lemma = true
else
-- Try again using the form without links.
local linkless_head = m_links.remove_links(head)
if linkless_head:find(" ") then
first_word, post = rmatch(linkless_head, "^(.-)( .*)$")
arg1 = first_word .. arg1 .. post
else
error("Unable to incorporate <...> spec into explicit head due to a multiword linked verb or " ..
"unbalanced brackets; please include <> explicitly: " .. arg1)
end
end
else
-- Will be incorporated through `head` below in the call to normalize_all_lemmas().
incorporated_headword_head_into_lemma = true
end
end
local parse_props = {
parse_indicator_spec = parse_indicator_spec,
angle_brackets_omittable = true,
allow_blank_lemma = true,
}
local alternant_multiword_spec = iut.parse_inflected_text(arg1, parse_props)
alternant_multiword_spec.pos = pos or "verbs"
alternant_multiword_spec.args = args
alternant_multiword_spec.source_template = source_template
alternant_multiword_spec.verb_form_of_form = verb_form_of_form
alternant_multiword_spec.incorporated_headword_head_into_lemma = incorporated_headword_head_into_lemma
normalize_all_lemmas(alternant_multiword_spec, head)
detect_all_indicator_specs(alternant_multiword_spec)
local inflect_props = {
lang = lang,
slot_list = alternant_multiword_spec.verb_slots,
inflect_word_spec = conjugate_verb,
combine_metadata = combine_metadata,
-- We add links around the generated verbal forms rather than allow the entire multiword
-- expression to be a link, so ensure that user-specified links get included as well.
include_user_specified_links = true,
}
iut.inflect_multiword_or_alternant_multiword_spec(alternant_multiword_spec, inflect_props)
if debug_translit then
for slot, forms in pairs(alternant_multiword_spec.forms) do
for _, form in ipairs(forms) do
if form.translit then
local full_form_translit = (lang:transliterate(m_links.remove_links(form.form)))
if full_form_translit ~= form.translit then
error(("Internal error: For slot '%s', form '%s' incremental translit '%s' not same as full translit '%s'"):
format(slot, form.form, form.translit, full_form_translit))
end
end
form.form = iut.remove_redundant_links(form.form)
end
end
end
-- Remove redundant brackets around entire forms.
for slot, forms in pairs(alternant_multiword_spec.forms) do
for _, form in ipairs(forms) do
form.form = iut.remove_redundant_links(form.form)
end
end
determine_slot_uncertainty_from_forms(alternant_multiword_spec)
determine_verb_properties_from_forms(alternant_multiword_spec)
compute_categories_and_annotation(alternant_multiword_spec)
if args.json and source_template == "ar-conj" then
-- There is a circular reference in `base.alternant_multiword_spec`, which points back to top level.
iut.map_word_specs(alternant_multiword_spec, function(base)
base.alternant_multiword_spec = nil
end)
return require("Module:JSON").toJSON(alternant_multiword_spec)
end
return alternant_multiword_spec
end
-- Entry point for {{ar-conj}}. Template-callable function to parse and conjugate a verb given
-- user-specified arguments and generate a displayable table of the conjugated forms.
function export.show(frame)
local parent_args = frame:getParent().args
local params = {
[1] = {},
["noautolinktext"] = {type = "boolean"},
["noautolinkverb"] = {type = "boolean"},
["t"] = {}, -- for use by {{ar-verb form}}; otherwise ignored
["id"] = {}, -- for use by {{ar-verb form}}; otherwise ignored
["pagename"] = {}, -- for testing/documentation pages
["json"] = {type = "boolean"}, -- for bot use
}
local args = require("Module:parameters").process(parent_args, params)
local alternant_multiword_spec = export.do_generate_forms(args, "ar-conj")
if type(alternant_multiword_spec) == "string" then
-- JSON return value
return alternant_multiword_spec
end
show_forms(alternant_multiword_spec)
return make_table(alternant_multiword_spec) ..
require("Module:utilities").format_categories(alternant_multiword_spec.categories, lang, nil, nil, force_cat)
end
function export.verb_forms(frame)
local parargs = frame:getParent().args
local params = {
[1] = {},
[2] = {},
[3] = {},
[4] = {},
[5] = {},
pagename = {},
}
for _, form in ipairs(allowed_vforms) do
-- FIXME: We go up to 5 here. The code supports unlimited variants but it's unlikely we will ever see more than
-- 2.
for index = 1, 5 do
local prefix = index == 1 and form or form .. index
params[prefix .. "-pv"] = {}
for _, extn in ipairs { "", "-vn", "-ap", "-pp" } do
params[prefix .. extn] = {}
params[prefix .. extn .. "-head"] = {}
-- FIXME: No -tr?
params[prefix .. extn .. "-gloss"] = {}
end
end
end
local args = require("Module:parameters").process(parargs, params)
local i = 1
local past_vowel_re = "^[aui,]*$"
local combined_root = nil
if not args[i] or rfind(args[i], past_vowel_re) then
combined_root = args.pagename or mw.loadData("Module:headword/data").pagename
if not rfind(combined_root, "^([^ ]) ([^ ]) ([^ ])$") and not
rfind(combined_root, "^([^ ]) ([^ ]) ([^ ]) ([^ ])$") then
error("When inferring roots from page title, need three or four space-separated radicals: " .. combined_root)
end
elseif rfind(args[i], " ") then
combined_root = args[i]
i = i + 1
else
local separate_roots = {}
while args[i] and not rfind(args[i], past_vowel_re) do
table.insert(separate_roots, args[i])
i = i + 1
end
combined_root = concat(separate_roots, " ")
end
local past_vowel = args[i]
i = i + 1
if past_vowel and not rfind(past_vowel, past_vowel_re) then
error("Unrecognized past vowel, should be 'a', 'i', 'u', 'a,u', etc. or empty: " .. past_vowel)
end
-- Spaces interfere with parsing as a unit in [[Module:inflection utilities]], so replace with underscore.
combined_root = combined_root:gsub(" ", "_")
local split_root = rsplit(combined_root, "_")
-- Map from verb forms (I, II, etc.) to a table of verb properties,
-- which has entries e.g. for "verb" (either true to autogenerate the verb
-- head, or an explicitly specified verb head using e.g. argument "I-head"),
-- and for "verb-gloss" (which comes from e.g. the argument "I" or "I-gloss"),
-- and for "vn" and "vn-gloss", "ap" and "ap-gloss", "pp" and "pp-gloss".
local verb_properties = {}
for _, form in ipairs(allowed_vforms) do
local formpropslist = {}
local derivs = {{"verb", ""}, {"vn", "-vn"}, {"ap", "-ap"}, {"pp", "-pp"}}
local index = 1
while true do
local formprops = {}
local prefix = index == 1 and form or form .. index
if prefix == "I" then
formprops.pv = past_vowel
end
if args[prefix .. "-pv"] then
formprops.pv = args[prefix .. "-pv"]
end
for _, deriv in ipairs(derivs) do
local prop = deriv[1]
local extn = deriv[2]
if args[prefix .. extn] == "+" then
formprops[prop] = true
elseif args[prefix .. extn] == "-" then
formprops[prop] = false
elseif args[prefix .. extn] then
formprops[prop] = true
formprops[prop .. "-gloss"] = args[prefix .. extn]
end
if args[prefix .. extn .. "-head"] then
if formprops[prop] == nil then
formprops[prop] = true
end
formprops[prop] = args[prefix .. extn .. "-head"]
end
if args[prefix .. extn .. "-gloss"] then
if formprops[prop] == nil then
formprops[prop] = true
end
formprops[prop .. "-gloss"] = args[prefix .. extn .. "-gloss"]
end
end
if formprops.verb then
-- If a verb form specified, also turn on vn (unless form I, with
-- unpredictable vn) and ap, and maybe pp, according to form,
-- weakness and past vowel. But don't turn these on if there's
-- an explicit on/off specification for them (e.g. I-pp=-).
if form ~= "I" and formprops.vn == nil then
formprops.vn = true
end
if formprops.ap == nil then
formprops.ap = true
end
local weakness = weakness_from_radicals(form, split_root[1], split_root[2], split_root[3],
split_root[4])
if formprops.pp == nil and not vform_probably_no_passive(form,
weakness, rsplit(formprops.pv or "", ","), {}) then
formprops.pp = true
end
if formprops.verb == true or formprops.vn == true or formprops.ap == true or formprops.pp == true then
formprops.need_autogen = true
end
table.insert(formpropslist, formprops)
index = index + 1
else
break
end
end
table.insert(verb_properties, {form, formpropslist})
end
-- Go through and create the verb form derivations as necessary, when they haven't been explicitly given.
for _, vplist in ipairs(verb_properties) do
local vform = vplist[1]
for _, props in ipairs(vplist[2]) do
if props.need_autogen then
local form_with_vowels
if vform == "I" then
local pv = props.pv
if not pv then
-- Make up likely past vowels based on weakness and actual radical.
if split_root[3] == W then -- final-weak
form_with_vowels = "I/a~u"
elseif split_root[3] == Y then
form_with_vowels = "I/a~i"
elseif split_root[2] == W then --hollow
form_with_vowels = "I/u~u"
elseif split_root[2] == Y then
form_with_vowels = "I/i~i"
else
-- most common; doesn't matter so much since we're not displaying the non-past
form_with_vowels = "I/a~u"
end
else
local pvs = rsplit(pv, ",")
local vowel_sufs = {}
for _, pv in ipairs(pvs) do
local vowel_spec
if pv == "a" then
-- Make up likely past vowels based on weakness and actual radical.
if split_root[3] == W then -- final-weak
vowel_spec = "a~u"
elseif split_root[3] == Y then
vowel_spec = "a~i"
elseif split_root[2] == W then --hollow
vowel_spec = "a~u"
elseif split_root[2] == Y then
vowel_spec = "a~i"
else
-- most common; doesn't matter so much since we're not displaying the non-past
vowel_spec = "a~u"
end
elseif pv == "i" then
-- most common; doesn't matter so much since we're not displaying the non-past
vowel_spec = "i~a"
elseif pv == "u" then
-- most common; doesn't matter so much since we're not displaying the non-past
vowel_spec = "u~u"
else
error(("Internal error: Bad past vowel '%s' in {{ar-verb forms}}"):format(pv))
end
table.insert(vowel_sufs, vowel_spec)
end
form_with_vowels = "I/" .. concat(vowel_sufs, "/")
end
else
form_with_vowels = vform
end
local angle_bracket_spec = ("%s<%s.pass>"):format(combined_root, form_with_vowels)
local alternant_multiword_spec = export.do_generate_forms({angle_bracket_spec}, "ar-verb forms")
local function format_forms(forms)
if not forms then
return "-" -- FIXME: Throw an error?
end
local formatted = {}
for _, form in ipairs(forms) do
if form.translit then
table.insert(formatted, ("%s//%s"):format(form.form, form.translit))
else
table.insert(formatted, form.form)
end
end
return concat(formatted, ",")
end
if props.verb == true then
props.verb = format_forms(alternant_multiword_spec.forms.past_3ms)
end
for _, deriv in ipairs({"vn", "ap", "pp"}) do
if props[deriv] == true then
props[deriv] = format_forms(alternant_multiword_spec.forms[deriv])
end
end
end
end
end
-- Go through and output the result
local formtextarr = {}
for _, vplist in ipairs(verb_properties) do
local form = vplist[1]
for _, props in ipairs(vplist[2]) do
local textarr = {}
if props.verb then
local text = "* '''[[Lampiran:Kata kerja bahasa Arab#Bentuk " .. form .. "|Bentuk " .. form .. "]]''': "
local linktext = {}
local splitheads = rsplit(props.verb, "[,،]")
for _, head in ipairs(splitheads) do
table.insert(linktext, m_links.full_link({lang = lang, term = head, gloss = props["verb-gloss"]}))
end
text = text .. concat(linktext, ", ")
table.insert(textarr, text)
for _, derivengl in ipairs({{"vn", "Kata nama kerjaan"}, {"ap", "Active participle"}, {"pp", "Passive participle"}}) do
local deriv = derivengl[1]
local engl = derivengl[2]
if props[deriv] then
local text = "** " .. engl .. ": "
local linktext = {}
local splitheads = rsplit(props[deriv], "[,،]")
for _, head in ipairs(splitheads) do
local ar, translit = head:match("^(.*)//(.-)$")
if not ar then
ar = head
end
table.insert(linktext, m_links.full_link {lang = lang, term = ar, tr = translit,
gloss = props[deriv .. "-gloss"]} )
end
text = text .. concat(linktext, ", ")
table.insert(textarr, text)
end
end
table.insert(formtextarr, concat(textarr, "\n"))
end
end
end
return concat(formtextarr, "\n")
end
-- Infer radicals from lemma headword (i.e. 3rd masculine singular past) and verb form (I, II, etc.). Throw an error if
-- headword is malformed. A given returned radical may be actually be a list of possible radicals, where the first one
-- should be used if the user didn't explicitly give the radical. If the list contains a field `ambig = true`, the
-- radical is considered ambiguous and should not be categorized. `is_reduced` indicates that the user specified
-- `.reduced` to indicate that the verb form is reduced by assimilation and/or haplology (typically archaic Koranic
-- forms such as اِدَّارَأَ instead of تَدَارَأَ; or اِسْطَاعَ instead of اِسْتِطَاعَ; etc.
function export.infer_radicals(data)
local headword, vform, passive, past_vowel, nonpast_vowel, is_reduced =
data.headword, data.vform, data.passive, data.past_vowel, data.nonpast_vowel, data.is_reduced
past_vowel = past_vowel or "-"
nonpast_vowel = nonpast_vowel or "-"
local function verify_vowel(vowel, param)
if vowel ~= A and vowel ~= I and vowel ~= U and vowel ~= "-" then
error(("Internal error: Bad value for %s: %s (should be Arabic diacritic vowel or '-')"):format(
param, vowel))
end
end
verify_vowel(past_vowel, "past_vowel")
verify_vowel(nonpast_vowel, "nonpast_vowel")
local ch = {}
local form_viii_assim, variant
-- sub out alif-madda for easier processing
headword = rsub(headword, AMAD, HAMZA .. ALIF)
local function infer_err(msg, noann)
local anns = {}
local nohead, novform
if noann == "nohead" then
nohead = true
elseif noann == "novform" then
novform = true
elseif noann == "nohead-vform" then
nohead = true
novform = true
elseif noann then
error(("Internal error: Unrecognized value for 'noann': %s"):format(dump(noann)))
end
if not nohead then
table.insert(anns, ("headword=%s"):format(data.headword))
end
if not novform then
table.insert(anns, ("verb form=%s"):format(data.vform))
end
anns = concat(anns, ", ")
if anns ~= "" then
anns = ": " .. anns
end
error(msg .. anns)
end
local len = ulen(headword)
local expected_length
-- extract the headword letters into an array
for i = 1, len do
table.insert(ch, usub(headword, i, i))
end
-- check that the letter at the given index is the given string, or
-- is one of the members of the given array
local function check(index, must)
local letter = ch[index]
if type(must) == "string" then
if not letter then
infer_err("Letter " .. index .. " is nil")
end
if letter ~= must then
infer_err(("For verb form %s, letter %s must be %s, not %s"):format(vform, index, must, letter),
"novform")
end
elseif not m_table.contains(must, letter) then
infer_err("For verb form " .. vform .. ", radical " .. index ..
" must be one of " .. concat(must, " ") .. ", not " .. letter, "novform")
end
end
-- Check that length of headword is within [min, max]
local function check_len(min, max)
if min and len < min then
infer_err(("Not enough letters for verb form %s, expected at least %s"):format(vform, min), "novform")
end
if max and len > max then
infer_err(("Too many letters for verb form %s, expected at most %s"):format(vform, max), "novform")
end
end
-- If the vowels are i~a or u~u, a form I verb beginning with w- normally keeps the w in the non-past. Otherwise it
-- loses it (i.e. it is "assimilated").
local function form_I_w_non_assimilated()
return req(past_vowel, I) and req(nonpast_vowel, A) or req(past_vowel, U) and req(nonpast_vowel, U)
end
-- Convert radicals to canonical form (handle various hamza varieties and check for misplaced alif or alif maqṣūra;
-- legitimate cases of these letters are handled above).
local function convert(rad, index)
if type(rad) == "table" then
for i, r in ipairs(rad) do
rad[i] = convert(r, index)
end
return rad
elseif rad == HAMZA_ON_ALIF or rad == HAMZA_UNDER_ALIF or
rad == HAMZA_ON_W or rad == HAMZA_ON_Y then
return HAMZA
elseif rad == AMAQ then
infer_err("Radical " .. index .. " must not be alif maqṣūra")
elseif rad == ALIF then
infer_err("Radical " .. index .. " must not be alif")
else
return rad
end
end
local quadlit = vform:find("q$")
-- find first radical, start of second/third radicals, check for
-- required letters
local radstart, rad1, rad2, rad3, rad4
local weakness
if vform == "I" or vform == "II" then
rad1 = ch[1]
radstart = 2
elseif vform == "III" then
rad1 = ch[1]
check(2, {ALIF, W}) -- W occurs in passive-only verbs
radstart = 3
elseif vform == "IV" then
-- this would be alif-madda but we replaced it with hamza-alif above.
if ch[1] == HAMZA and ch[2] == ALIF then
rad1 = HAMZA
else
check(1, HAMZA_ON_ALIF)
rad1 = ch[2]
end
radstart = 3
elseif vform == "V" then
check(1, is_reduced and ALIF or T)
rad1 = ch[2]
radstart = 3
elseif vform == "VI" then
check(1, is_reduced and ALIF or T)
if ch[2] == AMAD then
rad1 = HAMZA
radstart = 3
else
rad1 = ch[2]
check(3, {ALIF, W}) -- W occurs in passive-only verbs
radstart = 4
end
elseif vform == "VII" then
check(1, ALIF)
if is_reduced then
check(2, M)
rad1 = M
radstart = 3
else
check(2, N)
rad1 = ch[3]
radstart = 4
end
elseif vform == "VIII" then
check(1, ALIF)
rad1 = ch[2]
if rad1 == "د" then
rad1 = {"د", "ذ"} -- not considered ambiguous since it's usually د
radstart = 3
form_viii_assim = "دّ"
elseif rad1 == "ظ" and ch[3] == "ط" and len >= 5 then
-- [[اظطلم]], variant of [[اظلم]]
radstart = 4
form_viii_assim = "ظْط"
elseif rad1 == "ذ" and ch[3] == "د" and len >= 5 then
-- [[اذدكر]], variant of [[اذكر]]
radstart = 4
form_viii_assim = "ذْد"
elseif rad1 == T or rad1 == "ث" or rad1 == "ذ" or rad1 == "ط" or rad1 == "ظ" then
radstart = 3
form_viii_assim = rad1 .. SH
elseif rad1 == "ز" then
check(3, "د")
radstart = 4
form_viii_assim = "زْد"
elseif rad1 == "ص" or rad1 == "ض" then
check(3, "ط")
radstart = 4
form_viii_assim = rad1 .. SK .. "ط"
else
check(3, T)
radstart = 4
rad1 = convert(rad1, 1)
form_viii_assim = rad1 .. SK .. "ت"
end
if rad1 == T then
-- Radical is ambiguous, might be ت or و or ي but doesn't affect conjugation. Note that there are no
-- form-VIII verbs with initial radical ي given in Hans Wehr but Lane mentions at least:
-- - (page 2973) اِتَّأَسَ, with assimilation of the ي to ت, from root ي ء س;
-- - (page 2975) اِتَّبَسَ non-past يَتَّبِسُ and alternative اِيتَبَسَ non-past يَاتَبِسُ from the root ي ب س;
-- - (page 2976) اِتَّسَرَ non-past يَتَّسِرُ or alternatively يَأْتَسِرُ with hamza preserved from the root ي س ر.
-- These alternative forms seem very rare and probably not worth worrying about, but if we want to handle
-- them, we can do it when the time comes.
rad1 = {T, W, Y, ambig = true}
-- اِتَّخَذَ irregularly has hamza as the radical but assimilates like و
if ch[3] == "خ" and ch[4] == "ذ" then
rad1[4] = HAMZA
end
end
elseif vform == "IX" then
check(1, ALIF)
rad1 = ch[2]
radstart = 3
elseif vform == "X" then
check(1, ALIF)
check(2, S)
if is_reduced then
rad1 = ch[3]
radstart = 4
else
check(3, T)
rad1 = ch[4]
radstart = 5
end
elseif vform == "Iq" then
rad1 = ch[1]
rad2 = ch[2]
radstart = 3
elseif vform == "IIq" then
check(1, T)
rad1 = ch[2]
rad2 = ch[3]
radstart = 4
elseif vform == "IIIq" then
check(1, ALIF)
rad1 = ch[2]
rad2 = ch[3]
check(4, N)
radstart = 5
elseif vform == "IVq" then
check(1, ALIF)
rad1 = ch[2]
rad2 = ch[3]
radstart = 4
elseif vform == "XI" then
check_len(5, 5)
check(1, ALIF)
rad1 = ch[2]
rad2 = ch[3]
check(4, ALIF)
rad3 = ch[5]
weakness = "sound"
elseif vform == "XII" then
check(1, ALIF)
rad1 = ch[2]
if ch[3] ~= ch[5] then
infer_err("For verb form XII, letters 3 and 5 should be the same", "novform")
end
check(4, W)
radstart = 5
elseif vform == "XIII" then
check_len(5, 5)
check(1, ALIF)
rad1 = ch[2]
rad2 = ch[3]
check(4, W)
rad3 = ch[5]
if rad3 == AMAQ then
weakness = "final-weak"
else
weakness = "sound"
end
elseif vform == "XIV" then
check_len(6, 6)
check(1, ALIF)
rad1 = ch[2]
rad2 = ch[3]
check(4, N)
rad3 = ch[5]
if ch[6] == AMAQ then
check_waw_ya(rad3)
weakness = "final-weak"
else
if ch[5] ~= ch[6] then
infer_err("For verb form XIV, letters 5 and 6 should be the same", "novform")
end
weakness = "sound"
end
elseif vform == "XV" then
check_len(6, 6)
check(1, ALIF)
rad1 = ch[2]
rad2 = ch[3]
check(4, N)
rad3 = ch[5]
if rad3 == Y then
check(6, ALIF)
else
check(6, AMAQ)
end
weakness = "sound"
else
error("Internal error: Unrecognized verb form " .. vform)
end
-- Process the last two radicals. RADSTART is the index of the first of the two. If it's nil then all radicals have
-- already been processed above, and we don't do anything.
if radstart then
-- There must (normally) be one or two letters left.
if len == radstart then
if vform == "I" and ch[len] == Y then
-- short form حَيَّ
weakness = "final-weak"
rad2 = Y
rad3 = Y
variant = "short"
elseif vform == "IV" and rad1 == "ر" and ch[len] == AMAQ then
-- irregular verb أَرَى
weakness = "final-weak"
rad2 = HAMZA
rad3 = Y
elseif vform == "X" and rad1 == "ح" and ch[len] == AMAQ then
-- irregular verb اِسْتَحَى
weakness = "final-weak"
rad2 = Y
rad3 = Y
variant = "short"
else
-- If one letter left, then it's a geminate verb. If the letter is alif or alif maqṣūra, it will trigger
-- an error down the line.
if vform_supports_geminate(vform) then
weakness = "geminate"
rad2 = ch[len]
rad3 = ch[len]
if vform == "III" or vform == "VI" then
variant = "short"
end
else
infer_err("Apparent geminate verb, but geminate verbs not allowed for this verb form")
end
end
elseif quadlit then
-- Process last two radicals of a quadriliteral verb form.
rad3 = ch[radstart]
rad4 = ch[radstart + 1]
expected_length = radstart + 1
check_len(expected_length)
if rad4 == AMAQ or rad4 == ALIF and rad3 == Y or rad4 == Y then
-- rad4 can be Y in passive-only verbs.
if vform_supports_final_weak(vform) then
weakness = "final-weak"
-- Ambiguous radical; randomly pick wāw as radical (but avoid two wāws in a row); it could be wāw or
-- yāʾ, but doesn't affect the conjugation.
rad4 = rad3 == W and {Y, W, ambig = true} or {W, Y, ambig = true}
else
infer_err("Last radical is " .. rad4 .. " but verb form " .. vform ..
" doesn't support final-weak verbs", "novform")
end
else
weakness = "sound"
end
else
-- Process last two radicals of a triliteral verb form.
rad2 = ch[radstart]
rad3 = ch[radstart + 1]
expected_length = radstart + 1
check_len(expected_length)
if vform == "I" and (is_waw_ya(rad3) or rad3 == ALIF or rad3 == AMAQ) then
local inferred_past_vowel, inferred_nonpast_vowel
-- Check for final-weak form I verb. It can end in tall alif (rad3 = wāw) or alif maqṣūra (rad3 = yāʾ)
-- or a wāw or yāʾ (with a past vowel of i or u, e.g. nasiya/yansā "forget" or with a passive-only
-- verb).
if rad1 == W and not form_I_w_non_assimilated() then
weakness = "assimilated+final-weak"
else
weakness = "final-weak"
end
if rad3 == ALIF then
rad3 = W
inferred_past_vowel = A
inferred_nonpast_vowel = U
if is_passive_only(passive) then
infer_err("Final-weak form-I passive verbs should end in yāʔ (ي), not tall alif (ا)", "novform")
end
elseif rad3 == AMAQ then
rad3 = Y
inferred_past_vowel = A
inferred_nonpast_vowel = I
if is_passive_only(passive) then
infer_err("Final-weak form-I passive verbs should end in yāʔ (ي), not alif maqṣūra (ى)",
"novform")
end
elseif rad1 == "ح" and rad2 == Y and rad3 == Y then
-- Long variant حَيِيَ.
inferred_past_vowel = I
inferred_nonpast_vowel = A
variant = "long"
else
if not is_passive_only(passive) then
-- does a non-passive final-weak verb in -uwa ever happen? (YES: e.g. [[رجو]] "to be slack")
inferred_past_vowel = rad3 == Y and I or U
inferred_nonpast_vowel = A
end
-- Ambiguous radical; randomly pick wāw as radical (but avoid two wāws); it could be wāw or yāʾ, but
-- doesn't affect the conjugation.
rad3 = (rad1 == W or rad2 == W) and {Y, W, ambig = true} or {W, Y, ambig = true} -- ambiguous
end
if inferred_past_vowel then
local raw_past_vowel = rget(past_vowel)
local raw_nonpast_vowel = rget(nonpast_vowel)
if raw_past_vowel ~= "-" then
if raw_past_vowel ~= inferred_past_vowel then
infer_err(("Final-weak form-I verb inferred past vowel %s, which disagrees with " ..
"explicitly specified %s"):format(undia[inferred_past_vowel], undia[raw_past_vowel]), "novform")
else
-- in case of footnote in past_vowel
inferred_past_vowel = past_vowel
end
end
if raw_nonpast_vowel ~= "-" and raw_nonpast_vowel ~= A and inferred_nonpast_vowel == U then
-- if inferred as I or A, the reality can be the reverse; form-I final-weak verbs with a~a and
-- i~i exist, e.g. سَعَى/يَسْعَى, وَلِيَ/يَلِي. Weird verb [[صها]] (also written [[صهى]]) has non-past
-- يصهى so we can't throw an error in this situation.
if raw_nonpast_vowel ~= inferred_nonpast_vowel then
infer_err(("Final-weak form-I verb inferred non-past vowel %s, which disagrees with " ..
"explicitly specified %s"):format(undia[inferred_nonpast_vowel], undia[raw_nonpast_vowel]), "novform")
else
-- in case of footnote in nonpast_vowel
inferred_nonpast_vowel = nonpast_vowel
end
end
end
if not is_passive_only(passive) then
if rget(past_vowel) == "-" then
past_vowel = inferred_past_vowel
end
if rget(nonpast_vowel) == "-" then
nonpast_vowel = inferred_nonpast_vowel
end
end
elseif vform == "IX" and is_waw_ya(rad3) and len == radstart + 2 and ch[len] == AMAQ then
-- Final-weak form IX verbs like اِرْعَوَى "to desist, to repent, to see the light".
weakness = "final-weak"
expected_length = radstart + 2
elseif vform == "X" and rad1 == "ح" and rad2 == Y and rad3 == ALIF then
-- Long variant اِسْتَحْيَا.
weakness = "final-weak"
rad3 = Y
variant = "long"
elseif rad3 == AMAQ or rad2 == Y and rad3 == ALIF or rad3 == Y then
-- rad3 == Y happens in passive-only verbs.
if vform_supports_final_weak(vform) then
weakness = "final-weak"
else
infer_err("Last radical is " .. rad3 .. " but verb form doesn't support final-weak verbs")
end
-- Ambiguous radical; randomly pick wāw as radical (but avoid two wāws); it could be wāw or yāʾ, but
-- doesn't affect the conjugation.
rad3 = (rad1 == W or rad2 == W) and {Y, W, ambig = true} or {W, Y, ambig = true}
elseif rad2 == ALIF then
if vform_supports_hollow(vform) then
weakness = "hollow"
local function set_past_to_a()
if req(past_vowel, A) then
-- already set
elseif req(past_vowel, "-") or req(past_vowel, rget(nonpast_vowel)) then
past_vowel = A
else
infer_err(("Form I hollow verb with nonpast vowel set to '%s' must have past vowel set to 'a' or the same value, not %s"):
format(undia[rget(nonpast_vowel)], undia[rget(past_vowel)]), "novform")
end
end
if vform == "I" and req(nonpast_vowel, U) then
rad2 = W
set_past_to_a()
elseif vform == "I" and req(nonpast_vowel, I) then
rad2 = Y
set_past_to_a()
else
if req(nonpast_vowel, A) and not req(past_vowel, I) then
infer_err(("Form I hollow verb with nonpast vowel set to 'a' must have past vowel set to 'i', not %s"):
format(undia[rget(past_vowel)]), "novform")
end
-- Ambiguous radical; could be wāw or yāʾ; if verb form I, it's critical to get this right, and
-- the caller checks for this situation and throws an error if non-past vowel is "a" and second
-- radical isn't explicitly given.
rad2 = {W, Y, ambig = true, need_radical = true}
end
else
infer_err("Second radical is alif but verb form doesn't support hollow verbs")
end
elseif vform == "I" and rad1 == W and not form_I_w_non_assimilated() then
weakness = "assimilated"
elseif rad2 == rad3 and (vform == "III" or vform == "VI") then
weakness = "geminate"
variant = "long"
else
weakness = "sound"
end
end
if expected_length then
check_len(expected_length, expected_length)
end
end
rad1 = convert(rad1, 1)
rad2 = convert(rad2, 2)
rad3 = convert(rad3, 3)
rad4 = convert(rad4, 4)
if not weakness then
error("Internal error: Returned weakness from infer_radicals() is nil")
end
return {
weakness = weakness,
rad1 = rad1,
rad2 = rad2,
rad3 = rad3,
rad4 = rad4,
past_vowel = past_vowel,
nonpast_vowel = nonpast_vowel,
form_viii_assim = form_viii_assim,
variant = variant,
}
end
-- bot interface to infer_radicals()
function export.infer_radicals_json(frame)
local iparams = {
headword = {},
vform = {},
passive = {},
past_vowel = {},
nonpast_vowel = {},
is_reduced = {type = "boolean"},
}
local iargs = require("Module:parameters").process(frame.args, iparams)
return require("Module:JSON").toJSON(export.infer_radicals(iargs))
end
-- Infer vocalization from participle headword (active or passive), verb form (I, II, etc.) and whether the headword is
-- active or passive. Throw an error if headword is malformed. Returned radicals may contain Latin letters "t", "w" or "y"
-- indicating ambiguous radicals guessed to be tāʾ, wāw or yāʾ respectively.
function export.infer_participle_vocalization(headword, vform, weakness, is_active)
local chars = {}
local orig_headword = headword
-- Sub out alif-madda for easier processing.
headword = rsub(headword, AMAD, HAMZA .. ALIF)
local len = ulen(headword)
-- Extract the headword letters into an array.
for i = 1, len do
table.insert(chars, usub(headword, i, i))
end
local function form_intro_error_msg()
return ("For verb form %s %s%s participle %s, "):format(vform, orig_headword ~= headword and "normalized " or
"", is_active and "active" or "passive", headword)
end
local function err(msg)
error(form_intro_error_msg() .. msg, 1)
end
-- Check that length of headword is within [min, max].
local function check_len(min, max)
if min and len < min then
err(("expected at least %s letters but saw %s"):format(min, len))
elseif max and len > max then
err(("expected at most %s letters but saw %s"):format(max, len))
end
end
-- Get the character at `ind`, making sure it exists.
local function c(ind)
check_len(ind)
return chars[ind]
end
-- Check that the letter at the given index is the given string, or is one of the members of the given array
local function check(index, must)
local letter = chars[index]
local function make_possible_values()
if type(must) == "string" then
return must
else
return list_to_text(must, nil, " or ")
end
end
if not letter then
err(("expected a letter (specifically %s) at position %s, but participle is too short"):format(
make_possible_values(), index))
end
local matches
if type(must) == "string" then
matches = letter == must
else
matches = m_table.contains(must, letter)
end
if not matches then
err(("letter %s at index %s must be %s"):format(letter, index, make_possible_values()))
end
end
local function check_weakness(values, allow_missing, invert_condition)
local function make_possible_weaknesses()
for i, val in ipairs(values) do
values[i] = "'" .. val .. "'"
end
return list_to_text(values, nil, " or ")
end
if allow_missing and invert_condition then
error("Internal error: Can't specify both allow_missing and invert_condition")
end
if not weakness then
if allow_missing or invert_condition then
return
else
err(("weakness is unspecified but must be %s"):format(make_possible_weaknesses()))
end
else
local matches = m_table.contains(values, weakness)
if invert_condition and matches then
err(("weakness '%s' must not be %s"):format(weakness, make_possible_weaknesses()))
elseif not invert_condition and not matches then
err(("weakness '%s' must be %s"):format(weakness, make_possible_weaknesses()))
end
end
end
local vocalized
local function handle_possibly_final_weak(sound_prefix, expected_length)
check_len(expected_length, expected_length)
if c(expected_length) == AMAQ then
-- passive final-weak
if is_active then
err("participle in -ِى only allowed for passive participles")
end
check_weakness({"final-weak", "assimilated+final-weak"}, "allow missing")
vocalized = sound_prefix .. AN .. AMAQ
else
-- all others behave as if sound
check_weakness({"final-weak", "assimilated+final-weak"}, nil, "invert condition")
vocalized = sound_prefix .. (is_active and I or A) .. c(expected_length)
end
end
if not (vform == "I" and is_active) then
-- all participles except verb form I active begin in م-.
check(1, M)
end
if vform == "I" then
if is_active then
check(2, ALIF)
local sound_prefix = c(1) .. AA .. c(3)
if len == 3 then
if c(3) == HAMZA then
-- Either hollow with hamzated third radical, e.g. [[شاء]] active participle 'شَاءٍ', or final-weak
-- with hamzated second radical, e.g. [[رأى]] active participle 'رَاءٍ'. Theoretically (?), also
-- geminate with hamzated second/third radical, but I don't know if any such verbs exist.
if weakness == "geminate" then
vocalized = sound_prefix .. SH
else
check_weakness({"hollow", "final-weak"}, "allow missing")
vocalized = sound_prefix .. IN
end
else
check_weakness({"final-weak", "geminate"})
if weakness == "geminate" then
vocalized = sound_prefix .. SH
else
vocalized = sound_prefix .. IN
end
end
else
check_len(4, 4)
-- we will convert back to alif maqṣūra below as needed
vocalized = sound_prefix .. I .. c(4)
end
else
-- assimilated verbs: regular, e.g. مَوْزُون "weighed"
-- geminate verbs: regular, e.g. مَبْلُول "moistened"
-- third-hamzated verbs: مَبْرُوء
-- hollow verbs: مَقُود "led, driven"; مَزِيد "added, increased"
-- hollow first-hamzated verbs: مَئِيض "returned, reverted"; مَأْيُوس "despaired" (NOTE: formation is sound);
-- مَأُود or مَؤُود "bent; depleted"
-- hollow third-hamzated verbs: مَشِيء "willed, intended", مَضُوء "glittered?"
-- final-weak: مَلْقِيّ "found, encountered"; مَصْغُوّ "inclined"
-- hollow + final-weak: مَشْوِيّ "fried, grilled", مَهْوِيّ "loved"
-- first-hamzated + hollow + final-weak: مَأْوِيّ "received hospitably"
local sound_prefix = MA .. c(2) .. SK .. c(3)
if len == 5 then
-- sound, assimilated or geminate
check(4, W)
vocalized = sound_prefix .. UU .. c(5)
else
check_len(4, 4)
if c(4) == W then
-- final-weak third-wāw
vocalized = sound_prefix .. U .. W .. SH
elseif c(4) == Y then
-- final-weak third-yāʾ
vocalized = sound_prefix .. I .. Y .. SH
else
-- hollow
check(3, {W, Y})
if c(3) == W then
vocalized = MA .. c(2) .. UU .. c(4)
else
vocalized = MA .. c(2) .. II .. c(4)
end
end
end
end
elseif vform == "II" or vform == "V" or vform == "XII" or vform == "XIII" or vform == "Iq" or vform == "IIq" or
vform == "IIIq" then
local sound_prefix, expected_length
if vform == "II" then
sound_prefix = MU .. c(2) .. A .. c(3) .. SH
expected_length = 4
elseif vform == "V" then
check(2, T)
sound_prefix = MU .. T .. A .. c(3) .. A .. c(4) .. SH
expected_length = 5
elseif vform == "XII" then
-- e.g. [[احدودب]] "to be or become convex or humpbacked", مُحْدَوْدِب (active);
-- [[اثنونى]] "to be bent; to be doubled up", مُثْنَوْنٍ (active)
check(4, W)
if c(3) ~= c(5) then
err(("third letter %s should be the same as the fifth letter %s"):format(c(3), c(5)))
end
sound_prefix = MU .. c(2) .. SK .. c(3) .. A .. W .. SK .. c(5)
expected_length = 6
elseif vform == "XIII" then
-- e.g. [[اخروط]] "to get entangled; to extend", مُخْرَوِّط (active), مُخْرَوَّط (passive)
check(4, W)
sound_prefix = MU .. c(2) .. SK .. c(3) .. A .. W .. SH
expected_length = 5
elseif vform == "Iq" then
sound_prefix = MU .. c(2) .. A .. c(3) .. SK .. c(4)
expected_length = 5
elseif vform == "IIq" then
check(2, T)
sound_prefix = MU .. T .. A .. c(3) .. A .. c(4) .. SK .. c(5)
expected_length = 6
elseif vform == "IIIq" then
-- e.g. [[اخرنطم]] "to be proud and angry"
check(4, T)
sound_prefix = MU .. c(2) .. SK .. c(3) .. A .. N .. SK .. c(5)
expected_length = 6
else
error("Internal error: Unhandled verb form " .. vform)
end
if len == expected_length - 1 then
-- active final-weak
if not is_active then
err(("length-%s participle only allowed for active participles"):format(len))
end
check_weakness({"final-weak", "assimilated+final-weak"}, "allow missing")
vocalized = sound_prefix .. IN
else
handle_possibly_final_weak(sound_prefix, expected_length)
end
elseif vform == "III" or vform == "VI" then
local sound_prefix, expected_length
if vform == "VI" then
check(2, T)
check(4, ALIF)
sound_prefix = MU .. T .. A .. c(3) .. AA .. c(5)
expected_length = 6
else
sound_prefix = MU .. c(2) .. AA .. c(4)
expected_length = 5
end
if len == expected_length - 1 then
-- active final-weak or active or passive geminate
if is_active then
check_weakness({"geminate", "final-weak", "assimilated+final-weak"})
if weakness == "geminate" then
vocalized = sound_prefix .. SH
else
vocalized = sound_prefix .. IN
end
else
check_weakness({"geminate"}, "allow missing")
vocalized = sound_prefix .. SH
end
else
handle_possibly_final_weak(sound_prefix, expected_length)
end
elseif vform == "IV" or vform == "X" then
-- form IV:
-- sound: مُرْسِخ (active, "entrenching"), مُرْسَخ (passive, "entrenched")
-- first-hamzated (like sound): مُؤْيِس (active, "causing to despair"), مُؤْيَس (passive, "caused to despair")
-- final-weak: مُكْرٍ (active, "renting out"), مُكْرًى (passive, "rented out")
-- assimilated: مُورِد (active, "transferring"), مُورَد (passive, "transferred"); same when first-Y, e.g.
-- أَيْقَنَ "to be certain of": مُوقِن (active), مُوقَن (passive)
-- assimilated + final-weak: مُورٍ (active, "setting fire, kindling"), مُورًى (passive, "set fire, kindled")
-- geminate: مُمِدّ (active, "granting, helping"), مُمَدّ (passive, "granted, helped")
-- hollow: مُزِيل (active, "eliminating"), مُزَال (passive, "eliminated")
-- hollow + final-weak: مُعْيٍ (active, "tiring"), مُعْيًى (passive, "tired")
local sound_prefix, expected_length
if vform == "X" then
check(2, S)
check(3, T)
sound_prefix = MU .. S .. SK .. T .. A .. c(4)
expected_length = 6
else
sound_prefix = MU .. c(2)
expected_length = 4
end
if len == expected_length and c(len - 1) == Y and c(len) ~= AMAQ then
-- active hollow
if not is_active then
err("this shape only allowed for active participles")
end
check_weakness({"hollow"}, "allow missing")
vocalized = sound_prefix .. II .. c(len)
elseif len == expected_length and c(len - 1) == ALIF then
-- passive hollow
if is_active then
err("this shape only allowed for passive participles")
end
check_weakness({"hollow"}, "allow missing")
vocalized = sound_prefix .. AA .. c(len)
elseif len == expected_length - 1 then
-- active final-weak or active or passive geminate
if is_active then
check_weakness({"geminate", "final-weak", "assimilated+final-weak"})
if weakness == "geminate" then
vocalized = sound_prefix .. I .. c(len) .. SH
elseif vform == "IV" and c(2) == W then
-- assimilated final-weak
vocalized = sound_prefix .. c(len) .. IN
else
vocalized = sound_prefix .. SK .. c(len) .. IN
end
else
check_weakness({"geminate"}, "allow missing")
vocalized = sound_prefix .. A .. c(len) .. SH
end
else
if vform == "IV" and c(2) == W then
-- assimilated, possibly final-weak
sound_prefix = sound_prefix .. c(expected_length - 1)
else
sound_prefix = sound_prefix .. SK .. c(expected_length - 1)
end
handle_possibly_final_weak(sound_prefix, expected_length)
end
elseif vform == "VII" or vform == "VIII" then
-- form VII (passive participles are fairly rare but do exist):
-- sound: مُنْكَتِب (active "subscribing"), مُنْكَتَب (passive "subscribed")
-- geminate: مُنْضَمّ (both active "joining, containing" and passive "joined, contained")
-- final-weak: مُنْطَلٍ (active "fooling (someone)"), مُنْطَلًى (passive "fooled")
-- final-weak with medial wāw: مُنْطَوٍ (active "involving"), مُنْطَوًى (passive "involved")
-- hollow: مُنْقَاد (both active "complying with" and passive "complied with")
--
-- for form VIII, the same variants exist but things are complicated by assimilations involving the template T.
-- sound third-hamzated no assimilation: مُبْتَدِئ (active "beginning"), مُبْتَدَأ (passive "begun")
-- geminate no assimilation: مُبْتَزّ (both active "robbing" and passive "robbed")
-- final-weak no assimilation: مُبْتَنٍ (active "building"), مُبْتَنًى (passive "built")
-- final-weak with medial wāw no assimilation: مُحْتَوٍ (active "containing"), مُحْتَوًى (passive "contained")
-- hollow no assimilation: مُخْتَار (both active "choosing" and passive "chosen")
--
-- sound with total assimilation: مُتَّبِع (active "following"), مُتَّبَع (passive "followed")
-- sound with total assimilation, assimilating wāw: مُتَّعِد (active "threatening"), مُتَّعَد (passive "threatened")
-- sound with total assimilation, irregularly assimilating hamza: مُتَّخِذ (active "taking"), مُتَّخَذ (passive "taken")
-- sound with total assimilation (to ḏāl, producing dāl): مُدَّخِر (active "reserving"), مُدَّخَر (passive "reserved")
-- sound with total assimilation (to ḏāl): مُذَّكِر (active "remembering"), مُذَّكَر (passive "remembered")
-- sound with total assimilation (to ṭāʔ): مُطَّرِح (active "discarding"), مُطَّرَح (passive "discarded")
-- sound with total assimilation (to ẓāʔ): مُظَّلِم (active "tolerating"), مُظَّلَم (passive "tolerated")
-- final-weak with total assimilation, assimilating wāw: مُتَّقٍ (active "guarding against"), مُتَّقًى (passive "guarded against")
-- final-weak with total assimilation (to ṯāʔ): مُثَّنٍ (active "undulating"), مُثَّنًى (passive "undulated")
-- final-weak with total assimilation (to dāl): مُدَّعٍ (active "claiming"), مُدَّعًى (passive "claimed")
-- sound with partial assimilation (to zayn): مُزْدَهِر (active "thriving"), مُزْدَهَر (passive "thrived")
-- sound with medial wāw with partial assimilation (to zayn): مُزْدَوِج (active "appearing twice")
-- sound with partial assimilation (to ṣād): مُصْطَبِح (active "illuminating"), مُصْطَبَح (passive, "illuminated")
-- sound with partial assimilation (to ḍād): مُضْطَرِب (active "to be disturbed"; no passive)
-- geminate with partial assimilation (to ṣād): مُصْطَبّ (both active "effusing" and passive "effused")
-- geminate with partial assimilation (to ḍād): مُضْطَرّ (both active "forcing" and passive "forced")
-- final-weak with partial assimilation (to ṣād): مُصْطَلٍ (active "warming"), مُصْطَلًى (passive "warmed")
-- hollow with partial assimilation (to zayn): مُزْدَاد (both active "increasing" and passive "increased")
-- hollow with partial assimilation (to ṣad): مُصْطَاد (both active "hunting" and passive "hunted")
local sound_prefix, sufind
if vform == "VII" then
check(2, N)
sound_prefix = MU .. N .. SK .. c(3)
sufind = 4
else
local c2 = c(2)
if c2 == T or c2 == "د" or c2 == "ث" or c2 == "ذ" or c2 == "ط" or c2 == "ظ" then
-- full assimilation
sound_prefix = MU .. c2 .. SH
sufind = 3
else
-- partial or no assimilation
if c2 == "ز" then
check(3, "د")
elseif c2 == "ص" or c2 == "ض" then
check(3, "ط")
else
check(3, T)
end
sound_prefix = MU .. c2 .. SK .. c(3)
sufind = 4
end
end
if c(sufind) == ALIF then
-- hollow, active or passive
check_len(sufind + 1, sufind + 1)
check_weakness({"hollow"}, "allow missing")
vocalized = sound_prefix .. AA .. c(sufind + 1)
elseif len == sufind then
-- active final-weak or active or passive geminate
if is_active then
check_weakness({"geminate", "final-weak", "assimilated+final-weak"})
if weakness == "geminate" then
vocalized = sound_prefix .. A .. c(len) .. SH
else
vocalized = sound_prefix .. A .. c(len) .. IN
end
else
check_weakness({"geminate"}, "allow missing")
vocalized = sound_prefix .. A .. c(len) .. SH
end
else
sound_prefix = sound_prefix .. A .. c(sufind)
handle_possibly_final_weak(sound_prefix, sufind + 1)
end
elseif vform == "IX" then
check_len(4, 4)
vocalized = MU .. c(2) .. SK .. c(3) .. A .. c(4) .. SH
elseif vform == "IVq" then
-- e.g. [[اذلعب]] "to scamper away", مُذْلَعِبّ (active), مُذْلَعَبّ (passive);
-- [[اطمأن]] "to remain quietly; to be certain", مُطْمَئِنّ (active), مُطْمَأَنّ (passive)
check_len(5, 5)
local sound_prefix = MU .. c(2) .. SK .. c(3) .. A .. c(4)
if is_active then
vocalized = sound_prefix .. I .. c(5) .. SH
else
vocalized = sound_prefix .. A .. c(5) .. SH
end
elseif vform == "XI" then
check_len(5, 5)
check(4, ALIF)
vocalized = MU .. c(2) .. SK .. c(3) .. AA .. c(5) .. SH
-- e.g. [[احمار]] "to turn red, to blush", مُحْمَارّ (active)
elseif vform == "XIV" or vform == "XV" then
-- FIXME: Implement. No examples in Wiktionary currently; need to look up in a grammar.
error("Support for verb form " .. vform .. " not implemented yet")
else
error("Don't recognize verb form " .. vform)
end
vocalized = rsub(vocalized, HAMZA .. AA, AMAD)
local reconstructed_headword = lang:stripDiacritics(vocalized)
if reconstructed_headword ~= orig_headword then
error(("Internal error: Vocalized participle %s doesn't match original participle %s"):format(
vocalized, orig_headword))
end
return vocalized
end
function export.infer_participle_vocalization_json(frame)
local iparams = {
[1] = {required = true},
[2] = {required = true},
["weakness"] = {},
["passive"] = {type = "boolean"}
}
local iargs = require("Module:parameters").process(frame.args, iparams)
return export.infer_participle_vocalization(iargs[1], iargs[2], iargs.weakness, not iargs.passive)
end
return export
dze8iy29n34z0b1h1iw720tuznja4wa
379725
379721
2026-10-19T23:48:18Z
Hakimi97
2668
ar-verb > ar-kk
379725
Scribunto
text/plain
local export = {}
--[=[
This module implements {{ar-conj}} and provides the underlying conjugation functions for {{ar-verb}}
(whose actual formatting is done in [[Module:ar-headword]]).
Author: User:Benwing, from an early version (2013-2014) by User:Atitarev, User:ZxxZxxZ.
]=]
--[=[
TERMINOLOGY:
-- "slot" = A particular combination of tense/mood/person/number/etc.
Example slot names for verbs are "past_1s" (past tense first-person singular), "juss_pass_3fp" (non-past jussive
passive third-person feminine plural) "ap" (active participle). Each slot is filled with zero or more forms.
-- "form" = The conjugated Arabic form representing the value of a given slot.
-- "lemma" = The dictionary form of a given Arabic term. For Arabic, normally the third person masculine singular past,
although other forms may be used if this form is missing (e.g. in passive-only verbs or verbs lacking the past).
]=]
--[=[
FIXME:
1. Finish unimplemented conjugation types. Only IX-final-weak left (extremely rare, possibly only one verb اِعْمَايَ
(according to Haywood and Nahmad p. 244, who are very specific about the irregular occurrence of alif + yā instead
of expected اِعْمَيَّ with doubled yā). Not in Hans Wehr. NOTE: Not true about this, cf. form IX اِرْعَوَى "to desist,
to repent, to see the light". Also note form XII اِخْضَوْضَرَ = form IX اِخْضَرَّ "to be or become green".
[DONE except for اِعْمَايَ]
2. Implement irregular verbs as special cases and recognize them, e.g.
-- laysa "to not be"; only exists in the past tense, no non-past, no imperative, no participles, no passive, no
verbal noun. Irregular alternation las-/lays-. [IMPLEMENTABLE USING OVERRIDES]
-- istaḥā yastaḥī "be ashamed of" -- this is complex according to Hans Wehr because there are two verbs, regular
istaḥyā yastaḥyī "to spare (someone)'s life" and irregular istaḥyā yastaḥyī "to be ashamed to face (someone)",
which is irregular because it has the alternate irregular form istaḥā yastaḥī which only applies to this meaning.
Currently we follow Haywood and Nahmad in saying that both varieties can be spelled istaḥyā/istaḥā/istaḥḥā, but we
should instead use a variant= param similar to حَيَّ to distinguish the two possibilities, and maybe not include
istaḥḥā.
-- ʿayya/ʿayiya yaʿayyu/yaʿyā "to not find the right way, be incapable of, stammer, falter, fall ill". This appears
to be a mixture of a geminate and final-weak verb. Unclear what the whole paradigm looks like. Do the
consonant-ending parts in the past follow the final-weak paradigm? Is it the same in the non-past? Or can you
conjugate the non-past fully as either geminate or final-weak?
-- اِنْمَحَى inmaḥā or يمَّحَى immaḥā "to be effaced, obliterated; to disappear, vanish" has irregular assimilation of inm-
to imm- as an alternative. inmalasa "to become smooth; to glide; to slip away; to escape" also has immalasa as an
alternative. The only other form VII verbs in Hans Wehr beginning with -m- are inmalaḵa "to be pulled out, torn
out, wrenched" and inmāʿa "to be melted, to melt, to dissolve", which are not listed with imm- alternatives, but
might have them; if so, we should handle this generally. [DONE]
-- يَرَعَ yaraʕa yariʕu "to be a coward, to be chickenhearted" as an alternative form of يَرِعَ yariʕa yayraʕu (as given in
Wehr). [IMPLEMENTABLE USING OVERRIDES]
3. Implement individual override parameters for each paradigm part. See Module:fro-verb for an example of how to do this
generally. Note that {{temp|ar-conj-I}} and other of the older templates already had such individual override params.
[DONE]
Irregular verbs already implemented:
-- [ḥayya/ḥayiya yaḥyā "live" -- behaves like a normal final-weak verb
(e.g. past first singular ḥayītu) except in the past-tense parts with
vowel-initial endings (all the third person except for the third feminine
plural). The normal singular and dual endings have -yiya- in them, which
compresses to -yya-, with the normal endings the less preferred ones.
In masculine third plural, expected ḥayū is replaced by ḥayyū by
analogy to the -yy- parts, and the regular form is not given as an
alternant in John Mace. Barron's 201 verbs appears to have the regular
ḥayū as the part, however. Note also that final -yā appears with tall
alif. This appears to be a spelling convention of Arabic, also applying
in ḥayyā (form II, "to keep (someone) alive") and 'aḥyā (form IV,
"to animate, revive, give birth to, give new life to").] -- implemented
-- [ittaxadha yattaxidhu "take"] -- implemented
-- [sa'ala yas'alu "ask" with alternative jussive/imperative yasal/sal] -- implemented
-- [ra'ā yarā "see"] -- implemented
-- ['arā yurī "show"] -- implemented
-- ['akala ya'kulu "eat" with imperative kul] -- implemented
-- ['axadha ya'xudhu "take" with imperative xudh] -- implemented
-- ['amara ya'muru "order" with imperative mur] -- implemented
--]=]
local force_cat = false -- set to true for debugging
-- if true, always maintain manual translit during processing, and compare against full translit at the end
local debug_translit = false
local lang = require("Module:languages").getByCode("ar")
local m_links = require("Module:links")
local m_string_utilities = require("Module:string utilities")
local m_table = require("Module:table")
local ar_utilities = require("Module:ar-utilities")
local decorations_module = "Module:decorations"
local iut = require("Module:inflection utilities")
local put = require("Module:parse utilities")
local m_inflection_table = require("Module:inflection-table")
local concat = table.concat
local list_to_text = mw.text.listToText
local rfind = m_string_utilities.find
local rsubn = m_string_utilities.gsub
local rmatch = m_string_utilities.match
local rsplit = m_string_utilities.split
local usub = m_string_utilities.sub
local ulen = m_string_utilities.len
local u = m_string_utilities.char
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local dump = mw.dumpObject
-- Within this module, conjugations are the functions that do the actual
-- conjugating by creating the parts of a basic verb.
-- They are defined further down.
local conjugations = {}
-- hamza variants
local HAMZA = u(0x0621) -- hamza on the line (stand-alone hamza) = ء
local HAMZA_ON_ALIF = u(0x0623)
local HAMZA_ON_W = u(0x0624)
local HAMZA_UNDER_ALIF = u(0x0625)
local HAMZA_ON_Y = u(0x0626)
local HAMZA_ANY = "[" .. HAMZA .. HAMZA_ON_ALIF .. HAMZA_UNDER_ALIF .. HAMZA_ON_W .. HAMZA_ON_Y .. "]"
local HAMZA_PH = u(0xFFF0) -- hamza placeholder
local BAD = u(0xFFF1)
local BORDER = u(0xFFF2)
-- diacritics
local A = u(0x064E) -- fatḥa
local AN = u(0x064B) -- fatḥatān (fatḥa tanwīn)
local U = u(0x064F) -- ḍamma
local UN = u(0x064C) -- ḍammatān (ḍamma tanwīn)
local I = u(0x0650) -- kasra
local IN = u(0x064D) -- kasratān (kasra tanwīn)
local SK = u(0x0652) -- sukūn = no vowel
local SH = u(0x0651) -- šadda = gemination of consonants
local DAGGER_ALIF = u(0x0670)
local DIACRITIC_ANY_BUT_SH = "[" .. A .. I .. U .. AN .. IN .. UN .. SK .. DAGGER_ALIF .. "]"
-- Pattern matching short vowels
local AIU = "[" .. A .. I .. U .. "]"
-- Pattern matching short vowels or sukūn
local AIUSK = "[" .. A .. I .. U .. SK .. "]"
-- Pattern matching any diacritics that may be on a consonant
local DIACRITIC = SH .. "?" .. DIACRITIC_ANY_BUT_SH
-- translit_patterns
local vowels = "aeiouāēīōū"
local NV = "[^" .. vowels .. "]"
local dia = {a = A, i = I, u = U}
local undia = {[A] = "a", [I] = "i", [U] = "u", ["-"] = "-"}
-- various letters and signs
local ALIF = u(0x0627) -- ʾalif = ا
local AMAQ = u(0x0649) -- ʾalif maqṣūra = ى
local AMAD = u(0x0622) -- ʾalif madda = آ
local TAM = u(0x0629) -- tāʾ marbūṭa = ة
local T = u(0x062A) -- tāʾ = ت
local HYPHEN = u(0x0640)
local N = u(0x0646) -- nūn = ن
local W = u(0x0648) -- wāw = و
local Y = u(0x064A) -- yāʾ = ي
local S = "س"
local M = "م"
local LRM = u(0x200e) -- left-to-right mark
-- common combinations
local AH = A .. TAM
local AT = A .. T
local AA = A .. ALIF
local AAMAQ = A .. AMAQ
local AAH = AA .. TAM
local AAT = AA .. T
local II = I .. Y
local UU = U .. W
local AY = A .. Y
local AW = A .. W
local AYSK = AY .. SK
local AWSK = AW .. SK
local NA = N .. A
local NI = N .. I
local AAN = AA .. N
local AANI = AA .. NI
local AYNI = AYSK .. NI
local AWNA = AWSK .. NA
local AYNA = AYSK .. NA
local AYAAT = AY .. AAT
local UNU = "[" .. UN .. U .. "]"
local MA = M .. A
local MU = M .. U
local TA = T .. A
local TU = T .. U
local _I = ALIF .. I
local _U = ALIF .. U
local translit_cache = {
-- hamza variants
[HAMZA] = "ʔ",
[HAMZA_ON_ALIF] = "ʔ",
[HAMZA_ON_W] = "ʔ",
[HAMZA_UNDER_ALIF] = "ʔ",
[HAMZA_ON_Y] = "ʔ",
[HAMZA_PH] = "ʔ",
-- diacritics
[A] = "a",
[AN] = "an",
[U] = "u",
[UN] = "un",
[I] = "i",
[IN] = "in",
[SK] = "",
[SH] = "*", -- handled specially
[DAGGER_ALIF] = "ā",
-- various letters and signs
[""] = "",
[ALIF] = BAD, -- we should never be transliterating ALIF by itself, as its translit in isolation is ambiguous
[AMAQ] = BAD,
[AMAD] = "ʔā",
[TAM] = "",
[T] = "t",
[N] = "n",
[W] = "w",
[Y] = "y",
[S] = "s",
[M] = "m",
[LRM] = "",
-- common combinations
[AH] = "a",
[AT] = "at",
[AA] = "ā",
[AAMAQ] = "ā",
[AAH] = "āh",
[AAT] = "āt",
[II] = "ī",
[UU] = "ū",
[AY] = "ay",
[AW] = "aw",
[AYSK] = "ay",
[AWSK] = "aw",
[NA] = "na",
[NI] = "ni",
[AAN] = "ān",
[AANI] = "āni",
[AYNI] = "ayni",
[AWNA] = "awna",
[AYNA] = "ayna",
[AYAAT] = "ayāt",
[MA] = "ma",
[MU] = "mu",
[TA] = "ta",
[TU] = "tu",
[_I] = "i",
[_U] = "u",
}
local function transliterate(text)
local cached = translit_cache[text]
if cached then
if cached == BAD then
error(("Internal error: Unable to transliterate %s because explicitly marked as BAD"):format(text))
end
return cached
end
local tr = (lang:transliterate(text))
if not tr then
error(("Internal error: Unable to transliterate: %s"):format(text))
end
translit_cache[text] = tr
return tr
end
local all_person_number_list = {
"1s",
"2ms",
"2fs",
"3ms",
"3fs",
"1d",
"2d",
"3md",
"3fd",
"1p",
"2mp",
"2fp",
"3mp",
"3fp"
}
local imp_person_number_list = {}
for _, pn in ipairs(all_person_number_list) do
if pn:find("^2") then
table.insert(imp_person_number_list, pn)
end
end
local passive_types = m_table.listToSet {
"pass", -- verb has both active and passive
"ipass", -- verb is active with impersonal passive
"nopass", -- verb is active-only
"onlypass", -- verb is passive-only
"onlypass-impers", -- verb itself is impersonal, meaning passive-only with impersonal passive
}
local indicator_flags = m_table.listToSet {
"nopast", "no_nonpast", "noimp",
"nocat", -- don't categorize or include annotations about this; useful in suppletive parts of verbs
"reduced", -- verb has assimilation/reduction of initial coronals
"altgem", -- form X with alternative past geminate forms with final-weak endings
}
export.potential_lemma_slots = {"past_3ms", "past_pass_3ms", "ind_3ms", "ind_pass_3ms", "imp_2ms"}
export.unsettable_slots = {}
for _, potential_lemma_slot in ipairs(export.potential_lemma_slots) do
table.insert(export.unsettable_slots, potential_lemma_slot .. "_linked")
end
-- We don't set the active participle directly for form I because we don't want stative verbs (with past vowel i or u)
-- to default to فَاعِل. Instead we set the special slot 'ap1' and later copy it to 'ap' for non-stative verbs. The user
-- meanwhile can explicitly request the فَاعِل form for active participles for stative verbs using `ap:+`.
table.insert(export.unsettable_slots, "ap1") -- primary default فَاعِل for form I active participles
table.insert(export.unsettable_slots, "ap2") -- secondary default فَعِيل for form I active participles (stative I)
table.insert(export.unsettable_slots, "ap3") -- secondary default فَعِل for form I active participles (stative II)
table.insert(export.unsettable_slots, "apcd") -- secondary default أَفْعَل for form I active participles (color/defect)
table.insert(export.unsettable_slots, "apan") -- secondary default فَعْلَان for form I active participles (in -ān)
table.insert(export.unsettable_slots, "pp2") -- secondary default فَعِيل for form I passive participles (same as ap2)
table.insert(export.unsettable_slots, "vn2") -- secondary default فِعَال for form III verbal nouns
export.unsettable_slots_set = m_table.listToSet(export.unsettable_slots)
local default_indicator_to_active_participle_slot = {
["+"] = "ap1",
["++"] = "ap2",
["+++"] = "ap3",
["+cd"] = "apcd",
["+an"] = "apan",
}
local slots_that_may_be_uncertain = {
vn = "verbal noun",
ap = "active participle",
}
-- Initialize all the slots for which we generate forms.
local function add_slots(alternant_multiword_spec)
alternant_multiword_spec.verb_slots = {
{"ap", "act|part"},
{"pp", "pass|part"},
{"vn", "vnoun"},
}
for _, unsettable_slot in ipairs(export.unsettable_slots) do
table.insert(alternant_multiword_spec.verb_slots, {unsettable_slot, "-"})
end
-- Add entries for a slot with person/number variants.
-- `slot_prefix` is the prefix of the slot, typically specifying the tense/aspect.
-- `tag_suffix` is a string listing the set of inflection tags to add after the person/number tags.
-- `person_number_list` is a list of the person/number slot suffixes to add to `slot_prefix`.
local function add_personal_slot(slot_prefix, tag_suffix, person_number_list)
for _, persnum in ipairs(person_number_list) do
local slot = slot_prefix .. "_" .. persnum
local accel = persnum:gsub("(.)", "%1|") .. tag_suffix
table.insert(alternant_multiword_spec.verb_slots, {slot, accel})
end
end
local tenses = {
{"past", "past|%s"},
{"ind", "non-past|%s|ind"},
{"sub", "non-past|%s|sub"},
{"juss", "non-past|%s|juss"},
}
for _, slot_accel in ipairs(tenses) do
local slot, accel = unpack(slot_accel)
for _, voice in ipairs {"act", "pass"} do
add_personal_slot(voice == "act" and slot or slot .. "_pass", accel:format(voice),
all_person_number_list)
end
end
add_personal_slot("imp", "act|imp", imp_person_number_list)
alternant_multiword_spec.verb_slots_map = {}
for _, slot_accel in ipairs(alternant_multiword_spec.verb_slots) do
local slot, accel = unpack(slot_accel)
alternant_multiword_spec.verb_slots_map[slot] = accel
end
end
local overridable_stems = {}
local slot_override_param_mods = {
footnote = {
item_dest = "footnotes",
store = "insert",
},
alt = {},
t = {
-- [[Module:links]] expects the gloss in "gloss".
item_dest = "gloss",
},
gloss = {},
g = {
-- [[Module:links]] expects the genders in "g". `sublist = true` automatically splits on comma (optionally
-- with surrounding whitespace).
item_dest = "genders",
sublist = true,
},
pos = {},
lit = {},
id = {},
-- Qualifiers and labels
q = {
type = "qualifier",
},
qq = {
type = "qualifier",
},
l = {
type = "labels",
},
ll = {
type = "labels",
},
}
local function generate_obj(formval, parse_err, prefix, is_slot_override)
local val, uncertain = formval:match("^(.*)(%?)$")
val = val or formval
uncertain = not not uncertain
local ar, translit = val:match("^(.*)//(.*)$")
if not ar then
ar = val
end
if ar == "" then
if uncertain then
ar = "?"
else
error(("Can't specify blank value for override for %s override '%s'"):format(
is_slot_override and "slot" or "stem", prefix))
end
end
return {form = ar, translit = translit, uncertain = uncertain}
end
local function parse_inline_modifiers(comma_separated_group, parse_err, prefix, is_slot_override)
local function this_generate_obj(formval, parse_err)
return generate_obj(formval, parse_err, prefix, is_slot_override)
end
return put.parse_inline_modifiers_from_segments {
group = comma_separated_group,
props = {
param_mods = slot_override_param_mods,
parse_err = parse_err,
generate_obj = this_generate_obj,
pre_normalize_modifiers = function(data)
local modtext = data.modtext
modtext = modtext:match("^(%[.*%])$")
if modtext then
return ("<footnote:%s>"):format(modtext)
end
return data.modtext
end,
},
}
end
local function allow_multiple_values_for_override(comma_separated_groups, data, is_slot_override)
local retvals = {}
for _, comma_separated_group in ipairs(comma_separated_groups) do
local retval
if is_slot_override then
retval = parse_inline_modifiers(comma_separated_group, data.parse_err)
else
retval = generate_obj(comma_separated_group[1], data.parse_err, data.prefix, is_slot_override)
retval.footnotes = data.fetch_footnotes(comma_separated_group)
end
table.insert(retvals, retval)
end
for _, form in ipairs(retvals) do
if form.form == "+" or default_indicator_to_active_participle_slot[form.form] then
if form.form ~= "+" and default_indicator_to_active_participle_slot[form.form] and not is_slot_override then
error(("Stem override '%s' cannot use %s to request a secondary default"):format(
data.prefix, form.form))
end
data.base.slot_override_uses_default[data.prefix] = true
end
end
for _, form in ipairs(retvals) do
if form.form == "-" then
data.base.slot_explicitly_missing[data.prefix] = true
break
end
end
if data.base.slot_explicitly_missing[data.prefix] then
for _, form in ipairs(retvals) do
if form.form ~= "-" then
data.parse_err(("For slot or stem '%s', saw both - and a value other than -, which isn't allowed"):
format(data.prefix))
end
end
return nil
end
return retvals
end
local function simple_choice(choices)
return function(separated_groups, data)
if #separated_groups > 1 then
data.parse_err("For spec '" .. data.prefix .. ":', only one value currently allowed")
end
if #separated_groups[1] > 1 then
data.parse_err("For spec '" .. data.prefix .. ":', no footnotes currently allowed")
end
local choice = separated_groups[1][1]
if not m_table.contains(choices, choice) then
data.parse_err("For spec '" .. data.prefix .. ":', saw value '" .. choice .. "' but expected one of '" ..
concat(choices, ",") .. "'")
end
return choice
end
end
for _, overridable_stem in ipairs {
"past",
"past_v",
"past_c",
"past_pass",
"past_pass_v",
"past_pass_c",
"nonpast",
"nonpast_v",
"nonpast_c",
"nonpast_pass",
"nonpast_pass_v",
"nonpast_pass_c",
"imp",
"imp_v",
"imp_c",
} do
overridable_stems[overridable_stem] = allow_multiple_values_for_override
end
overridable_stems.past_final_weak_vowel = simple_choice { "ay", "aw", "ī", "ū" }
overridable_stems.past_pass_final_weak_vowel = simple_choice { "ay", "aw", "ī", "ū" }
overridable_stems.nonpast_final_weak_vowel = simple_choice { "ā", "ī", "ū" }
overridable_stems.nonpast_pass_final_weak_vowel = simple_choice { "ā", "ī", "ū" }
-------------------------------------------------------------------------------
-- Utility functions --
-------------------------------------------------------------------------------
-- version of rsubn() that discards all but the first return value
local function rsub(term, foo, bar)
return (rsubn(term, foo, bar))
end
-- Concatenate one or more strings or form objects.
local function q(...)
local not_all_strings = debug_translit
local has_manual_translit = debug_translit
for i = 1, select("#", ...) do
local argt = select(i, ...)
if not argt then
error(("Internal error: Saw nil at index %s: %s"):format(i, dump({...})))
end
if type(argt) ~= "string" then
not_all_strings = true
if argt.translit then
has_manual_translit = true
break
end
end
end
if not not_all_strings then
-- just strings, concatenate directly
return concat{...}
end
local formvals = {}
local translit = has_manual_translit and {} or nil
local footnotes
for i = 1, select("#", ...) do
local argt = select(i, ...)
if type(argt) == "string" then
formvals[i] = argt
if has_manual_translit then
translit[i] = transliterate(argt)
end
else
formvals[i] = argt.form
if has_manual_translit then
translit[i] = argt.translit or transliterate(argt.form)
end
footnotes = iut.combine_footnotes(footnotes, argt.footnotes)
end
end
-- FIXME: Do we want to support other properties?
return {
form = concat(formvals),
translit = has_manual_translit and concat(translit) or nil,
footnotes = footnotes,
}
end
-- Return the formval associated with `rad` (a radical or past/non-past vowel, either a string or form object).
local function rget(rad)
if type(rad) == "string" then
return rad
elseif type(rad) == "table" then
return rad.form
else
error(("Internal error: Unexpected type for radical or past/non-past vowel: %s"):format(dump(rad)))
end
end
export.rget = rget -- for use in [[Module:ar-headword]]
-- Return the footnotes associated with `rad` (a radical or past/non-past vowel, either a string or form object).
local function rget_footnotes(rad)
if type(rad) == "string" then
return nil
elseif type(rad) == "table" then
return rad.footnotes
else
error(("Internal error: Unexpected type for radical or past/non-past vowel: %s"):format(dump(rad)))
end
end
-- Return true if the formval associated with `rad` (a radical or past/non-past vowel, either a string or form object)
-- is `val`.
local function req(rad, val)
return rget(rad) == val
end
-- Map `vow` (a past/non-past vowel, either a string or form object without translit) by passing the formval through
-- `fn`. Don't call this on radicals because they may have manual translit and it isn't clear how to handle that.
local function map_vowel(vow, fn)
if type(vow) == "string" then
return fn(vow)
elseif type(vow) == "table" then
return {form = fn(vow.form), footnotes = vow.footnotes}
else
error(("Internal error: Unexpected type for past/non-past vowel: %s"):format(dump(vow)))
end
end
local function get_radicals_3(vowel_spec)
return vowel_spec.rad1, vowel_spec.rad2, vowel_spec.rad3, vowel_spec.past, vowel_spec.nonpast
end
local function get_radicals_4(vowel_spec)
return vowel_spec.rad1, vowel_spec.rad2, vowel_spec.rad3, vowel_spec.rad4
end
local function is_final_weak(base, vowel_spec)
return vowel_spec.weakness == "final-weak" or base.form == "XV"
end
local function link_term(text, face, id)
return m_links.full_link({lang = lang, term = text, tr = "-", id = id}, face)
end
local function tag_text(text, tag, class)
return m_links.full_link({lang = lang, alt = text, tr = "-"})
end
local function track(page)
require("Module:debug/track")("ar-verb/" .. page)
return true
end
local function track_if_ar_conj(base, page)
if base.alternant_multiword_spec.source_template == "ar-conj" then
require("Module:debug/track")("ar-verb/" .. page)
end
return true
end
-------------------------------------------------------------------------------
-- Basic functions to inflect tenses --
-------------------------------------------------------------------------------
local function skip_slot(base, slot, allow_overrides)
if base.slot_explicitly_missing[slot] then
return true
end
if not allow_overrides and base.slot_overrides[slot] and not base.slot_override_uses_default[slot] then
-- Skip any slots for which there are overrides, except those that request the default value using +, ++, etc.
return true
end
if base.passive == "nopass" and (slot == "pp" or slot:find("_pass")) then
return true
elseif base.passive == "onlypass" and slot ~= "pp" and slot ~= "vn" and not slot:find("_pass") then
return true
elseif base.passive == "ipass" and slot:find("_pass") and not slot:find("3ms") then
return true
elseif base.passive == "onlypass-impers" and slot ~= "pp" and slot ~= "vn" and (not slot:find("_pass") or
slot:find("_pass") and not slot:find("3ms")) then
return true
end
if base.nopast and slot:find("^past_") then
return true
end
if base.noimp and slot:find("^imp_") then
return true
end
if base.no_nonpast and (slot:find("^ind_") or slot:find("^sub_") or slot:find("^juss")) then
return true
end
return false
end
local function basic_combine_stem_ending(stem, ending)
return stem .. ending
end
local function basic_combine_stem_ending_tr(stem, ending)
return stem .. ending
end
-- Concatenate `prefixes`, `stems` and `endings` (any of which may be an abbreviate form list, i.e. strings, form
-- objects or lists of strings or form objects) and store into `slot`. If a user-supplied override exists for the slot,
-- nothing will happen unless `allow_overrides` is provided.
local function add3(base, slot, prefixes, stems, endings, allow_overrides)
if skip_slot(base, slot, allow_overrides) then
return
end
-- Optimization since the prefixes are almost always single strings.
if type(prefixes) == "string" then
local function do_combine_stem_ending(stem, ending)
return prefixes .. stem .. ending
end
local function do_combine_stem_ending_tr(stem, ending)
return transliterate(prefixes) .. stem .. ending
end
iut.add_forms(base.forms, slot, stems, endings, do_combine_stem_ending, transliterate,
do_combine_stem_ending_tr, base.form_footnotes)
else
iut.add_multiple_forms(base.forms, slot, {prefixes, stems, endings}, basic_combine_stem_ending, transliterate,
basic_combine_stem_ending_tr, base.form_footnotes)
end
end
-- Insert one or more forms in `form_or_forms` into `slot`. `form_or_forms` is an abbreviated form list (see comment at
-- top of [[Module:inflection utilities]]). If a user-supplied override exists for the slot, nothing will happen unless
-- `allow_overrides` is provided. BEWARE: One form object should never occur in two different slots, or twice in a given
-- slot; if taking a form object from an existing slot, make sure to shallowCopy() it.
local function insert_form_or_forms(base, slot, form_or_forms, allow_overrides, uncertain)
if not skip_slot(base, slot, allow_overrides) then
-- Some optimizations of the most common case of inserting a single string.
if type(form_or_forms) == "string" and not base.form_footnotes then
form_or_forms = {form = form_or_forms, uncertain = uncertain}
iut.insert_form(base.forms, slot, form_or_forms)
else
local list = iut.convert_to_general_list_form(form_or_forms, base.form_footnotes)
if uncertain then
for _, formobj in ipairs(list) do
formobj.uncertain = true
end
end
iut.insert_forms(base.forms, slot, list)
end
end
end
-- Insert `string_or_form` into both the ap2 and pp2 slots, shallowCopying a form object to make sure no form objects
-- occur in two slots.
local function insert_ap2_pp2(base, string_or_form)
insert_form_or_forms(base, "ap2", string_or_form)
if type(string_or_form) == "table" then
string_or_form = m_table.shallowCopy(string_or_form)
end
insert_form_or_forms(base, "pp2", string_or_form)
end
-- Convert `stemforms` (a string, a form object, or a list of strings and/or form objects) into "general form" (a list
-- of form objects) and map `fn` over the list of objects. `fn` is passed two arguments (form value and translit) and
-- should likewise return the new form value and translit. Footnotes will be preserved. FIXME: Preserve other metadata.
local function map_general(stemforms, fn)
return iut.map_forms(iut.convert_to_general_list_form(stemforms), fn)
end
-- Similar to map_general() except that `fn` should return a single value (one or more strings or form objects), instead
-- of two values (form value and translit), and the resulting value(s) from all calls to `fn` will be flattened to
-- construct the overall return value. Footnotes will be preserved. FIXME: Preserve other metadata.
local function flatmap_general(stemforms, fn)
return iut.flatmap_forms(iut.convert_to_general_list_form(stemforms), fn)
end
-- Given user-supplied stem overrides in `base`, construct any derived stem overrides (e.g. vowel-specific or
-- consonant-specific variants), and truncate initial y-/ي- in any non-past overrides.
local function construct_stems(base)
local stems = base.stem_overrides
stems.past_v = stems.past_v or stems.past
stems.past_c = stems.past_c or stems.past
stems.past_pass_v = stems.past_pass_v or stems.past_pass
stems.past_pass_c = stems.past_pass_c or stems.past_pass
stems.nonpast_v = stems.nonpast_v or stems.nonpast
stems.nonpast_c = stems.nonpast_c or stems.nonpast
stems.nonpast_pass_v = stems.nonpast_pass_v or stems.nonpast_pass
stems.nonpast_pass_c = stems.nonpast_pass_c or stems.nonpast_pass
stems.imp_v = stems.imp_v or stems.imp
stems.imp_c = stems.imp_c or stems.imp
local function truncate_nonpast_initial_cons(stem_type, form, translit)
if form == "+" then
return form, translit
end
if not form:find("^" .. Y) then
error(("Form value %s for stem type '%s' should begin with ي"):format(form, stem_type))
end
form = form:gsub("^" .. Y, "")
if translit then
if not translit:find("^y") then
error(("Translit value %s for stem type '%s' should begin with y"):format(translit, stem_type))
end
translit = translit:gsub("^y", "")
end
return form, translit
end
for _, nonpast_stem_type in ipairs { "nonpast_v", "nonpast_c", "nonpast_pass_v", "nonpast_pass_c" } do
if stems[nonpast_stem_type] then
stems[nonpast_stem_type] = map_general(stems[nonpast_stem_type], function(form, translit)
return truncate_nonpast_initial_cons(nonpast_stem_type, form, translit)
end)
end
end
end
-- Given user-specified overrides for stem `stemname`, return overrides with occurrences of + replaced by
-- `default_stem`. If no overrides, return `default_stem`, or {} if no default.
local function override_stem_if_needed(base, stemname, default_stem)
local overrides = base.stem_overrides[stemname]
if not overrides then
return default_stem or {}
end
return map_general(overrides, function(form, translit)
if form ~= "+" and default_indicator_to_active_participle_slot[form] then
error(("Stem overrides cannot use secondary default indicators but saw %s in stem override '%s'"):format(
form, stemname))
end
if form == "+" then
if translit then
error(("Cannot supply manual translit along with + for stem override '%s'"):format(stemname))
end
if not default_stem then
error(("Cannot use + for stem override '%s' because no default is available"):format(stemname))
end
if type(default_stem) ~= "string" then
error(("Internal error: Default stem for '%s' is not a string: %s"):format(stemname, dump(default_stem)))
end
return default_stem
end
return form, translit
end)
end
-------------------------------------------------------------------------------
-- Properties of different verbal forms --
-------------------------------------------------------------------------------
local allowed_vforms = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX",
"X", "XI", "XII", "XIII", "XIV", "XV", "Iq", "IIq", "IIIq", "IVq"}
local allowed_vforms_set = m_table.listToSet(allowed_vforms)
local allowed_vforms_with_weakness = m_table.shallowCopy(allowed_vforms)
-- The user needs to be able to explicitly specify that a form-I verb (specifically one whose initial radical is و) is
-- sound. Cf. wajiʕa yawjaʕu (not #yajaʕu) "to ache, to hurt". In general, i~a and u~u verbs whose initial radical is و
-- seem to not assimilate the first radical; cf. وقح "to be shameless", variously waqaḥa~yaqiḥu, waquḥa~yawquḥu and
-- waqiḥa~yawqaḥu, whereas a~i verbs (wafaḍa~yafiḍu "to rush"), i~i verbs (wafiqa~yafiqu "to be proper, to be suitable")
-- and a~a verbs (waḍaʕa~yaḍaʕu "to set down, to place") do assimilate. But there are naturally exceptions, e.g.
-- waṭiʔa~yaṭaʔu "to tread, to trample"; wasiʕa~yasaʕu "to be spacious; to be well-off"; waṯiʔa~yaṯaʔu "to get bruised,
-- to be sprained". Also beware of waniya~yawnā "to be faint; to languish", which is sound in the first radical and
-- final-weak in the last radical. Nonetheless, the regularity of the patterns mentioned above suggest we should provide
-- them as defaults.
-- Note that there are other cases of unexpectedly sound verbs, e.g. izdawaja~yazdawiju "to be in pairs", layisa~yalyasu
-- "to be valiant, to be brave", ʔaḥwaja~yuḥwiju "to need", istahwana~yastahwinu "to consider easy", sawisa~yaswasu "to
-- be or become moth-eaten or worm-eaten" (vs. sāsa~yasūsu "to govern, to rule" from the same radicals), ʕawira~yaʕwaru
-- "to be one-eyed", istajwaba~yastajwibu "to interrogate", etc. But in these cases there is no need for explicit user
-- specification as the lemma itself specifies the unexpected soundness.
for _, form_with_weakness in ipairs { "I-sound", "I-assimilated", "none-sound", "none-hollow", "none-geminate",
"none-final-weak" } do
table.insert(allowed_vforms_with_weakness, form_with_weakness)
end
local allowed_vforms_with_weakness_set = m_table.listToSet(allowed_vforms_with_weakness)
local function vform_supports_final_weak(vform)
return vform ~= "XI" and vform ~= "XV" and vform ~= "IVq"
end
local function vform_supports_geminate(vform)
return vform == "I" or vform == "III" or vform == "IV" or vform == "VI" or vform == "VII" or vform == "VIII" or
vform == "X"
end
local function vform_supports_hollow(vform)
return vform == "I" or vform == "IV" or vform == "VII" or vform == "VIII" or vform == "X"
end
local function vform_probably_impersonal_passive(vform, weakness, past_vowel, nonpast_vowel)
return vform == "I" and req(past_vowel, I) or vform == "V" or vform == "VI" or vform == "X" or vform == "IIq"
end
local function vform_probably_full_passive(vform)
return vform == "II" or vform == "III" or vform == "IV" or vform == "Iq"
end
local function vform_probably_no_passive(vform, weakness, past_vowel, nonpast_vowel)
return vform == "I" and req(past_vowel, U) or vform == "VII" or vform == "IX" or
vform == "XI" or vform == "XII" or vform == "XIII" or vform == "XIV" or vform == "XV" or
vform == "IIIq" or vform == "IVq"
end
-- Active vforms II, III, IV, Iq use non-past prefixes in -u- instead of -a-.
local function prefix_vowel_from_vform(vform)
if vform == "II" or vform == "III" or vform == "IV" or vform == "Iq" then
return "u"
else
return "a"
end
end
-- True if the active non-past takes a-vocalization rather than i-vocalization in its last syllable.
local function vform_nonpast_a_vowel(vform)
return vform == "V" or vform == "VI" or vform == "XV" or vform == "IIq"
end
-- True if the `passive` spec indicates a passive-only verb.
local function is_passive_only(passive)
return passive == "onlypass" or passive == "onlypass-impers"
end
export.is_passive_only = is_passive_only -- for use in [[Module:ar-headword]]
-------------------------------------------------------------------------------
-- Properties of specific sounds --
-------------------------------------------------------------------------------
-- Is radical wāw (و) or yāʾ (ي)?
local function is_waw_ya(rad)
return req(rad, W) or req(rad, Y)
end
-- Check that radical is wāw (و) or yāʾ (ي), error if not
local function check_waw_ya(rad)
if not is_waw_ya(rad) then
error("Expecting weak radical: '" .. rget(rad) .. "' should be " .. W .. " or " .. Y)
end
end
-- Form-I verb حيّ or حيي and form-X verb استحيا or استحى
local function hayy_radicals(rad1, rad2, rad3)
return req(rad1, "ح") and req(rad2, Y) and is_waw_ya(rad3)
end
-- FUCK ME HARD. "Lua error at line 1514: main function has more than 200 local variables".
local function create_conjugations()
-------------------------------------------------------------------------------
-- Radicals associated with various irregular verbs --
-------------------------------------------------------------------------------
-- Form-I verb أخذ or form-VIII verb اتخذ
local function axadh_radicals(rad1, rad2, rad3)
return req(rad1, HAMZA) and req(rad2, "خ") and req(rad3, "ذ")
end
-- Form-I verb whose imperative has a reduced form: أكل and أخذ and أمر. Return "shortonly" if only
-- short-form imperatives exist (أكل and أخذ) or "shortlong" if long-form imperatives also exist (أمر);
-- they are used after a clitic like فَ and وَ.
local function reduced_imperative_verb(rad1, rad2, rad3)
return axadh_radicals(rad1, rad2, rad3) and "shortonly" or
req(rad1, HAMZA) and req(rad2, "ك") and req(rad3, "ل") and "shortonly" or
req(rad1, HAMZA) and req(rad2, "م") and req(rad3, "ر") and "shortlong"
end
-- Form-I verb رأى and form-IV verb أرى
local function raa_radicals(rad1, rad2, rad3)
return req(rad1, "ر") and req(rad2, HAMZA) and is_waw_ya(rad3)
end
-- Form-I verb سأل
local function saal_radicals(rad1, rad2, rad3)
return req(rad1, "س") and req(rad2, HAMZA) and req(rad3, "ل")
end
-- Form-I verb كان
local function kaan_radicals(rad1, rad2, rad3)
return req(rad1, "ك") and req(rad2, W) and req(rad3, N)
end
-------------------------------------------------------------------------------
-- Sets of past endings --
-------------------------------------------------------------------------------
-- The 14 endings of the sound/hollow/geminate past tense.
local past_endings = {
-- singular
SK .. TU, SK .. TA, SK .. "تِ", A, A .. "تْ",
--dual
SK .. "نَا", SK .. "تُمَا", AA, A .. "تَا",
-- plural
SK .. "نَا", SK .. "تُمْ",
-- shadda + vowel diacritic ends up in the wrong order due to Unicode
-- bug, so keep them separate to avoid this
SK .. "تُن" .. SH .. A, UU .. ALIF, SK .. "نَ"
}
-- Make endings for final-weak past in -aytu or -awtu. AYAW is AY or AW as appropriate. Note that AA and AW are
-- global variables.
local function make_past_endings_ay_aw(ayaw, third_sg_masc)
return {
-- singular
ayaw .. SK .. TU, ayaw .. SK .. TA, ayaw .. SK .. "تِ", third_sg_masc, A .. "تْ",
--dual
ayaw .. SK .. "نَا", ayaw .. SK .. "تُمَا", ayaw .. AA, A .. "تَا",
-- plural
ayaw .. SK .. "نَا", ayaw .. SK .. "تُمْ",
-- shadda + vowel diacritic ends up in the wrong order due to Unicode
-- bug, so keep them separate to avoid this
ayaw .. SK .. "تُن" .. SH .. A, AW .. SK .. ALIF, ayaw .. SK .. "نَ"
}
end
-- past final-weak -aytu endings
local past_endings_ay = make_past_endings_ay_aw(AY, AAMAQ)
-- past final-weak -awtu endings
local past_endings_aw = make_past_endings_ay_aw(AW, AA)
-- used for alternative endings for form-X geminate verbs like اِسْتَمَرَّ
local past_endings_ay_12_person_only = {
-- singular
AY .. SK .. TU, AY .. SK .. TA, AY .. SK .. "تِ", {}, {},
--dual
AY .. SK .. "نَا", AY .. SK .. "تُمَا", {}, {},
-- plural
AY .. SK .. "نَا", AY .. SK .. "تُمْ",
-- shadda + vowel diacritic ends up in the wrong order due to Unicode
-- bug, so keep them separate to avoid this
AY .. SK .. "تُن" .. SH .. A, {}, {},
}
-- Make endings for final-weak past in -ītu or -ūtu. IIUU is ī or ū as appropriate. Note that AA and UU are global
-- variables.
local function make_past_endings_ii_uu(iiuu)
return {
-- singular
iiuu .. TU, iiuu .. TA, iiuu .. "تِ", iiuu .. A, iiuu .. A .. "تْ",
--dual
iiuu .. "نَا", iiuu .. "تُمَا", iiuu .. AA, iiuu .. A .. "تَا",
-- plural
iiuu .. "نَا", iiuu .. "تُمْ",
-- shadda + vowel diacritic ends up in the wrong order due to Unicode
-- bug, so keep them separate to avoid this
iiuu .. "تُن" .. SH .. A, UU .. ALIF, iiuu .. "نَ"
}
end
-- past final-weak -ītu endings
local past_endings_ii = make_past_endings_ii_uu(II)
-- past final-weak -ūtu endings
local past_endings_uu = make_past_endings_ii_uu(UU)
-------------------------------------------------------------------------------
-- Sets of non-past prefixes and endings --
-------------------------------------------------------------------------------
local nonpast_prefix_consonants = {
-- singular
HAMZA, T, T, Y, T,
-- dual
N, T, Y, T,
-- plural
N, T, T, Y, Y
}
-- There are only five distinct endings in all non-past verbs. Make any set of non-past endings given these five
-- distinct endings.
local function make_nonpast_endings(null, fem, dual, pl, fempl)
return {
-- singular
null, null, fem, null, null,
-- dual
null, dual, dual, dual,
-- plural
null, pl, fempl, pl, fempl
}
end
-- endings for non-past indicative
local ind_endings = make_nonpast_endings(
U,
II .. NA,
AANI,
UU .. NA,
SK .. NA
)
-- Make the endings for non-past subjunctive/jussive, given the vowel diacritic used in "null" endings
-- (1s/2ms/3ms/3fs/1p).
local function make_sub_juss_endings(dia_null)
return make_nonpast_endings(
dia_null,
II,
AA,
UU .. ALIF,
SK .. NA
)
end
-- endings for non-past subjunctive
local sub_endings = make_sub_juss_endings(A)
-- endings for non-past jussive
local juss_endings = make_sub_juss_endings(SK)
-- endings for alternative geminate non-past jussive in -a; same as subjunctive
local juss_endings_alt_a = sub_endings
-- endings for alternative geminate non-past jussive in -i
local juss_endings_alt_i = make_sub_juss_endings(I)
-- Endings for final-weak non-past indicative in -ā. Note that AY, AW and AAMAQ are global variables.
local ind_endings_aa = make_nonpast_endings(
AAMAQ,
AYSK .. NA,
AY .. AANI,
AWSK .. NA,
AYSK .. NA
)
-- Make endings for final-weak non-past indicative in -ī or -ū; IIUU is ī or ū as appropriate. Note that II and UU
-- are global variables.
local function make_ind_endings_ii_uu(iiuu)
return make_nonpast_endings(
iiuu,
II .. NA,
iiuu .. AANI,
UU .. NA,
iiuu .. NA
)
end
-- endings for final-weak non-past indicative in -ī
local ind_endings_ii = make_ind_endings_ii_uu(II)
-- endings for final-weak non-past indicative in -ū
local ind_endings_uu = make_ind_endings_ii_uu(UU)
-- Endings for final-weak non-past subjunctive in -ā. Note that AY, AW, ALIF, AAMAQ are global variables.
local sub_endings_aa = make_nonpast_endings(
AAMAQ,
AYSK,
AY .. AA,
AWSK .. ALIF,
AYSK .. NA
)
-- Make endings for final-weak non-past subjunctive in -ī or -ū. IIUU is ī or ū as appropriate. Note that AA, II,
-- UU, ALIF are global variables.
local function make_sub_endings_ii_uu(iiuu)
return make_nonpast_endings(
iiuu .. A,
II,
iiuu .. AA,
UU .. ALIF,
iiuu .. NA
)
end
-- endings for final-weak non-past subjunctive in -ī
local sub_endings_ii = make_sub_endings_ii_uu(II)
-- endings for final-weak non-past subjunctive in -ū
local sub_endings_uu = make_sub_endings_ii_uu(UU)
-- endings for final-weak non-past jussive in -ā
local juss_endings_aa = make_nonpast_endings(
A,
AYSK,
AY .. AA,
AWSK .. ALIF,
AYSK .. NA
)
-- Make endings for final-weak non-past jussive in -ī or -ū. IU is short i or u, IIUU is long ī or ū as appropriate.
-- Note that AA, II, UU, ALIF are global variables.
local function make_juss_endings_ii_uu(iu, iiuu)
return make_nonpast_endings(
iu,
II,
iiuu .. AA,
UU .. ALIF,
iiuu .. NA
)
end
-- endings for final-weak non-past jussive in -ī
local juss_endings_ii = make_juss_endings_ii_uu(I, II)
-- endings for final-weak non-past jussive in -ū
local juss_endings_uu = make_juss_endings_ii_uu(U, UU)
-------------------------------------------------------------------------------
-- Sets of imperative endings --
-------------------------------------------------------------------------------
-- Extract the second person jussive endings to get corresponding imperative endings.
local function imperative_endings_from_jussive(endings)
return {endings[2], endings[3], endings[7], endings[11], endings[12]}
end
-- normal imperative endings
local imp_endings = imperative_endings_from_jussive(juss_endings)
-- alternative geminate imperative endings in -a
local imp_endings_alt_a = imperative_endings_from_jussive(juss_endings_alt_a)
-- alternative geminate imperative endings in -i
local imp_endings_alt_i = imperative_endings_from_jussive(juss_endings_alt_i)
-- final-weak imperative endings in -ā
local imp_endings_aa = imperative_endings_from_jussive(juss_endings_aa)
-- final-weak imperative endings in -ī
local imp_endings_ii = imperative_endings_from_jussive(juss_endings_ii)
-- final-weak imperative endings in -ū
local imp_endings_uu = imperative_endings_from_jussive(juss_endings_uu)
-------------------------------------------------------------------------------
-- Basic functions to inflect tenses --
-------------------------------------------------------------------------------
-- Add to `base` the inflections for the tense indicated by `tense` (the prefix in the slot names, e.g. 'past'
-- or 'juss_pass'), formed by combining the `prefixes`, `stems` and `endings`. Each of `prefixes`, `stems` and
-- `endings` is either a sequence of 5 (for the imperative) or 13 (for other tenses) abbreviated form lists (each of
-- which is either a string, a form object, or a list of strings and/or form objects; see
-- [[Module:inflection utilities]] for more info). Alternatively, any of `prefixes`, `stems` or `endings` can be a
-- single-element list containing an abbreviated form list, with an additional key `all_same` set to true, or (as a
-- special case) a single string; in the latter cases, the same value is used for all 5 or 13 slots. If existing
-- inflections already exist, they will be added to, not overridden. `pnums` is the list of person/number slot name
-- suffixes, which must match up with the elements in `prefixes`, `stems` and `endings` (i.e. 5 for imperative, 13
-- otherwise).
local function inflect_tense_1(base, tense, prefixes, stems, endings, pnums)
if not prefixes or not stems or not endings then
return
end
local function verify_affixes(affixname, affixes)
local function interr(msg)
error(("Internal error: For tense '%s', '%s' %s: %s"):format(tense, affixname, msg, dump(affixes)))
end
if type(affixes) == "string" then
-- do nothing
elseif type(affixes) ~= "table" then
interr("is not a table or string")
elseif affixes.all_same then
if #affixes ~= 1 then
interr(("with all_same = true should have length 1 but has length %s"):format(#affixes))
end
else
if #affixes ~= #pnums then
interr(("should have length %s but has length %s"):format(#pnums, #affixes))
end
end
end
verify_affixes("prefixes", prefixes)
verify_affixes("stems", stems)
verify_affixes("endings", endings)
local function get_affix(affixes, i)
if type(affixes) == "string" then
return affixes
elseif affixes.all_same then
return affixes[1]
else
return affixes[i]
end
end
for i, pnum in ipairs(pnums) do
local prefix = get_affix(prefixes, i)
local stem = get_affix(stems, i)
local ending = get_affix(endings, i)
local slot = tense .. "_" .. pnum
add3(base, slot, prefix, stem, ending)
end
end
-- Add to `base` the inflections for the tense indicated by `tense` (the prefix in the slot names, e.g. 'past'
-- or 'juss_pass'), formed by combining the `prefixes`, `stems` and `endings`. This is a simple wrapper around
-- inflect_tense_1() that applies to all tenses other than the imperative; see inflect_tense_1() for more
-- information about the parameters.
local function inflect_tense(base, tense, prefixes, stems, endings)
inflect_tense_1(base, tense, prefixes, stems, endings, all_person_number_list)
end
-- Like inflect_tense() but for the imperative, which has only five parts instead of 13 and no prefixes.
local function inflect_tense_imp(base, stems, endings)
inflect_tense_1(base, "imp", "", stems, endings, imp_person_number_list)
end
-------------------------------------------------------------------------------
-- Functions to inflect the past tense --
-------------------------------------------------------------------------------
-- Generate past verbs using specified vowel and consonant stems; works for sound, assimilated, hollow, and geminate
-- verbs, active and passive.
local function past_2stem_conj(base, tense, v_stem, c_stem, footnote_12)
local passive = tense:find("_pass") and "_pass" or ""
-- Override stems with user-specified stems if available.
v_stem = override_stem_if_needed(base, "past" .. passive .. "_v", v_stem)
local c_stem_12 = c_stem
if footnote_12 then
c_stem_12 = iut.combine_form_and_footnotes(c_stem_12, footnote_12)
end
c_stem_12 = override_stem_if_needed(base, "past" .. passive .. "_c", c_stem_12)
local c_stem_3 = override_stem_if_needed(base, "past" .. passive .. "_c", c_stem)
inflect_tense(base, tense, "", {
-- singular
c_stem_12, c_stem_12, c_stem_12, v_stem, v_stem,
--dual
c_stem_12, c_stem_12, v_stem, v_stem,
-- plural
c_stem_12, c_stem_12, c_stem_12, v_stem, c_stem_3
}, past_endings)
end
-- Generate past verbs using single specified stem; works for sound and assimilated verbs, active and passive.
local function past_1stem_conj(base, tense, stem)
past_2stem_conj(base, tense, stem, stem)
end
-------------------------------------------------------------------------------
-- Functions to inflect non-past tenses --
-------------------------------------------------------------------------------
-- Generate non-past conjugation, with two stems, for vowel-initial and consonant-initial endings, respectively.
-- Useful for active and passive; for all forms; for all weaknesses (sound, assimilated, hollow, final-weak and
-- geminate) and for all types of non-past (indicative, subjunctive, jussive) except for the imperative. (There is a
-- separate wrapper function below for geminate jussives because they have three alternants.) Both stems may be the
-- same, e.g. for sound verbs.
-- `prefix_vowel` will be either "a" or "u". `endings` should be an array of 13 items. If `endings` is nil or
-- omitted, infer the endings from the tense. If `jussive` is true, or `endings` is nil and `tense` indicatives
-- jussive, use the jussive pattern of vowel/consonant stems (different from the normal ones).
local function nonpast_2stem_conj(base, tense, prefix_vowel, v_stem, c_stem, endings, jussive)
local passive = tense:find("_pass") and "_pass" or ""
-- Override stems with user-specified stems if available.
v_stem = override_stem_if_needed(base, "nonpast" .. passive .. "_v",
v_stem and q(dia[prefix_vowel], v_stem) or nil)
c_stem = override_stem_if_needed(base, "nonpast" .. passive .. "_c",
c_stem and q(dia[prefix_vowel], c_stem) or nil)
if not endings then
if tense:find("^ind") then
endings = ind_endings
elseif tense:find("^sub") then
endings = sub_endings
elseif tense:find("^juss") then
jussive = true
endings = juss_endings
else
error("Internal error: Unrecognized tense '" .. tense .."'")
end
end
if not jussive then
inflect_tense(base, tense, nonpast_prefix_consonants, {
-- singular
v_stem, v_stem, v_stem, v_stem, v_stem,
--dual
v_stem, v_stem, v_stem, v_stem,
-- plural
v_stem, v_stem, c_stem, v_stem, c_stem
}, endings)
else
inflect_tense(base, tense, nonpast_prefix_consonants, {
-- singular
-- 'adlul, tadlul, tadullī, yadlul, tadlul
c_stem, c_stem, v_stem, c_stem, c_stem,
--dual
-- nadlul, tadullā, yadullā, tadullā
c_stem, v_stem, v_stem, v_stem,
-- plural
-- nadlul, tadullū, tadlulna, yadullū, yadlulna
c_stem, v_stem, c_stem, v_stem, c_stem
}, endings)
end
end
-- Generate non-past conjugation with one stem (no distinct stems for vowel-initial and consonant-initial endings).
-- See nonpast_2stem_conj().
local function nonpast_1stem_conj(base, tense, prefix_vowel, stem, endings, jussive)
nonpast_2stem_conj(base, tense, prefix_vowel, stem, stem, endings, jussive)
end
-- Generate active/passive jussive geminative. There are three alternants, two with terminations -a and -i and one
-- in a null termination with a distinct pattern of vowel/consonant stem usage. See nonpast_2stem_conj() for a
-- description of the arguments.
local function jussive_gem_conj(base, tense, prefix_vowel, v_stem, c_stem)
-- alternative in -a
nonpast_2stem_conj(base, tense, prefix_vowel, v_stem, c_stem, juss_endings_alt_a)
-- alternative in -i
nonpast_2stem_conj(base, tense, prefix_vowel, v_stem, c_stem, juss_endings_alt_i)
-- alternative in -null; requires different combination of v_stem and
-- c_stem since the null endings require the c_stem (e.g. "tadlul" here)
-- whereas the corresponding endings above in -a or -i require the v_stem
-- (e.g. "tadulla, tadulli" above)
nonpast_2stem_conj(base, tense, prefix_vowel, v_stem, c_stem, juss_endings, "jussive")
end
-------------------------------------------------------------------------------
-- Functions to inflect the imperative --
-------------------------------------------------------------------------------
-- Generate imperative conjugation, with two stems, for vowel-initial and consonant-initial endings, respectively.
-- Useful for all forms, and for all weaknesses other than final-weak. Note that the two stems may be the same
-- (specifically for sound and assimilated verbs). If `endings` is nil or omitted, use `imp_endings`. If `alt_gem`
-- is specified, use the pattern of vowel and consonant stems appropriate for the alternative geminate imperatives
-- that use a null ending of -a or -i instead of an empty ending.
local function make_2stem_imperative(base, v_stem, c_stem, endings, alt_gem)
endings = endings or imp_endings
-- Override stems with user-specified stems if available.
v_stem = override_stem_if_needed(base, "imp_v", v_stem)
c_stem = override_stem_if_needed(base, "imp_c", c_stem)
if alt_gem then
inflect_tense_imp(base, {v_stem, v_stem, v_stem, v_stem, c_stem}, endings)
else
inflect_tense_imp(base, {c_stem, v_stem, v_stem, v_stem, c_stem}, endings)
end
end
-- Generate imperative parts for sound or assimilated verbs.
local function make_1stem_imperative(base, stem)
make_2stem_imperative(base, stem, stem)
end
-- Generate imperative parts for geminate verbs form I (also IV, VII, VIII, X).
local function make_gem_imperative(base, v_stem, c_stem)
make_2stem_imperative(base, v_stem, c_stem, imp_endings_alt_a, "alt gem")
make_2stem_imperative(base, v_stem, c_stem, imp_endings_alt_i, "alt gem")
make_2stem_imperative(base, v_stem, c_stem)
end
-------------------------------------------------------------------------------
-- Functions to inflect entire verbs --
-------------------------------------------------------------------------------
-- Generate finite parts of a sound verb (also works for assimilated verbs) from five stems (past and non-past,
-- active and passive, plus imperative) plus the prefix vowel in the active non-past ("a" or "u").
local function make_sound_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem, imp_stem,
prefix_vowel)
past_1stem_conj(base, "past", past_stem)
past_1stem_conj(base, "past_pass", past_pass_stem)
nonpast_1stem_conj(base, "ind", prefix_vowel, nonpast_stem)
nonpast_1stem_conj(base, "sub", prefix_vowel, nonpast_stem)
nonpast_1stem_conj(base, "juss", prefix_vowel, nonpast_stem)
nonpast_1stem_conj(base, "ind_pass", "u", nonpast_pass_stem)
nonpast_1stem_conj(base, "sub_pass", "u", nonpast_pass_stem)
nonpast_1stem_conj(base, "juss_pass", "u", nonpast_pass_stem)
make_1stem_imperative(base, imp_stem)
end
local function past_final_weak_endings_from_vowel(vowel)
if vowel == "ay" then
return past_endings_ay
elseif vowel == "aw" then
return past_endings_aw
elseif vowel == "ī" then
return past_endings_ii
elseif vowel == "ū" then
return past_endings_uu
elseif not vowel then
return nil
else
error(("Internal error: Unrecognized past final-weak vowel spec '%s'"):format(vowel))
end
end
local function nonpast_final_weak_endings_from_vowel(vowel)
if vowel == "ā" then
return ind_endings_aa, sub_endings_aa, juss_endings_aa, imp_endings_aa
elseif vowel == "ī" then
return ind_endings_ii, sub_endings_ii, juss_endings_ii, imp_endings_ii
elseif vowel == "ū" then
return ind_endings_uu, sub_endings_uu, juss_endings_uu, imp_endings_uu
elseif not vowel then
return nil
else
error(("Internal error: Unrecognized non-past final-weak vowel spec '%s'"):format(vowel))
end
end
-- Generate finite parts of a final-weak verb from five stems (past and non-past, active and passive, plus
-- imperative), the past active ending vowel (ay, aw, ī or ū), the non-past active ending vowel (ā, ī or ū) and the
-- prefix vowel in the active non-past (a or u).
local function make_final_weak_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem, imp_stem,
past_ending_vowel, nonpast_ending_vowel, prefix_vowel)
past_stem = override_stem_if_needed(base, "past", past_stem)
past_pass_stem = override_stem_if_needed(base, "past_pass", past_pass_stem)
-- Don't call override_stem_if_needed() here for non-past stems; it's called in nonpast_2stem_conj().
imp_stem = override_stem_if_needed(base, "imp", imp_stem)
-- + not supported for ending vowel overrides
past_ending_vowel = base.stem_overrides.past_final_weak_vowel or past_ending_vowel
local past_pass_ending_vowel = base.stem_overrides.past_pass_final_weak_vowel or "ī"
nonpast_ending_vowel = base.stem_overrides.nonpast_final_weak_vowel or nonpast_ending_vowel
local nonpast_pass_ending_vowel = base.stem_overrides.nonpast_pass_final_weak_vowel or "ā"
local past_endings = past_final_weak_endings_from_vowel(past_ending_vowel)
local past_pass_endings = past_final_weak_endings_from_vowel(past_pass_ending_vowel)
local ind_endings, sub_endings, juss_endings, imp_endings =
nonpast_final_weak_endings_from_vowel(nonpast_ending_vowel)
local ind_pass_endings, sub_pass_endings, juss_pass_endings =
nonpast_final_weak_endings_from_vowel(nonpast_pass_ending_vowel)
inflect_tense(base, "past", "", {past_stem, all_same = 1}, past_endings)
inflect_tense(base, "past_pass", "", {past_pass_stem, all_same = 1}, past_pass_endings)
nonpast_1stem_conj(base, "ind", prefix_vowel, nonpast_stem, ind_endings)
nonpast_1stem_conj(base, "sub", prefix_vowel, nonpast_stem, sub_endings)
nonpast_1stem_conj(base, "juss", prefix_vowel, nonpast_stem, juss_endings)
nonpast_1stem_conj(base, "ind_pass", "u", nonpast_pass_stem, ind_pass_endings)
nonpast_1stem_conj(base, "sub_pass", "u", nonpast_pass_stem, sub_pass_endings)
nonpast_1stem_conj(base, "juss_pass", "u", nonpast_pass_stem, juss_pass_endings)
inflect_tense_imp(base, {imp_stem, all_same = 1}, imp_endings)
end
-- Generate finite parts of an augmented (form II+) final-weak verb from five stems (past and non-past, active and
-- passive, plus imperative) plus the prefix vowel in the active non-past ("a" or "u") and a flag indicating if it
-- behaves like a form V/VI verb in taking non-past endings in -ā instead of -ī.
local function make_augmented_final_weak_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem,
imp_stem, prefix_vowel, form56)
make_final_weak_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem, imp_stem, "ay",
form56 and "ā" or "ī", prefix_vowel)
end
-- Generate finite parts of an augmented (form II+) sound or final-weak verb, given:
-- * `base` (conjugation data structure);
-- * `vowel_spec` (radicals, weakness);
-- * `past_stem_base` (active past stem minus last syllable (= -al or -ā));
-- * `nonpast_stem_base` (non-past stem minus last syllable (= -al/-il or -ā/-ī);
-- * `past_pass_stem_base` (passive past stem minus last syllable (= -il or -ī));
-- * `vn` (verbal noun).
local function make_augmented_sound_final_weak_verb(base, vowel_spec, past_stem_base, nonpast_stem_base,
past_pass_stem_base, vn)
insert_form_or_forms(base, "vn", vn)
local lastrad = base.quadlit and vowel_spec.rad4 or vowel_spec.rad3
local final_weak = is_final_weak(base, vowel_spec)
local prefix_vowel = prefix_vowel_from_vform(base.verb_form)
local form56 = vform_nonpast_a_vowel(base.verb_form)
local a_base_suffix = final_weak and "" or q(A, lastrad)
local i_base_suffix = final_weak and "" or q(I, lastrad)
-- past and non-past stems, active and passive
local past_stem = q(past_stem_base, a_base_suffix)
-- In forms 5 and 6, non-past has /a/ as last stem vowel in the non-past
-- in both active and passive, but /i/ in the active participle and /a/
-- in the passive participle. Elsewhere, consistent /i/ in active non-past
-- and participle, consistent /a/ in passive non-past and participle.
-- Hence, forms 5 and 6 differ only in the non-past active (but not
-- active participle), so we have to split the finite non-past stem and
-- active participle stem.
local nonpast_stem = q(nonpast_stem_base, form56 and a_base_suffix or i_base_suffix)
local ap_stem = q(nonpast_stem_base, i_base_suffix)
local past_pass_stem = q(past_pass_stem_base, i_base_suffix)
local nonpast_pass_stem = q(nonpast_stem_base, a_base_suffix)
-- imperative stem
local imp_stem = q(past_stem_base, form56 and a_base_suffix or i_base_suffix)
-- make parts
if final_weak then
make_augmented_final_weak_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem, imp_stem,
prefix_vowel, form56)
else
make_sound_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem, imp_stem, prefix_vowel)
end
-- active and passive participle
if final_weak then
insert_form_or_forms(base, "ap", q(MU, ap_stem, IN))
insert_form_or_forms(base, "pp", q(MU, nonpast_pass_stem, AN, AMAQ))
else
insert_form_or_forms(base, "ap", q(MU, ap_stem))
insert_form_or_forms(base, "pp", q(MU, nonpast_pass_stem))
end
end
-- Generate finite parts of a hollow or geminate verb from ten stems (vowel and consonant stems for each of past and
-- non-past, active and passive, plus imperative) plus the prefix vowel in the active non-past ("a" or "u"), plus a
-- flag indicating if we are a geminate verb.
local function make_hollow_geminate_verb(base, geminate, past_v_stem, past_c_stem, past_pass_v_stem,
past_pass_c_stem, nonpast_v_stem, nonpast_c_stem, nonpast_pass_v_stem, nonpast_pass_c_stem, imp_v_stem,
imp_c_stem, prefix_vowel, altgem_note)
past_2stem_conj(base, "past", past_v_stem, past_c_stem, altgem_note)
past_2stem_conj(base, "past_pass", past_pass_v_stem, past_pass_c_stem)
nonpast_2stem_conj(base, "ind", prefix_vowel, nonpast_v_stem, nonpast_c_stem)
nonpast_2stem_conj(base, "sub", prefix_vowel, nonpast_v_stem, nonpast_c_stem)
nonpast_2stem_conj(base, "ind_pass", "u", nonpast_pass_v_stem, nonpast_pass_c_stem)
nonpast_2stem_conj(base, "sub_pass", "u", nonpast_pass_v_stem, nonpast_pass_c_stem)
if geminate then
jussive_gem_conj(base, "juss", prefix_vowel, nonpast_v_stem, nonpast_c_stem)
jussive_gem_conj(base, "juss_pass", "u", nonpast_pass_v_stem, nonpast_pass_c_stem)
make_gem_imperative(base, imp_v_stem, imp_c_stem)
else
nonpast_2stem_conj(base, "juss", prefix_vowel, nonpast_v_stem, nonpast_c_stem)
nonpast_2stem_conj(base, "juss_pass", "u", nonpast_pass_v_stem, nonpast_pass_c_stem)
make_2stem_imperative(base, imp_v_stem, imp_c_stem)
end
end
-- Generate finite parts of an augmented (form II+) hollow verb, given:
-- * `base` (conjugation data structure);
-- * `vowel_spec` (radicals, weakness);
-- * `past_stem_base` (invariable part of active past stem);
-- * `nonpast_stem_base` (invariable part of nonpast stem);
-- * `past_pass_stem_base` (invariable part of passive past stem);
-- * `vn` (verbal noun).
local function make_augmented_hollow_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base,
vn)
insert_form_or_forms(base, "vn", vn)
local lastrad = base.quadlit and vowel_spec.rad4 or vowel_spec.rad3
local form410 = base.verb_form == "IV" or base.verb_form == "X"
local prefix_vowel = prefix_vowel_from_vform(base.verb_form)
local a_base_suffix_v, a_base_suffix_c
local i_base_suffix_v, i_base_suffix_c
a_base_suffix_v = q(AA, lastrad) -- 'af-āl-a, inf-āl-a
a_base_suffix_c = q(A, lastrad) -- 'af-al-tu, inf-al-tu
i_base_suffix_v = q(II, lastrad) -- 'uf-īl-a, unf-īl-a
i_base_suffix_c = q(I, lastrad) -- 'uf-il-tu, unf-il-tu
-- past and non-past stems, active and passive, for vowel-initial and
-- consonant-initial endings
local past_v_stem = q(past_stem_base, a_base_suffix_v)
local past_c_stem = q(past_stem_base, a_base_suffix_c)
-- yu-f-īl-u, ya-staf-īl-u but yanf-āl-u, yaft-āl-u
local nonpast_v_stem = q(nonpast_stem_base, form410 and i_base_suffix_v or a_base_suffix_v)
local nonpast_c_stem = q(nonpast_stem_base, form410 and i_base_suffix_c or a_base_suffix_c)
local past_pass_v_stem = q(past_pass_stem_base, i_base_suffix_v)
local past_pass_c_stem = q(past_pass_stem_base, i_base_suffix_c)
local nonpast_pass_v_stem = q(nonpast_stem_base, a_base_suffix_v)
local nonpast_pass_c_stem = q(nonpast_stem_base, a_base_suffix_c)
-- imperative stem
local imp_v_stem = q(past_stem_base, form410 and i_base_suffix_v or a_base_suffix_v)
local imp_c_stem = q(past_stem_base, form410 and i_base_suffix_c or a_base_suffix_c)
-- make parts
make_hollow_geminate_verb(base, false, past_v_stem, past_c_stem, past_pass_v_stem,
past_pass_c_stem, nonpast_v_stem, nonpast_c_stem, nonpast_pass_v_stem,
nonpast_pass_c_stem, imp_v_stem, imp_c_stem, prefix_vowel)
-- active participle
insert_form_or_forms(base, "ap", q(MU, nonpast_v_stem))
-- passive participle
insert_form_or_forms(base, "pp", q(MU, nonpast_pass_v_stem))
end
-- Generate finite parts of an augmented (form II+) geminate verb, given:
-- * `base` (conjugation data structure);
-- * `vowel_spec` (radicals, weakness);
-- * `past_stem_base` (invariable part of active past stem; this and the stem bases below will end with a consonant
-- for forms IV, X, IVq, and a short vowel for the others);
-- * `nonpast_stem_base` (invariable part of nonpast stem);
-- * `past_pass_stem_base` (invariable part of passive past stem);
-- * `vn` (verbal noun);
-- * `altgem_note` (footnote to add to active past 1/2-person forms, when alternative forms are supplied [form X]).
local function make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base,
past_pass_stem_base, vn, altgem_note)
insert_form_or_forms(base, "vn", vn)
local vform = base.verb_form
local lastrad = base.quadlit and vowel_spec.rad4 or vowel_spec.rad3
local prefix_vowel = prefix_vowel_from_vform(vform)
local a_base_suffix_v, a_base_suffix_c
local i_base_suffix_v, i_base_suffix_c
if vform == "IV" or vform == "X" or vform == "IVq" then
a_base_suffix_v = q(A, lastrad, SH) -- 'af-all
a_base_suffix_c = q(SK, lastrad, A, lastrad) -- 'af-lal
i_base_suffix_v = q(I, lastrad, SH) -- yuf-ill
i_base_suffix_c = q(SK, lastrad, I, lastrad) -- yuf-lil
else
a_base_suffix_v = q(lastrad, SH) -- fā-ll, infa-ll
a_base_suffix_c = q(lastrad, A, lastrad) -- fā-lal, infa-lal
i_base_suffix_v = q(lastrad, SH) -- yufā-ll, yanfa-ll
i_base_suffix_c = q(lastrad, I, lastrad) -- yufā-lil, yanfa-lil
end
-- past and non-past stems, active and passive, for vowel-initial and
-- consonant-initial endings
local past_v_stem = q(past_stem_base, a_base_suffix_v)
local past_c_stem = q(past_stem_base, a_base_suffix_c)
local nonpast_v_stem = q(nonpast_stem_base, vform_nonpast_a_vowel(vform) and a_base_suffix_v or i_base_suffix_v)
local nonpast_c_stem = q(nonpast_stem_base, vform_nonpast_a_vowel(vform) and a_base_suffix_c or i_base_suffix_c)
-- NOTE: Formerly had a comment that "vform III and VI passive past do not have contracted parts, only
-- uncontracted parts, which are added separately by those functions". This is based on Mace
-- "Arabic Verbs and Essential Grammar" (1999) entry 63 (continued), which shows passive ḥūjija but no ḥūjja;
-- but that is apparently a mistake, as (1) verb tables in other books do show contracted passive parts for
-- these forms; (2) there is no mention of such an exception on p. 99, which explains how geminate ("doubled")
-- verbs work (on the contrary, it says "The contracted and uncontracted pairs (see above) are found all
-- over Forms III and VI of the doubled verbs").
local past_pass_v_stem = q(past_pass_stem_base, i_base_suffix_v)
local past_pass_c_stem = q(past_pass_stem_base, i_base_suffix_c)
local nonpast_pass_v_stem = q(nonpast_stem_base, a_base_suffix_v)
local nonpast_pass_c_stem = q(nonpast_stem_base, a_base_suffix_c)
-- imperative stem
local imp_v_stem = q(past_stem_base, vform_nonpast_a_vowel(vform) and a_base_suffix_v or i_base_suffix_v)
local imp_c_stem = q(past_stem_base, vform_nonpast_a_vowel(vform) and a_base_suffix_c or i_base_suffix_c)
-- make parts
make_hollow_geminate_verb(base, "geminate", past_v_stem, past_c_stem, past_pass_v_stem,
past_pass_c_stem, nonpast_v_stem, nonpast_c_stem, nonpast_pass_v_stem,
nonpast_pass_c_stem, imp_v_stem, imp_c_stem, prefix_vowel, altgem_note)
-- active participle
insert_form_or_forms(base, "ap", q(MU, nonpast_v_stem))
-- passive participle
insert_form_or_forms(base, "pp", q(MU, nonpast_pass_v_stem))
end
-------------------------------------------------------------------------------
-- Conjugation functions for specific conjugation types --
-------------------------------------------------------------------------------
local function form_i_imp_stem_through_rad1(base, nonpast_vowel, rad1)
local imp_vowel = map_vowel(nonpast_vowel, function(vow)
if vow == A or vow == I then
return I
elseif vow == U then
return U
elseif not skip_slot(base, "imp_2ms") then
error(("Internal error: Non-past vowel %s isn't a, i, or u, should have been caught earlier"):format(
dump(nonpast_vowel)))
else
-- Passive-only; imperative won't ever be displayed so it doesn't matter.
return I
end
end)
-- Mace ("Arabic Verbs and Essentials of Grammar" p. 63: [https://archive.org/details/arabicverbsessen00john/page/62/mode/2up])
-- claims that initial hamza is assimilated/elided into a long vowel in the form-I imperative, but apparently
-- this isn't corrrect.
local vowel_on_alif = map_vowel(imp_vowel, function(vow)
return ALIF .. vow
end)
return q(vowel_on_alif, rad1, SK)
end
-- Implement form-I sound or assimilated verb. ASSIMILATED is true for assimilated verbs.
local function make_form_i_sound_assimilated_verb(base, vowel_spec, assimilated)
local rad1, rad2, rad3, past_vowel, nonpast_vowel = get_radicals_3(vowel_spec)
-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied
-- past and non-past stems, active and passive
local past_stem = q(rad1, A, rad2, past_vowel, rad3)
local nonpast_stem = assimilated and q(rad2, nonpast_vowel, rad3) or
q(rad1, SK, rad2, nonpast_vowel, rad3)
local past_pass_stem = q(rad1, U, rad2, I, rad3)
local nonpast_pass_stem = q(rad1, SK, rad2, A, rad3)
-- imperative stem
-- check for irregular verb with reduced imperative (أَخَذَ or أَكَلَ or أَمَرَ)
local reducedimp = reduced_imperative_verb(rad1, rad2, rad3)
if reducedimp then
base.irregular = true
end
local imp_stem_suffix = q(rad2, nonpast_vowel, rad3)
local long_imp_stem_base = form_i_imp_stem_through_rad1(base, nonpast_vowel, rad1)
local short_imp_stem_base = ""
local imp_stem = q((assimilated or reducedimp) and "" or long_imp_stem_base, imp_stem_suffix)
-- make parts
make_sound_verb(base, past_stem, past_pass_stem, nonpast_stem, nonpast_pass_stem, imp_stem, "a")
if reducedimp == "shortlong" then
make_1stem_imperative(base, iut.combine_form_and_footnotes(q(long_imp_stem_base, imp_stem_suffix),
mw.getCurrentFrame():preprocess("[used especially with a clitic such as {{m|ar|فَ}} or {{m|ar|وَ}}]")))
end
-- Check for irregular verb سَأَلَ with alternative jussive and imperative. Calling this after make_sound_verb()
-- adds additional entries to the paradigm parts.
if saal_radicals(rad1, rad2, rad3) then
base.irregular = true
nonpast_1stem_conj(base, "juss", "a", "سَل")
nonpast_1stem_conj(base, "juss_pass", "u", "سَل")
make_1stem_imperative(base, "سَل")
end
-- Active participle.
insert_form_or_forms(base, "ap1", q(rad1, AA, rad2, I, rad3))
-- Insert alternative active participle (stative type I) فَعِيل. Since not all verbs have this, we require that
-- verbs that do have it specify it explicitly; a shortcut ++ is provided to make this easier (e.g. <ap:++> to
-- indicate that the alternative form should be used for the active participle, <ap:+,++> to indicate that both
-- forms can be used, and <ap:-> to indicate that there is no active participle). The same form is used for
-- secondary default passive participle.
insert_ap2_pp2(base, q(rad1, A, rad2, II, rad3))
-- Active participle, stative type II فَعِل (+++).
insert_form_or_forms(base, "ap3", q(rad1, A, rad2, I, rad3))
-- Active participle, color/defect أَفْعَل (+cd).
insert_form_or_forms(base, "apcd", q(HAMZA, A, rad1, SK, rad2, A, rad3))
-- Active participle, -ān فَعْلَان (+an).
insert_form_or_forms(base, "apan", q(rad1, A, rad2, SK, rad3, AAN))
-- Passive participle.
insert_form_or_forms(base, "pp", q(MA, rad1, SK, rad2, UU, rad3))
end
conjugations["I-sound"] = function(base, vowel_spec)
make_form_i_sound_assimilated_verb(base, vowel_spec, false)
end
conjugations["none-sound"] = function(base, vowel_spec)
-- All default stems are nil.
make_sound_verb(base)
end
conjugations["none-hollow"] = function(base, vowel_spec)
-- All default stems are nil.
make_hollow_geminate_verb(base, false)
end
conjugations["none-geminate"] = function(base, vowel_spec)
-- All default stems are nil.
make_hollow_geminate_verb(base, "geminate")
end
conjugations["none-final-weak"] = function(base, vowel_spec)
-- All default stems are nil.
make_final_weak_verb(base)
end
conjugations["I-assimilated"] = function(base, vowel_spec)
make_form_i_sound_assimilated_verb(base, vowel_spec, "assimilated")
end
local function make_form_i_hayy_verb(base, vowel_spec)
-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied
base.irregular = true
-- past and non-past stems, active and passive, and imperative stem
local past_c_stem = "حَيِي"
local past_v_stem_long = past_c_stem
local past_v_stem_short = "حَيّ"
local past_pass_c_stem = "حُيِي"
local past_pass_v_stem_long = past_pass_c_stem
local past_pass_v_stem_short = "حُيّ"
local nonpast_stem = "حْي"
local nonpast_pass_stem = nonpast_stem
local imp_stem = _I .. nonpast_stem
-- make parts
past_2stem_conj(base, "past", {}, past_c_stem)
past_2stem_conj(base, "past_pass", {}, past_pass_c_stem)
local variant = vowel_spec.variant or "both"
if variant == "short" or variant == "both" then
past_2stem_conj(base, "past", past_v_stem_short, {})
past_2stem_conj(base, "past_pass", past_pass_v_stem_short, {})
end
local function inflect_long_variant(tense, long_stem, short_stem)
inflect_tense_1(base, tense, "",
{long_stem, long_stem, long_stem, long_stem, short_stem},
{past_endings[4], past_endings[5], past_endings[8], past_endings[9],
past_endings[13]},
{"3ms", "3fs", "3md", "3fd", "3mp"})
end
if variant == "long" or variant == "both" then
inflect_long_variant("past", past_v_stem_long, past_v_stem_short)
inflect_long_variant("past_pass", past_pass_v_stem_long, past_pass_v_stem_short)
end
nonpast_1stem_conj(base, "ind", "a", nonpast_stem, ind_endings_aa)
nonpast_1stem_conj(base, "sub", "a", nonpast_stem, sub_endings_aa)
nonpast_1stem_conj(base, "juss", "a", nonpast_stem, juss_endings_aa)
nonpast_1stem_conj(base, "ind_pass", "u", nonpast_pass_stem, ind_endings_aa)
nonpast_1stem_conj(base, "sub_pass", "u", nonpast_pass_stem, sub_endings_aa)
nonpast_1stem_conj(base, "juss_pass", "u", nonpast_pass_stem, juss_endings_aa)
inflect_tense_imp(base, {imp_stem, all_same = 1}, imp_endings_aa)
-- active and passive participles apparently do not exist for this verb
end
-- Implement form-I final-weak assimilated+final-weak verb. ASSIMILATED is true for assimilated verbs.
local function make_form_i_final_weak_verb(base, vowel_spec, assimilated)
local rad1, rad2, rad3, past_vowel, nonpast_vowel = get_radicals_3(vowel_spec)
-- حَيَّ or حَيِيَ is weird enough that we handle it as a separate function.
if hayy_radicals(rad1, rad2, rad3) then
make_form_i_hayy_verb(base, vowel_spec)
return
end
-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied.
-- Past and non-past stems, active and passive, and imperative stem.
local past_stem = q(rad1, A, rad2)
local past_pass_stem = q(rad1, U, rad2)
local nonpast_stem, nonpast_pass_stem, imp_stem
if raa_radicals(rad1, rad2, rad3) then
base.irregular = true
nonpast_stem = rad1
nonpast_pass_stem = rad1
imp_stem = rad1
else
nonpast_pass_stem = q(rad1, SK, rad2)
if assimilated then
nonpast_stem = rad2
imp_stem = rad2
else
nonpast_stem = nonpast_pass_stem
imp_stem = q(form_i_imp_stem_through_rad1(base, nonpast_vowel, rad1), rad2)
end
end
-- Make parts.
local past_ending_vowel =
req(rad3, Y) and req(past_vowel, A) and "ay" or
req(rad3, W) and req(past_vowel, A) and "aw" or
req(past_vowel, I) and "ī" or "ū"
-- Try to preserve footnotes attached to the third radical and/or past and/or non-past vowels.
local past_footnotes = iut.combine_footnotes(rget_footnotes(rad3), rget_footnotes(past_vowel))
local nonpast_ending_vowel = req(nonpast_vowel, A) and "ā" or req(nonpast_vowel, I) and "ī" or "ū"
local nonpast_footnotes = iut.combine_footnotes(rget_footnotes(rad3), rget_footnotes(nonpast_vowel))
make_final_weak_verb(base,
iut.combine_form_and_footnotes(past_stem, past_footnotes),
iut.combine_form_and_footnotes(past_pass_stem, past_footnotes),
iut.combine_form_and_footnotes(nonpast_stem, nonpast_footnotes),
iut.combine_form_and_footnotes(nonpast_pass_stem, nonpast_footnotes),
iut.combine_form_and_footnotes(imp_stem, nonpast_footnotes),
past_ending_vowel, nonpast_ending_vowel, "a")
-- Active participle.
insert_form_or_forms(base, "ap1", q(rad1, AA, rad2, IN))
-- Active participle, stative type I فَعِيّ (++). FIXME: Is this correct when rad3 is W?
insert_ap2_pp2(base, q(rad1, A, rad2, II, SH))
-- Active participle, stative type II فَعٍ (+++). FIXME: Any examples of this to verify it's correct?
insert_form_or_forms(base, "ap3", q(rad1, A, rad2, IN))
-- Active participle, color/defect أَفْعَى (+cd).
insert_form_or_forms(base, "apcd", q(HAMZA, A, rad1, SK, rad2, AAMAQ))
-- Active participle, -ān فَعْيَان or فَعْوَان (+an).
-- FIXME: Any examples of this for both rad3 = W and y to verify it's correct?
insert_form_or_forms(base, "apan", q(rad1, A, rad2, SK, rad3, AAN))
-- Passive participle.
insert_form_or_forms(base, "pp", q(MA, rad1, SK, rad2, req(rad3, Y) and II or UU, SH))
end
conjugations["I-final-weak"] = function(base, vowel_spec)
make_form_i_final_weak_verb(base, vowel_spec, false)
end
conjugations["I-assimilated+final-weak"] = function(base, vowel_spec)
make_form_i_final_weak_verb(base, vowel_spec, "assimilated")
end
conjugations["I-hollow"] = function(base, vowel_spec)
local rad1, rad2, rad3, past_vowel, nonpast_vowel = get_radicals_3(vowel_spec)
-- In some sense, hollow vowels i~i and u~u are more "correct" than a~i and a~u, but the latter follow the
-- pattern of other form-I verbs, so we map i~i to a~i and u~u to a~u in infer_radicals(). Now however we have
-- to undo this to get the actual past vowel based on the non-past vowel.
if req(past_vowel, A) then
past_vowel = map_vowel(past_vowel, function(vow)
return req(nonpast_vowel, A) and I or rget(nonpast_vowel)
end)
end
local lengthened_nonpast = map_vowel(nonpast_vowel, function(vow)
return vow == U and UU or vow == I and II or AA
end)
-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied.
-- active past stems - vowel (v) and consonant (c)
local past_v_stem = q(rad1, AA, rad3)
local past_c_stem = q(rad1, past_vowel, rad3)
-- active non-past stems - vowel (v) and consonant (c)
local nonpast_v_stem = q(rad1, lengthened_nonpast, rad3)
local nonpast_c_stem = q(rad1, nonpast_vowel, rad3)
-- passive past stems - vowel (v) and consonant (c)
-- 'ufīla, 'ufiltu
local past_pass_v_stem = q(rad1, II, rad3)
local past_pass_c_stem = q(rad1, I, rad3)
-- passive non-past stems - vowel (v) and consonant (c)
-- yufāla/yufalna
-- stem is built differently but conjugation is identical to sound verbs
local nonpast_pass_v_stem = q(rad1, AA, rad3)
local nonpast_pass_c_stem = q(rad1, A, rad3)
-- imperative stem
local imp_v_stem = nonpast_v_stem
local imp_c_stem = nonpast_c_stem
-- make parts
make_hollow_geminate_verb(base, false, past_v_stem, past_c_stem, past_pass_v_stem,
past_pass_c_stem, nonpast_v_stem, nonpast_c_stem, nonpast_pass_v_stem,
nonpast_pass_c_stem, imp_v_stem, imp_c_stem, "a")
if kaan_radicals(rad1, rad2, rad3) then
local endings = make_nonpast_endings(U, {}, {}, {}, {})
inflect_tense(base, "juss", nonpast_prefix_consonants, q(A, rad1), endings)
base.irregular = true
end
-- Active participle.
insert_form_or_forms(base, "ap1", req(rad3, HAMZA) and q(rad1, AA, HAMZA, IN) or
q(rad1, AA, HAMZA, I, rad3))
-- Active participle, stative type I فَيِّد (++). FIXME: Any examples of this to verify it's correct?
insert_ap2_pp2(base, q(rad1, A, Y, SH, I, rad3))
-- Active participle, stative type II فَيِد (+++). FIXME: Any examples of this to verify it's correct?
insert_form_or_forms(base, "ap3", q(rad1, A, Y, I, rad3))
-- Active participle, color/defect أَفّيَد or أَفّوَد (+cd). FIXME: Any examples of this to verify it's correct?
insert_form_or_forms(base, "apcd", q(HAMZA, A, rad1, SK, rad2, A, rad3))
-- Active participle, -ān فَيْدَان or فَوْدَان (+an). Example: جَاعَ "to be hungry", act part جَوْعَان
insert_form_or_forms(base, "apan", q(rad1, A, rad2, SK, rad3, AAN))
-- Passive participle.
insert_form_or_forms(base, "pp", q(MA, rad1, req(rad2, Y) and II or UU, rad3))
end
conjugations["I-geminate"] = function(base, vowel_spec)
local rad1, rad2, rad3, past_vowel, nonpast_vowel = get_radicals_3(vowel_spec)
-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied.
-- active past stems - vowel (v) and consonant (c)
local past_v_stem = q(rad1, A, rad2, SH)
local past_c_stem = q(rad1, A, rad2, past_vowel, rad2)
-- active non-past stems - vowel (v) and consonant (c)
local nonpast_v_stem = q(rad1, nonpast_vowel, rad2, SH)
local nonpast_c_stem = q(rad1, SK, rad2, nonpast_vowel, rad2)
-- passive past stems - vowel (v) and consonant (c)
-- dulla/dulilta
local past_pass_v_stem = q(rad1, U, rad2, SH)
local past_pass_c_stem = q(rad1, U, rad2, I, rad2)
-- passive non-past stems - vowel (v) and consonant (c)
--yudallu/yudlalna
-- stem is built differently but conjugation is identical to sound verbs
local nonpast_pass_v_stem = q(rad1, A, rad2, SH)
local nonpast_pass_c_stem = q(rad1, SK, rad2, A, rad2)
-- imperative stem
local imp_v_stem = q(rad1, nonpast_vowel, rad2, SH)
local imp_c_stem = q(form_i_imp_stem_through_rad1(base, nonpast_vowel, rad1), rad2, nonpast_vowel, rad2)
-- make parts
make_hollow_geminate_verb(base, "geminate", past_v_stem, past_c_stem, past_pass_v_stem,
past_pass_c_stem, nonpast_v_stem, nonpast_c_stem, nonpast_pass_v_stem,
nonpast_pass_c_stem, imp_v_stem, imp_c_stem, "a")
-- Active participle.
insert_form_or_forms(base, "ap1", q(rad1, AA, rad2, SH))
-- Active participle, stative type I فَعِيع (++). FIXME: Any examples of this to verify it's correct?
insert_ap2_pp2(base, q(rad1, A, rad2, II, rad2))
-- Active participle, stative type II فَعّ (+++). Example: بَرَّ "to be pious", active participle بَرّ
insert_form_or_forms(base, "ap3", q(rad1, A, rad2, SH))
-- Active participle, color/defect أَفَعّ (+cd).
-- Example: لَصَّ "to be thievish, to steal repeatedly", active participle أَلَصّ.
insert_form_or_forms(base, "apcd", q(HAMZA, A, rad1, A, rad2, SH))
-- Active participle, -ān فَعَّان (+an). FIXME: Any examples of this to verify it's correct?
insert_form_or_forms(base, "apan", q(rad1, A, rad2, SH, AAN))
-- Passive participle.
insert_form_or_forms(base, "pp", q(MA, rad1, SK, rad2, UU, rad2))
end
-- Return the ta- (active, past and non-past) and tu- (passive past) prefixes for a form II/III/V/VI verb.
-- Form V and VI verbs normally use ta- and tu-, but reduced (base.reduced) verbs use different prefixes. Form II
-- and III verbs have no prefix.
local function form_ii_iii_v_vi_ta_tu_prefix(base, rad1)
local vform = base.verb_form
if vform == "V" or vform == "VI" then
if base.reduced then
-- To simplify the code, we generate two rad1's with a sukūn between them, which is cleaned up in
-- postprocessing.
return q(_I, rad1, SK), q(rad1, SK), q(_U, rad1, SK)
else
return TA, TA, TU
end
else
return "", "", ""
end
end
-- Make form II or V sound or final-weak verb.
local function make_form_ii_v_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local final_weak = is_final_weak(base, vowel_spec)
local vform = base.verb_form
local ta_past_prefix, ta_nonpast_prefix, tu_past_prefix = form_ii_iii_v_vi_ta_tu_prefix(base, rad1)
local vn = vform == "V" and
q(ta_past_prefix, rad1, A, rad2, SH, final_weak and IN or q(U, rad3)) or
q(TA, rad1, SK, rad2, II, final_weak and AH or rad3)
-- various stem bases
local past_stem_base = q(ta_past_prefix, rad1, A, rad2, SH)
local nonpast_stem_base = q(ta_nonpast_prefix, rad1, A, rad2, SH)
local past_pass_stem_base = q(tu_past_prefix, rad1, U, rad2, SH)
-- make parts
make_augmented_sound_final_weak_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base,
vn)
end
conjugations["II-sound"] = function(base, vowel_spec)
make_form_ii_v_sound_final_weak_verb(base, vowel_spec)
end
conjugations["II-final-weak"] = function(base, vowel_spec)
make_form_ii_v_sound_final_weak_verb(base, vowel_spec)
end
local function make_form_iii_alt_vn(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local final_weak = is_final_weak(base, vowel_spec)
-- Insert alternative verbal noun فِعَال. Since not all verbs have this, we require that verbs that do have it
-- specify it explicitly; a shortcut ++ is provided to make this easier (e.g. <vn:+,++> to indicate that
-- both the normal verbal noun مُفَاعَلَة and secondary verbal noun فِعَال are available).
insert_form_or_forms(base, "vn2", q(rad1, I, rad2, AA, final_weak and HAMZA or rad3))
end
-- Make form III or VI sound or final-weak verb.
local function make_form_iii_vi_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local final_weak = is_final_weak(base, vowel_spec)
local vform = base.verb_form
local ta_past_prefix, ta_nonpast_prefix, tu_past_prefix = form_ii_iii_v_vi_ta_tu_prefix(base, rad1)
local vn = vform == "VI" and
q(ta_past_prefix, rad1, AA, rad2, final_weak and IN or q(U, rad3)) or
q(MU, rad1, AA, rad2, final_weak and AAH or q(A, rad3, AH))
-- various stem bases
local past_stem_base = q(ta_past_prefix, rad1, AA, rad2)
local nonpast_stem_base = q(ta_nonpast_prefix, rad1, AA, rad2)
local past_pass_stem_base = q(tu_past_prefix, rad1, UU, rad2)
-- make parts
make_augmented_sound_final_weak_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base,
vn)
if vform == "III" then
make_form_iii_alt_vn(base, vowel_spec)
end
end
conjugations["III-sound"] = function(base, vowel_spec)
make_form_iii_vi_sound_final_weak_verb(base, vowel_spec)
end
conjugations["III-final-weak"] = function(base, vowel_spec)
make_form_iii_vi_sound_final_weak_verb(base, vowel_spec)
end
-- Make form III or VI geminate verb.
local function make_form_iii_vi_geminate_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vform = base.verb_form
local ta_past_prefix, ta_nonpast_prefix, tu_past_prefix = form_ii_iii_v_vi_ta_tu_prefix(base, rad1)
-- Alternative verbal noun فِعَال will be inserted when we add sound parts below.
local vn = vform == "VI" and q(ta_past_prefix, rad1, AA, rad2, SH) or q(MU, rad1, AA, rad2, SH, AH)
-- Various stem bases.
local past_stem_base = q(ta_past_prefix, rad1, AA)
local nonpast_stem_base = q(ta_nonpast_prefix, rad1, AA)
local past_pass_stem_base = q(tu_past_prefix, rad1, UU)
-- Make parts.
local variant = vowel_spec.variant or "short"
if variant == "short" or variant == "both" then
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
-- Also add alternative sound (non-compressed) parts. This will lead to some duplicate entries, but they are
-- removed during addition.
if variant == "long" or variant == "both" then
make_form_iii_vi_sound_final_weak_verb(base, vowel_spec)
elseif vform == "III" then
-- Still need to add the alternative form-III verbal noun.
make_form_iii_alt_vn(base, vowel_spec)
end
end
conjugations["III-geminate"] = function(base, vowel_spec)
make_form_iii_vi_geminate_verb(base, vowel_spec)
end
-- Make form IV sound or final-weak verb.
local function make_form_iv_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local final_weak = is_final_weak(base, vowel_spec)
-- core of stem base, minus stem prefixes
local stem_core
-- check for irregular verb أَرَى
local is_raa = raa_radicals(rad1, rad2, rad3)
if is_raa then
base.irregular = true
stem_core = rad1
else
stem_core = q(rad1, SK, rad2)
end
-- verbal noun
local vn = is_raa and
q(HAMZA, I, stem_core, AA, HAMZA, AH) or
q(HAMZA, I, stem_core, AA, final_weak and HAMZA or rad3)
-- various stem bases
local past_stem_base = q(HAMZA, A, stem_core)
local nonpast_stem_base = stem_core
local past_pass_stem_base = q(HAMZA, U, stem_core)
-- make parts
make_augmented_sound_final_weak_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base,
vn)
end
conjugations["IV-sound"] = function(base, vowel_spec)
make_form_iv_sound_final_weak_verb(base, vowel_spec)
end
conjugations["IV-final-weak"] = function(base, vowel_spec)
make_form_iv_sound_final_weak_verb(base, vowel_spec)
end
conjugations["IV-hollow"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
-- verbal noun
local vn = q(HAMZA, I, rad1, AA, rad3, AH)
-- various stem bases
local past_stem_base = q(HAMZA, A, rad1)
local nonpast_stem_base = rad1
local past_pass_stem_base = q(HAMZA, U, rad1)
-- make parts
make_augmented_hollow_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["IV-geminate"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = q(HAMZA, I, rad1, SK, rad2, AA, rad2)
-- various stem bases
local past_stem_base = q(HAMZA, A, rad1)
local nonpast_stem_base = rad1
local past_pass_stem_base = q(HAMZA, U, rad1)
-- make parts
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["V-sound"] = function(base, vowel_spec)
make_form_ii_v_sound_final_weak_verb(base, vowel_spec)
end
conjugations["V-final-weak"] = function(base, vowel_spec)
make_form_ii_v_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VI-sound"] = function(base, vowel_spec)
make_form_iii_vi_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VI-final-weak"] = function(base, vowel_spec)
make_form_iii_vi_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VI-geminate"] = function(base, vowel_spec)
make_form_iii_vi_geminate_verb(base, vowel_spec)
end
-- Make a verbal noun of the general form that applies to forms VII and above. RAD12 is the first consonant cluster
-- (after initial اِ) and RAD34 is the second consonant cluster. RAD5 is the final consonant.
local function high_form_verbal_noun(rad12, rad34, rad5)
return q(_I, rad12, I, rad34, AA, rad5)
end
-- Populate a sound or final-weak verb for any of the various high-numbered augmented forms (form VII and up) that
-- have up to 5 consonants in two clusters in the stem and the same pattern of vowels between. Some of these
-- consonants in certain verb parts are w's, which leads to apparent anomalies in certain stems of these parts, but
-- these anomalies are handled automatically in postprocessing, where we resolve sequences of iwC -> īC, uwC -> ūC,
-- w + sukūn + w -> w + shadda.
-- RAD12 is the first consonant cluster (after initial اِ) and RAD34 is the second consonant cluster. RAD5 is the
-- final consonant.
local function make_high_form_sound_final_weak_verb(base, vowel_spec, rad12, rad34, rad5)
local final_weak = is_final_weak(base, vowel_spec)
local vn = high_form_verbal_noun(rad12, rad34, final_weak and HAMZA or rad5)
-- various stem bases
local nonpast_stem_base = q(rad12, A, rad34)
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, rad12, U, rad34)
-- make parts
make_augmented_sound_final_weak_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base,
vn)
end
local function form_vii_nrad1(base, rad1)
if base.reduced then
if not req(rad1, M) then
error(("Internal error: Form VII first radical %s is not م but .reduced specified; should have been caught earlier"):
format(rget(rad1)))
end
return M .. SH
else
return q("نْ", rad1)
end
end
-- Make form VII sound or final-weak verb.
local function make_form_vii_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
make_high_form_sound_final_weak_verb(base, vowel_spec, form_vii_nrad1(base, rad1), rad2, rad3)
end
conjugations["VII-sound"] = function(base, vowel_spec)
make_form_vii_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VII-final-weak"] = function(base, vowel_spec)
make_form_vii_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VII-hollow"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local nrad1 = form_vii_nrad1(base, rad1)
local vn = high_form_verbal_noun(nrad1, Y, rad3)
-- various stem bases
local nonpast_stem_base = nrad1
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, nrad1)
-- make parts
make_augmented_hollow_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["VII-geminate"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local nrad1 = form_vii_nrad1(base, rad1)
local vn = high_form_verbal_noun(nrad1, rad2, rad2)
-- various stem bases
local nonpast_stem_base = q(nrad1, A)
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, nrad1, U)
-- make parts
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
-- Return Form VIII verbal noun.
local function form_viii_verbal_noun(base, vowel_spec, rad1, rad2, rad3)
local final_weak = is_final_weak(base, vowel_spec)
rad3 = final_weak and HAMZA or rad3
return {high_form_verbal_noun(vowel_spec.form_viii_assim, rad2, rad3)}
end
-- Make form VIII sound or final-weak verb.
local function make_form_viii_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
-- check for irregular verb اِتَّخَذَ
if axadh_radicals(rad1, rad2, rad3) then
base.irregular = true
rad1 = T
end
make_high_form_sound_final_weak_verb(base, vowel_spec, vowel_spec.form_viii_assim, rad2, rad3)
end
conjugations["VIII-sound"] = function(base, vowel_spec)
make_form_viii_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VIII-final-weak"] = function(base, vowel_spec)
make_form_viii_sound_final_weak_verb(base, vowel_spec)
end
conjugations["VIII-hollow"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = form_viii_verbal_noun(base, vowel_spec, rad1, Y, rad3)
-- various stem bases
local nonpast_stem_base = vowel_spec.form_viii_assim
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, nonpast_stem_base)
-- make parts
make_augmented_hollow_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["VIII-geminate"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = form_viii_verbal_noun(base, vowel_spec, rad1, rad2, rad2)
-- various stem bases
local nonpast_stem_base = q(vowel_spec.form_viii_assim, A)
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, vowel_spec.form_viii_assim, U)
-- make parts
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["IX-sound"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = q(_I, rad1, SK, rad2, I, rad3, AA, rad3)
-- various stem bases
local nonpast_stem_base = q(rad1, SK, rad2, A)
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, rad1, SK, rad2, U)
-- make parts
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["IX-final-weak"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
make_high_form_sound_final_weak_verb(base, vowel_spec, q(rad1, SK, rad2), rad3, rad3)
end
-- Populate a sound or final-weak verb for any of the various high-numbered
-- augmented forms that have 5 consonants in the stem and the same pattern of
-- vowels. Some of these consonants in certain verb parts are w's, which leads to
-- apparent anomalies in certain stems of these parts, but these anomalies
-- are handled automatically in postprocessing, where we resolve sequences of
-- iwC -> īC, uwC -> ūC, w + sukūn + w -> w + shadda.
local function make_high5_form_sound_final_weak_verb(base, vowel_spec, rad1, rad2, rad3, rad4, rad5)
make_high_form_sound_final_weak_verb(base, vowel_spec, q(rad1, SK, rad2), q(rad3, SK, rad4), rad5)
end
-- Make form X sound or final-weak verb.
local function make_form_x_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
-- check for irregular verb اِسْتَحْيَا (also اِسْتَحَى)
local is_hayy = hayy_radicals(rad1, rad2, rad3)
local variant = vowel_spec.variant or "both"
if not is_hayy or variant == "long" or variant == "both" then
make_high5_form_sound_final_weak_verb(base, vowel_spec, S, T, rad1, rad2, rad3)
end
if is_hayy and (variant == "short" or variant == "both") then
base.irregular = true
-- Add alternative entries to the verbal paradigms. Any duplicates are removed during addition.
make_high_form_sound_final_weak_verb(base, vowel_spec, S .. SK .. T, rad1, rad3)
end
end
conjugations["X-sound"] = function(base, vowel_spec)
make_form_x_sound_final_weak_verb(base, vowel_spec)
end
conjugations["X-final-weak"] = function(base, vowel_spec)
make_form_x_sound_final_weak_verb(base, vowel_spec)
end
conjugations["X-hollow"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = q(base.reduced and "اِسْ" or "اِسْتِ", rad1, AA, rad3, AH)
-- various stem bases
local past_stem_base = q(base.reduced and "اِسْ" or "اِسْتَ", rad1)
local nonpast_stem_base = q(base.reduced and "سْ" or "سْتَ", rad1)
local past_pass_stem_base = q(base.reduced and "اُسْ" or "اُسْتُ", rad1)
-- make parts
make_augmented_hollow_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
conjugations["X-geminate"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = q("اِسْتِ", rad1, SK, rad2, AA, rad2)
-- various stem bases
local past_stem_base = q("اِسْتَ", rad1)
local nonpast_stem_base = q("سْتَ", rad1)
local past_pass_stem_base = q("اُسْتُ", rad1)
-- make parts
if base.altgem then
inflect_tense(base, "past", "", {q(past_stem_base, A, rad2, SH), all_same = 1},
past_endings_ay_12_person_only)
end
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn,
base.altgem and "[uncommon]" or nil)
end
conjugations["XI-sound"] = function(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local vn = q(_I, rad1, SK, rad2, II, rad3, AA, rad3)
-- various stem bases
local nonpast_stem_base = q(rad1, SK, rad2, AA)
local past_stem_base = q(_I, nonpast_stem_base)
local past_pass_stem_base = q(_U, rad1, SK, rad2, UU)
-- make parts
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
-- Probably no form XI final-weak, since already geminate in form; would behave as XI-sound.
-- Make form XII sound or final-weak verb.
local function make_form_xii_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
make_high5_form_sound_final_weak_verb(base, vowel_spec, rad1, rad2, W, rad2, rad3)
end
conjugations["XII-sound"] = function(base, vowel_spec)
make_form_xii_sound_final_weak_verb(base, vowel_spec)
end
conjugations["XII-final-weak"] = function(base, vowel_spec)
make_form_xii_sound_final_weak_verb(base, vowel_spec)
end
-- Make form XIII sound or final-weak verb.
local function make_form_xiii_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
make_high5_form_sound_final_weak_verb(base, vowel_spec, rad1, rad2, W, W, rad3)
end
conjugations["XIII-sound"] = function(base, vowel_spec)
make_form_xiii_sound_final_weak_verb(base, vowel_spec)
end
conjugations["XIII-final-weak"] = function(base, vowel_spec)
make_form_xiii_sound_final_weak_verb(base, vowel_spec)
end
-- Make a form XIV or XV sound or final-weak verb. Last radical appears twice (if`anlala / yaf`anlilu) so if it were
-- w or y you'd get if`anwā / yaf`anwī or if`anyā / yaf`anyī, i.e. unlike for most augmented verbs, the identity of
-- the radical matters.
local function make_form_xiv_xv_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3 = get_radicals_3(vowel_spec)
local lastrad = base.verb_form == "XV" and Y or rad3
make_high5_form_sound_final_weak_verb(base, vowel_spec, rad1, rad2, N, rad3, lastrad)
end
conjugations["XIV-sound"] = function(base, vowel_spec)
make_form_xiv_xv_sound_final_weak_verb(base, vowel_spec)
end
conjugations["XIV-final-weak"] = function(base, vowel_spec)
make_form_xiv_xv_sound_final_weak_verb(base, vowel_spec)
end
conjugations["XV-sound"] = function(base, vowel_spec)
make_form_xiv_xv_sound_final_weak_verb(base, vowel_spec)
end
-- Probably no form XV final-weak, since already final-weak in form; would behave as XV-sound.
-- Make form Iq or IIq sound or final-weak verb.
local function make_form_iq_iiq_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3, rad4 = get_radicals_4(vowel_spec)
local final_weak = is_final_weak(base, vowel_spec)
local vform = base.verb_form
local vn = vform == "IIq" and
q(TA, rad1, A, rad2, SK, rad3, (final_weak and IN or q(U, rad4))) or
q(rad1, A, rad2, SK, rad3, (final_weak and AAH or q(A, rad4, AH)))
local ta_pref = vform == "IIq" and TA or ""
local tu_pref = vform == "IIq" and TU or ""
-- various stem bases
local past_stem_base = q(ta_pref, rad1, A, rad2, SK, rad3)
local nonpast_stem_base = past_stem_base
local past_pass_stem_base = q(tu_pref, rad1, U, rad2, SK, rad3)
-- make parts
make_augmented_sound_final_weak_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base,
vn)
end
conjugations["Iq-sound"] = function(base, vowel_spec)
make_form_iq_iiq_sound_final_weak_verb(base, vowel_spec)
end
conjugations["Iq-final-weak"] = function(base, vowel_spec)
make_form_iq_iiq_sound_final_weak_verb(base, vowel_spec)
end
conjugations["IIq-sound"] = function(base, vowel_spec)
make_form_iq_iiq_sound_final_weak_verb(base, vowel_spec)
end
conjugations["IIq-final-weak"] = function(base, vowel_spec)
make_form_iq_iiq_sound_final_weak_verb(base, vowel_spec)
end
-- Make form IIIq sound or final-weak verb.
local function make_form_iiiq_sound_final_weak_verb(base, vowel_spec)
local rad1, rad2, rad3, rad4 = get_radicals_4(vowel_spec)
make_high5_form_sound_final_weak_verb(base, vowel_spec, rad1, rad2, N, rad3, rad4)
end
conjugations["IIIq-sound"] = function(base, vowel_spec)
make_form_iiiq_sound_final_weak_verb(base, vowel_spec)
end
conjugations["IIIq-final-weak"] = function(base, vowel_spec)
make_form_iiiq_sound_final_weak_verb(base, vowel_spec)
end
conjugations["IVq-sound"] = function(base, vowel_spec)
local rad1, rad2, rad3, rad4 = get_radicals_4(vowel_spec)
local vn = q(_I, rad1, SK, rad2, I, rad3, SK, rad4, AA, rad4)
-- various stem bases
local past_stem_base = q(_I, rad1, SK, rad2, A, rad3)
local nonpast_stem_base = q(rad1, SK, rad2, A, rad3)
local past_pass_stem_base = q(_U, rad1, SK, rad2, U, rad3)
-- make parts
make_augmented_geminate_verb(base, vowel_spec, past_stem_base, nonpast_stem_base, past_pass_stem_base, vn)
end
-- Probably no form IVq final-weak, since already geminate in form; would behave as IVq-sound.
end
create_conjugations()
-------------------------------------------------------------------------------
-- Guts of main conjugation function --
-------------------------------------------------------------------------------
-- Given form, weakness and radicals, check to make sure the radicals present are allowable for the weakness. Hamzas on
-- alif/wāw/yāʾ seats are never allowed (should always appear as hamza-on-the-line), and various weaknesses have various
-- strictures on allowable consonants.
local function check_radicals(form, weakness, rad1, rad2, rad3, rad4)
local function hamza_check(index, rad)
if rad == HAMZA_ON_ALIF or rad == HAMZA_UNDER_ALIF or
rad == HAMZA_ON_W or rad == HAMZA_ON_Y then
error("Radical " .. index .. " is " .. rad .. " but should be ء (hamza on the line)")
end
end
local function check_waw_ya(index, rad)
if not is_waw_ya(rad) then
error("Radical " .. index .. " is " .. rad .. " but should be و or ي")
end
end
local function check_not_waw_ya(index, rad)
if is_waw_ya(rad) then
error("In a sound verb, radical " .. index .. " should not be و or ي")
end
end
hamza_check(rad1)
hamza_check(rad2)
hamza_check(rad3)
hamza_check(rad4)
if weakness == "assimilated" or weakness == "assimilated+final-weak" then
if rad1 ~= W then
error("Radical 1 is " .. rad1 .. " but should be و")
end
-- don't check that non-assimilated form I verbs don't have wāw as their
-- first radical because some form-I verbs exist where a first-radical wāw
-- behaves as sound, e.g. wajuha yawjuhu "to be distinguished".
end
if weakness == "final-weak" or weakness == "assimilated+final-weak" then
if rad4 then
check_waw_ya(4, rad4)
else
check_waw_ya(3, rad3)
end
elseif vform_supports_final_weak(form) then
-- non-final-weak verbs cannot have weak final radical if there's a corresponding
-- final-weak verb category. I think this is safe. We may have problems with
-- ḥayya/ḥayiya yaḥyā if we treat it as a geminate verb.
if rad4 then
check_not_waw_ya(4, rad4)
else
check_not_waw_ya(3, rad3)
end
end
if weakness == "hollow" then
check_waw_ya(2, rad2)
-- don't check that non-hollow verbs in forms that support hollow verbs
-- don't have wāw or yāʾ as their second radical because some verbs exist
-- where a middle-radical wāw/yāʾ behaves as sound, e.g. form-VIII izdawaja
-- "to be in pairs".
end
if weakness == "geminate" then
if rad4 then
error("Internal error: No geminate quadrilaterals, should not be seen")
end
if rad2 ~= rad3 then
error("Weakness is geminate; radical 3 is " .. rad3 .. " but should be same as radical 2 " .. rad2)
end
elseif vform_supports_geminate(form) then
-- non-geminate verbs cannot have second and third radical same if there's
-- a corresponding geminate verb category. I think this is safe. We
-- don't fuss over double wāw or double yāʾ because this could legitimately
-- be a final-weak verb with middle wāw/yāʾ, treated as sound.
if rad4 then
error("Internal error: No quadrilaterals should support geminate verbs")
end
if rad2 == rad3 and not is_waw_ya(rad2) then
error("Weakness is '" .. weakness .. "'; radical 2 and 3 are same at " .. rad2 .. " but should not be; consider making weakness 'geminate'")
end
end
end
-- array of substitutions; each element is a 2-entry array FROM, TO; do it
-- this way so the concatenations only get evaluated once
local postprocess_subs = {
-- reorder short-vowel + shadda -> shadda + short-vowel for easier processing
{"(" .. AIU .. ")" .. SH, SH .. "%1"},
----------same letter separated by sukūn should instead use shadda---------
------------happens e.g. in kun-nā "we were".-----------------
{"(.)" .. SK .. "%1", "%1" .. SH},
---------------------------- assimilated verbs ----------------------------
-- iw, iy -> ī (assimilated verbs)
{I .. W .. SK, II},
{I .. Y .. SK, II},
-- uw, uy -> ū (assimilated verbs)
{U .. W .. SK, UU},
{U .. Y .. SK, UU},
-------------- final -yā uses tall alif not alif maqṣūra ------------------
{"(" .. Y .. SH .. "?" .. A .. ")" .. AMAQ, "%1" .. ALIF},
----------------------- handle hamza assimilation -------------------------
-- initial hamza + short-vowel + hamza + sukūn -> hamza + long vowel
{HAMZA .. A .. HAMZA .. SK, HAMZA .. A .. ALIF},
{HAMZA .. I .. HAMZA .. SK, HAMZA .. I .. Y},
{HAMZA .. U .. HAMZA .. SK, HAMZA .. U .. W}
}
local postprocess_tr_subs = {
{"ī([" .. vowels .. "y*])", "iy%1"},
{"ū([" .. vowels .. "w*])", "uw%1"},
{"(.)%*", "%1%1"}, -- implement shadda
---------------------------- assimilated verbs ----------------------------
-- iw, iy -> ī (assimilated verbs)
{"iw([^" .. vowels .. "w])", "ī%1"},
{"iy([^" .. vowels .. "y])", "ī%1"},
-- uw, uy -> ū (assimilated verbs)
{"uw([^" .. vowels .. "w])", "ū%1"},
{"uy([^" .. vowels .. "y])", "ū%1"},
----------------------- handle hamza assimilation -------------------------
-- initial hamza + short-vowel + hamza + sukūn -> hamza + long vowel
{"ʔaʔ(" .. NV .. ")", "ʔā%1"},
{"ʔiʔ(" .. NV .. ")", "ʔī%1"},
{"ʔuʔ(" .. NV .. ")", "ʔū%1"},
}
-- Post-process verb parts to eliminate phonological anomalies. Many of the changes, particularly the tricky ones,
-- involve converting hamza to have the proper seat. The rules for this are complicated and are documented on the
-- [[w:Hamza]] Wikipedia page. In some cases there are alternatives allowed, and we handle them below by returning
-- multiple possibilities.
--
-- `incidental_final`: a word-final hamza is final only because a weak third radical was dropped; see
-- ar_utilities.seat_hamza().
local function postprocess_term(term, incidental_final)
if term == "?" then
return "?"
end
-- Add BORDER at text boundaries.
term = BORDER .. term .. BORDER
-- Do the main post-processing, based on the pattern substitutions in postprocess_subs.
for _, sub in ipairs(postprocess_subs) do
term = rsub(term, sub[1], sub[2])
end
term = term:gsub(BORDER, "")
if not rfind(term, HAMZA) then
return term
end
local spellings = ar_utilities.seat_hamza((term:gsub(HAMZA, HAMZA_PH)), {incidental_final = incidental_final})
if #spellings == 1 then
return spellings[1].form
end
local forms = {}
for _, spelling in ipairs(spellings) do
table.insert(forms, spelling.form)
end
return forms
end
local function postprocess_translit(translit)
if translit == "?" then
return "?"
end
-- Add BORDER at text boundaries.
translit = BORDER .. translit .. BORDER
-- Do the main post-processing, based on the pattern substitutions in postprocess_tr_subs.
for _, sub in ipairs(postprocess_tr_subs) do
translit = rsub(translit, sub[1], sub[2])
end
translit = translit:gsub(BORDER, "")
return translit
end
local function postprocess_forms(base)
-- Final-weak verbs with hamza as second radical.
local incidental_final = false
for _, vowel_spec in ipairs(base.conj_vowels) do
if vowel_spec.weakness and rfind(vowel_spec.weakness, "final%-weak") and vowel_spec.rad2 and
req(vowel_spec.rad2, HAMZA) then
incidental_final = true
end
end
local converted_values = {}
for slot, forms in pairs(base.forms) do
local need_dedup = false
for i, form in ipairs(forms) do
local term = postprocess_term(form.form, incidental_final)
local translit = form.translit and postprocess_translit(form.translit) or nil
if term ~= form.form or translit ~= form.translit then
need_dedup = true
end
converted_values[i] = {term, translit}
end
if need_dedup then
local temp_dedup = {}
for i = 1, #forms do
local new_term, new_translit = unpack(converted_values[i])
if type(new_term) == "table" then
for _, nt in ipairs(new_term) do
local new_formobj = {
form = nt,
translit = new_translit,
footnotes = forms[i].footnotes,
}
iut.insert_form(temp_dedup, "temp", new_formobj)
end
else
local new_formobj = {
form = new_term,
translit = new_translit,
footnotes = forms[i].footnotes,
}
iut.insert_form(temp_dedup, "temp", new_formobj)
end
end
base.forms[slot] = temp_dedup.temp
end
end
end
local function process_slot_overrides(base)
for slot, forms in pairs(base.slot_overrides) do
local existing_values = base.forms[slot]
base.forms[slot] = nil
for _, form in ipairs(forms) do
-- + in active participle for form I requests slot ap1
if form.form == "+" and (base.verb_form ~= "I" or slot ~= "ap") then
if not existing_values then
error(("Slot '%s' requested the default value but no such value available"):format(slot))
end
-- We maintain an invariant that no two slots share a form object (although they may share the footnote
-- lists inside the form objects). However, there is no need to copy the form objects here because there
-- is a one-to-one correspondence between slots and slot overrides, i.e. you can't have a default value
-- go into two slots.
insert_form_or_forms(base, slot, existing_values, "allow overrides", form.uncertain)
elseif default_indicator_to_active_participle_slot[form.form] then
if form.form == "++" then
if slot ~= "vn" and slot ~= "ap" and slot ~= "pp" then
error(("Secondary default value request '++' only applicable to verbal nouns and pariciples, but found in slot '%s'"):
format(slot))
end
else
if slot ~= "ap" then
error(("Secondary default value request '%s' only applicable to active pariciples, but found in slot '%s'"):
format(form.form, slot))
end
end
local secondary_default_slot =
slot == "vn" and "vn2" or slot == "pp" and "pp2" or
default_indicator_to_active_participle_slot[form.form]
local existing_values = base.forms[secondary_default_slot]
if not existing_values then
error(("Slot '%s' requested a secondary default value using '%s' but no such value available"):
format(slot, form.form))
end
-- See comment above about the lack of need to copy the form objects.
insert_form_or_forms(base, slot, existing_values, "allow overrides", form.uncertain)
-- To make sure there aren't shared form objects.
base.forms[secondary_default_slot] = nil
else
insert_form_or_forms(base, slot, form, "allow overrides", form.uncertain)
end
end
end
-- Now, for non-stative form-I verbs, fill the active participle slot from ap1 unless it should be missing (e.g.
-- passive-only or user specified 'ap:-').
if base.verb_form == "I" and not base.forms.ap and base.forms.ap1 and not skip_slot(base, "ap") then
local saw_non_stative = false
for _, vowel_spec in ipairs(base.conj_vowels) do
if req(vowel_spec.past, A) then
saw_non_stative = true
break
end
end
if saw_non_stative then
base.forms.ap = base.forms.ap1
-- To make sure there aren't shared form objects.
base.forms.ap1 = nil
end
end
end
local function handle_lemma_linked(base)
-- Compute linked versions of potential lemma slots, for use in {{ar-verb}}. We substitute the original lemma
-- (before removing links) for forms that are the same as the lemma, if the original lemma has links.
for _, slot in ipairs(export.potential_lemma_slots) do
if base.forms[slot] then
insert_form_or_forms(base, slot .. "_linked", iut.map_forms(base.forms[slot], function(form)
if form == base.lemma and rfind(base.linked_lemma, "%[%[") then
return base.linked_lemma
else
return form
end
end))
end
end
end
-- Process specs given by the user using 'addnote[SLOTSPEC][FOOTNOTE][FOOTNOTE][...]'.
local function process_addnote_specs(base)
for _, spec in ipairs(base.addnote_specs) do
for _, slot_spec in ipairs(spec.slot_specs) do
slot_spec = "^" .. slot_spec .. "$"
for slot, forms in pairs(base.forms) do
if rfind(slot, slot_spec) then
-- To save on memory, side-effect the existing forms.
for _, form in ipairs(forms) do
form.footnotes = iut.combine_footnotes(form.footnotes, spec.footnotes)
end
end
end
end
end
end
local function add_missing_links_to_forms(base)
-- Any forms without links should get them now. Redundant ones will be stripped later.
for slot, forms in pairs(base.forms) do
for _, form in ipairs(forms) do
if not form.form:find("%[%[") then
form.form = "[[" .. form.form .. "]]"
end
end
end
end
local function conjugate_verb(base)
construct_stems(base)
for _, vowel_spec in ipairs(base.conj_vowels) do
-- Reconstruct conjugation type from verb form and (possibly inferred) weakness.
conj_type = base.verb_form .. "-" .. vowel_spec.weakness
-- Check that the conjugation type is recognized.
if not conjugations[conj_type] then
error("Unknown conjugation type '" .. conj_type .. "'")
end
-- The way the conjugation functions work is they always add entries to the appropriate parts of the paradigm
-- (each of which is an array), rather than setting the values. This makes it possible to call more than one
-- conjugation function and essentially get a paradigm of the "either A or B" kind. Doing this may insert
-- duplicate entries into a particular paradigm part, but this is not a problem because we check for duplicate
-- entries when adding them, and don't insert in that case.
conjugations[conj_type](base, vowel_spec)
end
postprocess_forms(base)
process_slot_overrides(base)
-- This should happen before add_missing_links_to_forms() so that the comparison `form == base.lemma` in
-- handle_lemma_linked() works correctly and compares unlinked forms to unlinked forms.
handle_lemma_linked(base)
process_addnote_specs(base)
if not base.alternant_multiword_spec.args.noautolinkverb then
add_missing_links_to_forms(base)
end
end
local function parse_indicator_spec(angle_bracket_spec)
-- Store the original angle bracket spec so we can reconstruct the overall conj spec with the lemma(s) in them.
local base = {
angle_bracket_spec = angle_bracket_spec,
conj_vowels = {},
root_consonants = {},
user_stem_overrides = {},
user_slot_overrides = {},
slot_explicitly_missing = {},
slot_uncertain = {},
slot_override_uses_default = {},
addnote_specs = {},
}
local function parse_err(msg)
error(msg .. ": " .. angle_bracket_spec)
end
local function fetch_footnotes(separated_group)
local footnotes
for j = 2, #separated_group - 1, 2 do
if separated_group[j + 1] ~= "" then
parse_err("Extraneous text after bracketed footnotes: '" .. concat(separated_group) .. "'")
end
if not footnotes then
footnotes = {}
end
table.insert(footnotes, separated_group[j])
end
return footnotes
end
local inside = angle_bracket_spec:match("^<(.*)>$")
assert(inside)
local segments = put.parse_multi_delimiter_balanced_segment_run(inside, {{"[", "]"}, {"<", ">"}})
local dot_separated_groups = put.split_alternating_runs_and_strip_spaces(segments, "%.")
-- The first dot-separated element must specify the verb form, e.g. IV or IIq. If the form is I, it needs to include
-- the the past and non-past vowels, e.g. I/a~u for kataba ~ yaktubu. More than one vowel can be given,
-- comma-separated, and more than one past~non-past pair can be given, slash-separated, e.g. I/a,u~u/i~a for form I
-- كمل, which can be conjugated as kamala/kamula ~ yakmulu or kamila ~ yakmalu. An individual vowel spec must be one
-- of a, i or u and in general (a) at least one past~non-past pair most be given, and (b) both past and non-past
-- vowels must be given even though sometimes the vowel can be determined from the unvocalized form. An exception is
-- passive-only verbs, where the vowels can't in general be determined (except indirectly in some cases by looking
-- at an associated non-passive verb); in that case, the vowel~vowel spec can left out.
local slash_separated_groups = put.split_alternating_runs_and_strip_spaces(dot_separated_groups[1], "/")
local form_spec = slash_separated_groups[1]
base.form_footnotes = fetch_footnotes(form_spec)
if form_spec[1] == "" then
parse_err("Missing verb form")
end
if not allowed_vforms_with_weakness_set[form_spec[1]] then
parse_err(("Unrecognized verb form '%s', should be one of %s"):format(
form_spec[1], list_to_text(allowed_vforms, nil, " or ")))
end
if form_spec[1]:find("%-") then
base.verb_form, base.explicit_weakness = form_spec[1]:match("^(.-)%-(.*)$")
else
base.verb_form = form_spec[1]
end
if #slash_separated_groups > 1 then
if base.verb_form ~= "I" then
parse_err(("Past~non-past vowels can only be specified when verb form is I, but saw form '%s'"):format(
base.verb_form))
end
for i = 2, #slash_separated_groups do
local slash_separated_group = slash_separated_groups[i]
local tilde_separated_groups = put.split_alternating_runs_and_strip_spaces(slash_separated_group, "~")
if #tilde_separated_groups ~= 2 then
parse_err(("Expected two tilde-separated vowel specs: %s"):format(concat(slash_separated_group)))
end
local function parse_conj_vowels(tilde_separated_group, vtype)
local conj_vowel_objects = {}
local comma_separated_groups = put.split_alternating_runs_and_strip_spaces(tilde_separated_group, ",")
for _, comma_separated_group in ipairs(comma_separated_groups) do
local conj_vowel = comma_separated_group[1]
if conj_vowel ~= "a" and conj_vowel ~= "i" and conj_vowel ~= "u" then
parse_err(("Expected %s conjugation vowel '%s' to be one of a, i or u in %s"):format(
vtype, conj_vowel, concat(slash_separated_group)))
end
conj_vowel = dia[conj_vowel]
local conj_vowel_footnotes = fetch_footnotes(comma_separated_group)
-- Try to use strings when possible as it makes q() significantly more efficient.
if conj_vowel_footnotes then
table.insert(conj_vowel_objects, {form = conj_vowel, footnotes = conj_vowel_footnotes})
else
table.insert(conj_vowel_objects, conj_vowel)
end
end
return conj_vowel_objects
end
local conj_vowel_spec = {
past = parse_conj_vowels(tilde_separated_groups[1], "past"),
nonpast = parse_conj_vowels(tilde_separated_groups[2], "non-past"),
}
table.insert(base.conj_vowels, conj_vowel_spec)
end
end
for i = 2, #dot_separated_groups do
local dot_separated_group = dot_separated_groups[i]
local first_element = dot_separated_group[1]
if first_element == "addnote" then
local spec_and_footnotes = fetch_footnotes(dot_separated_group)
if #spec_and_footnotes < 2 then
parse_err("Spec with 'addnote' should be of the form 'addnote[SLOTSPEC][FOOTNOTE][FOOTNOTE][...]'")
end
local slot_spec = table.remove(spec_and_footnotes, 1)
local slot_spec_inside = rmatch(slot_spec, "^%[(.*)%]$")
if not slot_spec_inside then
parse_err("Internal error: slot_spec " .. slot_spec .. " should be surrounded with brackets")
end
local slot_specs = rsplit(slot_spec_inside, ",")
-- FIXME: Here, [[Module:it-verb]] called strip_spaces(). Generally we don't do this. Should we?
table.insert(base.addnote_specs, {slot_specs = slot_specs, footnotes = spec_and_footnotes})
elseif first_element:find("^var:") then
if #dot_separated_group > 1 then
parse_err(("Can't attach footnotes to 'var:' spec '%s'"):format(first_element))
end
base.variant = first_element:match("^var:(.*)$")
elseif first_element:find("^I+V?:") then
local root_cons, root_cons_value = first_element:match("^(I+V?):(.*)$")
local root_index
if root_cons == "I" then
root_index = 1
elseif root_cons == "II" then
root_index = 2
elseif root_cons == "III" then
root_index = 3
elseif root_cons == "IV" then
root_index = 4
if not base.verb_form:find("q$") then
parse_err(("Can't specify root consonant IV for non-quadriliteral verb form '%s': %s"):format(
base.verb_form, first_element))
end
end
local cons, translit = root_cons_value:match("^(.*)//(.*)$")
if not cons then
cons = root_cons_value
end
local root_footnotes = fetch_footnotes(dot_separated_group)
if not translit and not root_footnotes then
base.root_consonants[root_index] = cons
else
base.root_consonants[root_index] = {form = cons, translit = translit, footnotes = root_footnotes}
end
elseif first_element:find("^[a-z][a-z0-9_]*:") then
local slot_or_stem, remainder = first_element:match("^(.-):(.*)$")
dot_separated_group[1] = remainder
local comma_separated_groups = put.split_alternating_runs_and_strip_spaces(dot_separated_group, "[,،]")
if overridable_stems[slot_or_stem] then
if base.user_stem_overrides[slot_or_stem] then
parse_err("Overridable stem '" .. slot_or_stem .. "' specified twice")
end
base.user_stem_overrides[slot_or_stem] = overridable_stems[slot_or_stem](comma_separated_groups,
{prefix = slot_or_stem, base = base, parse_err = parse_err, fetch_footnotes = fetch_footnotes})
else -- assume a form override; we validate further later when the possible slots are available
if base.user_slot_overrides[slot_or_stem] then
parse_err("Form override '" .. slot_or_stem .. "' specified twice")
end
base.user_slot_overrides[slot_or_stem] = allow_multiple_values_for_override(comma_separated_groups,
{prefix = slot_or_stem, base = base, parse_err = parse_err, fetch_footnotes = fetch_footnotes},
"is form override")
end
elseif indicator_flags[first_element] then
if #dot_separated_group > 1 then
parse_err("No footnotes allowed with '" .. first_element .. "' spec")
end
if base[first_element] then
parse_err("Spec '" .. first_element .. "' specified twice")
end
base[first_element] = true
else
local passive, uncertain = first_element:match("^(.*)(%?)$")
passive = passive or first_element
uncertain = not not uncertain
if passive_types[passive] then
if #dot_separated_group > 1 then
parse_err("No footnotes allowed with '" .. passive .. "' spec")
end
if base.passive then
parse_err("Value for passive type specified twice")
end
base.passive = passive
base.passive_uncertain = uncertain
else
parse_err("Unrecognized spec '" .. first_element .. "'")
end
end
end
return base
end
-- Normalize all lemmas, substituting the pagename for blank lemmas and adding links to multiword lemmas.
local function normalize_all_lemmas(alternant_multiword_spec, head)
-- (1) Add links to all before and after text. Remember the original text so we can reconstruct the verb spec later.
if not alternant_multiword_spec.args.noautolinktext then
iut.add_links_to_before_and_after_text(alternant_multiword_spec, "remember original")
end
-- (2) Remove any links from the lemma, but remember the original form so we can use it below in the 'lemma_linked'
-- form.
iut.map_word_specs(alternant_multiword_spec, function(base)
if base.lemma == "" then
base.lemma = head
end
base.user_specified_lemma = base.lemma
base.lemma = m_links.remove_links(base.lemma)
base.user_specified_verb = base.lemma
base.verb = base.user_specified_verb
local linked_lemma
if alternant_multiword_spec.args.noautolinkverb or base.user_specified_lemma:find("%[%[") then
linked_lemma = base.user_specified_lemma
else
-- Add links to the lemma so the user doesn't specifically need to, since we preserve
-- links in multiword lemmas and include links in non-lemma forms rather than allowing
-- the entire form to be a link.
linked_lemma = iut.add_links(base.user_specified_lemma)
end
base.linked_lemma = linked_lemma
end)
end
-- Determine weakness from radicals. Used when root given in place of lemma (e.g. for {{ar-verb forms}}).
local function weakness_from_radicals(form, rad1, rad2, rad3, rad4)
local weakness = nil
local quadlit = form:find("q$")
-- If weakness unspecified, derive from radicals.
if not quadlit then
if is_waw_ya(rad3) and rad1 == W and form == "I" then
weakness = "assimilated+final-weak"
elseif is_waw_ya(rad3) and vform_supports_final_weak(form) then
weakness = "final-weak"
elseif rad2 == rad3 and vform_supports_geminate(form) then
weakness = "geminate"
elseif is_waw_ya(rad2) and vform_supports_hollow(form) then
weakness = "hollow"
elseif rad1 == W and form == "I" then
weakness = "assimilated"
else
weakness = "sound"
end
else
if is_waw_ya(rad4) then
weakness = "final-weak"
else
weakness = "sound"
end
end
return weakness
end
-- Join the infixed tāʔ (ت) to the first radical in form VIII verbs. This may cause assimilation of the tāʔ to the
-- radical or in some cases the radical to the tāʔ. Used when a root is supplied instead of a lemma (which already has
-- the appropriate assimilation in it).
local function form_viii_join_ta(rad)
if rad == W or rad == Y or rad == "ت" then return "تّ"
elseif rad == "د" then return "دّ"
elseif rad == "ث" then return "ثّ"
elseif rad == "ذ" then return "ذّ"
elseif rad == "ز" then return "زْد"
elseif rad == "ص" then return "صْط"
elseif rad == "ض" then return "ضْط"
elseif rad == "ط" then return "طّ"
elseif rad == "ظ" then return "ظّ"
else return rad .. SK .. "ت"
end
end
local function detect_indicator_spec(base)
base.forms = {}
base.stem_overrides = {}
base.slot_overrides = {}
if not base.conj_vowels[1] then
-- These may be converted to inferred vowels. If not, we throw an error if form I and not passive-only.
base.conj_vowels = {{
past = "-",
nonpast = "-",
}}
else
-- If multiple vowels specified for a given vowel type (e.g. a,u~u), expand so that each spec in
local expansion = {}
for _, spec in ipairs(base.conj_vowels) do
for _, past in ipairs(spec.past) do
for _, nonpast in ipairs(spec.nonpast) do
table.insert(expansion, {past = past, nonpast = nonpast})
end
end
end
base.conj_vowels = expansion
end
local vform = base.verb_form
-- check for quadriliteral form (Iq, IIq, IIIq, IVq)
base.quadlit = not not vform:find("q$")
-- Infer radicals as necessary. We infer a separate set of radicals for each past~non-past vowel combination because
-- they may be different (particularly with form-I hollow verbs).
for _, vowel_spec in ipairs(base.conj_vowels) do
-- NOTE: rad1, rad2, etc. refer to user-specified radicals, which are formobj tables that optionally specify an
-- explicit manual translit, whereas ir1, ir2, etc. refer to inferred radicals, which are either strings or
-- lists of possible radicals.
local rads = base.root_consonants
local rad1, rad2, rad3, rad4 = rads[1], rads[2], rads[3], rads[4]
-- Default any unspecified radicals to radicals determined from the headword. The returned radicals may be
-- lists of possible radicals, where the first radical should be chosen if the user didn't explicitly specify a
-- radical but all are allowed. If `ambig = true` is set in the table, the radical is considered ambiguous and
-- categories won't be created for weak radicals.
local weakness, ir1, ir2, ir3, ir4
if vform ~= "none" then
ir1, ir2, ir3 = rmatch(base.lemma, "^([^_])_([^_])_([^_])$")
if not ir1 then
ir1, ir2, ir3, ir4 = rmatch(base.lemma, "^([^_])_([^_])_([^_])_([^_])$")
end
if ir1 then
-- root given instead of lemma
weakness = weakness_from_radicals(vform, ir1, ir2, ir3, ir4)
if vform == "VIII" then
vowel_spec.form_viii_assim = form_viii_join_ta(ir1)
end
else
local ret = export.infer_radicals {
headword = base.lemma,
vform = vform,
passive = base.passive,
past_vowel = vowel_spec.past,
nonpast_vowel = vowel_spec.nonpast,
is_reduced = base.reduced,
}
weakness, ir1, ir2, ir3, ir4 = ret.weakness, ret.rad1, ret.rad2, ret.rad3, ret.rad4
vowel_spec.form_viii_assim = ret.form_viii_assim
vowel_spec.past = ret.past_vowel
vowel_spec.nonpast = ret.nonpast_vowel
vowel_spec.variant = base.variant or ret.variant
end
end
-- For most ambiguous radicals, the choice of radical doesn't matter because it doesn't affect the conjugation
-- one way or another. For form I hollow verbs, however, it definitely does. In fact, the choice of radical is
-- critical even beyond the past and non-past vowels because it affects the form of the passive participle. So,
-- check for this and signal an error if the radical could not be inferred and is not given explicitly.
if vform == "I" and type(ir2) == "table" and ir2.need_radical and not rad2 then
error("Unable to guess middle radical of hollow form I verb; need to specify radical explicitly")
end
if vform == "I" and not is_passive_only(base.passive) and (
rget(vowel_spec.past) == "-" or rget(vowel_spec.nonpast) == "-") then
error("Form I verb that isn't passive-only or final-weak must have past~non-past vowels specified")
end
-- Convert ambiguous radicals.
local function regularize_inferred_radical(rad)
if type(rad) == "table" then
if rad.ambig then
return {form = rad[1], ambig = true}
else
return rad[1]
end
else
return rad
end
end
-- Return the appropriate radical at index `index` (1 through 4), based either on the user-specified radical
-- `user_radical` or (if unspecified) `inferred_radical`, inferred from the unvocalized lemma. Two values are
-- returned, the "regularized" version of the radical (where ambiguous inferred radicals are converted to their
-- most likely actual radical) and the non-regularized version. The returned values are form objects rather than
-- strings.
local function fetch_radical(user_radical, inferred_radical, index)
if not user_radical then
return regularize_inferred_radical(inferred_radical), inferred_radical
else
local rad_formval = rget(user_radical)
if type(inferred_radical) == "table" then
local allowed_radical_set = m_table.listToSet(inferred_radical)
if not allowed_radical_set[rad_formval] then
error(("For lemma %s, radical %s ambiguously inferred as %s but user radical incompatibly given as %s"):
format(base.lemma, index,
list_to_text(inferred_radical, nil, " or "), rad_formval))
end
elseif rad_formval ~= inferred_radical then
error(("For lemma %s, radical %s inferred as %s but user radical incompatibly given as %s"):
format(base.lemma, index, inferred_radical, rad_formval))
end
return user_radical, user_radical
end
end
if vform ~= "none" then
vowel_spec.rad1, vowel_spec.unreg_rad1 = fetch_radical(rad1, ir1, 1)
vowel_spec.rad2, vowel_spec.unreg_rad2 = fetch_radical(rad2, ir2, 2)
vowel_spec.rad3, vowel_spec.unreg_rad3 = fetch_radical(rad3, ir3, 3)
if base.quadlit then
vowel_spec.rad4, vowel_spec.unreg_rad4 = fetch_radical(rad4, ir4, 4)
end
end
if vform == "I" then
-- If explicit weakness given using 'I-sound' or 'I-assimilated', we may need to adjust the inferred weakness.
if base.explicit_weakness == "sound" then
if weakness == "assimilated" then
weakness = "sound"
elseif weakness == "assimilated+final-weak" then
-- Verbs like waniya~yawnā "to be faint; to languish" (although the defaults should handle this
-- correctly)
weakness = "final-weak"
else
error(("Can't specify form 'I-sound' when inferred weakness is '%s' for lemma %s"):format(
weakness, base.lemma))
end
elseif base.explicit_weakness == "assimilated" then
if weakness == "sound" then
-- i~a verbs like waṭiʔa~yaṭaʔu "to tread, to trample"; wasiʕa~yasaʕu "to be spacious; to be well-off";
-- waṯiʔa~yaṯaʔu "to get bruised, to be sprained", which would default to sound.
weakness = "assimilated"
elseif weakness == "final-weak" then
-- For completeness; not clear if any verbs occur where this is needed. (There are plenty of
-- assimilated+final-weak verbs but the defaults should take care of them.)
weakness = "assimilated+final-weak"
else
error(("Can't specify form 'I-assimilated' when inferred weakness is '%s' for lemma %s"):format(
weakness, base.lemma))
end
elseif base.explicit_weakness then
error(("Internal error: Unrecognized value '%s' for base.explicit_weakness"):format(base.explicit_weakness))
end
elseif vform == "none" then
weakness = base.explicit_weakness
elseif base.explicit_weakness then
error(("Internal error: Explicit weakness should not be specifiable except with forms I and none, but saw explicit weakness '%s' with verb form '%s'"):
format(base.explicit_weakness, vform))
end
vowel_spec.weakness = weakness
if vform ~= "none" then
-- Error if radicals are wrong given the weakness. More likely to happen if the weakness is explicitly given
-- rather than inferred. Will also happen if certain incorrect letters are included as radicals e.g. hamza on
-- top of various letters, alif maqṣūra, tā' marbūṭa.
check_radicals(vform, weakness, rget(vowel_spec.rad1), rget(vowel_spec.rad2), rget(vowel_spec.rad3),
base.quadlit and rget(vowel_spec.rad4) or nil)
end
-- Check the variant value.
local form_iii_vi_geminate = (vform == "III" or vform == "VI") and rget(vowel_spec.rad2) == rget(vowel_spec.rad3) and
not req(vowel_spec.rad2, Y)
local hayy_i_x = hayy_radicals(vowel_spec.rad1, vowel_spec.rad2, vowel_spec.rad3) and (vform == "I" or vform == "X")
if form_iii_vi_geminate or hayy_i_x then
if vowel_spec.variant and vowel_spec.variant ~= "long" and vowel_spec.variant ~= "short" and vowel_spec.variant ~= "both" then
error(("For form-III/VI geminate verb or form-I/X verb with ح-ي-ي radicals, saw unrecognized 'var:%s' value; should be 'var:long', 'var:short' or 'var:both'"):format(
vowel_spec.variant))
end
elseif vowel_spec.variant then
error(("Variant value 'var:%s' not allowed in this context"):format(vowel_spec.variant))
end
end
-- If form I, regroup expanded vowels for display purposes.
if vform == "I" then
local group_by_past = {}
for _, vowel_spec in ipairs(base.conj_vowels) do
m_table.insertIfNot(group_by_past, {
past = undia[rget(vowel_spec.past)],
nonpasts = {undia[rget(vowel_spec.nonpast)]},
}, {
key = function(obj) return obj.past end,
combine = function(obj1, obj2)
for _, nonpast in ipairs(obj2.nonpasts) do
m_table.insertIfNot(obj1.nonpasts, nonpast)
end
end,
})
end
local group_by_nonpast = {}
for _, vowel_spec in ipairs(group_by_past) do
m_table.insertIfNot(group_by_nonpast, {
pasts = {vowel_spec.past},
nonpasts = vowel_spec.nonpasts,
}, {
key = function(obj) return obj.nonpasts end,
combine = function(obj1, obj2)
for _, past in ipairs(obj2.pasts) do
m_table.insertIfNot(obj1.pasts, past)
end
end,
})
end
base.grouped_conj_vowels = group_by_nonpast
end
-- Set value of passive. If not specified, default is yes for forms II, III, IV and Iq; no but uncertainly for
-- forms VII, IX, XI - XV and IIIq - IVq, as well as form I with past vowel u; impersonal but uncertainly for form
-- V, VI, X and IIq, as well as form I with past vowel i; and yes but uncertainly for the remainder (form I with
-- past vowel only a and form VIII).
if not base.passive then
base.passive_defaulted = true
-- Temporary tracking for defaulted passives by verb form, weakness and (for form I) past/non-past vowels.
track_if_ar_conj(base, "passive-defaulted/" .. vform)
for _, vowel_spec in ipairs(base.conj_vowels) do
track_if_ar_conj(base, "passive-defaulted/" .. vform.. "/" .. vowel_spec.weakness)
if vform == "I" then
local past_nonpast = ("%s~%s"):format(undia[vowel_spec.past], undia[vowel_spec.nonpast])
track_if_ar_conj(base, "passive-defaulted/I/" .. past_nonpast)
track_if_ar_conj(base, "passive-defaulted/I/" .. vowel_spec.weakness .. "/" .. past_nonpast)
end
end
if vform_probably_full_passive(vform) then
base.passive = "pass"
else
base.passive_uncertain = true
for _, vowel_spec in ipairs(base.conj_vowels) do
if vform_probably_no_passive(vform, vowel_spec.weakness, vowel_spec.past, vowel_spec.nonpast) then
base.passive = "nopass"
break
elseif vform_probably_impersonal_passive(vform, vowel_spec.weakness, vowel_spec.past,
vowel_spec.nonpast) then
base.passive = "ipass"
break
end
end
base.passive = base.passive or "pass"
end
end
-- NOTE: Currently there are no built-in stems or form overrides for Arabic; this code is inherited from
-- [[Module:ca-verb]], where such things do exist, and is kept for generality in case we decide in the future to
-- implement such things.
-- Override built-in verb stems and overrides with user-specified ones.
for stem, values in pairs(base.user_stem_overrides) do
base.stem_overrides[stem] = values
end
for slot, values in pairs(base.user_slot_overrides) do
if not base.alternant_multiword_spec.verb_slots_map[slot] then
error("Unrecognized override slot '" .. slot .. "': " .. base.angle_bracket_spec)
end
if export.unsettable_slots_set[slot] then
error("Slot '" .. slot .. "' cannot be set using an override: " .. base.angle_bracket_spec)
end
if skip_slot(base, slot, "allow overrides") then
error("Override slot '" .. slot ..
"' would be skipped based on the passive, 'noimp' and/or 'no_nonpast' settings: " ..
base.angle_bracket_spec)
end
base.slot_overrides[slot] = values
end
if base.verb_form == "none-final-weak" then
for _, stem_type in ipairs { "past", "past_pass", "nonpast", "nonpast_pass" } do
if base.stem_overrides[stem_type .. "_c"] or base.stem_overrides[stem_type .. "_v"] then
error(("Specify past stem for verb type 'none-final-weak' using '%s:...' not '%s_c:...' or '%s_v:...'"):
format(stem_type, stem_type, stem_type))
end
end
for _, stem_type in ipairs { "past", "nonpast" } do
if base.stem_overrides[stem_type] or not base.stem_overrides[stem_type .. "_final_weak_vowel"] then
error(("For verb type 'none-final-weak', if '%s:...' specified, so must '%s_final_weak_vowel:...'"):
format(stem_type, stem_type))
end
end
end
end
local function detect_all_indicator_specs(alternant_multiword_spec)
add_slots(alternant_multiword_spec)
alternant_multiword_spec.verb_forms = {}
-- This means at least one individual base had the slot marked as explicitly missing. Another base (e.g. when
-- there are multiple alternants) might have a value for the slot. In practice, we only respect this when there are
-- no overall values in the slot and `slot_uncertain` isn't set; in this case, we display "no ..." for the slot
-- instead of simply not displaying anything for the slot.
alternant_multiword_spec.slot_explicitly_missing = {}
-- This means at least one individual base had no values for the slot and the slot marked as explicitly uncertain.
-- Note that this is different from a value being present but marked as uncertain (e.g. if an override was given
-- with a ? after it); this causes the form object for the value to have `uncertain = true` set. If there are no
-- overall values in the slot and `slot_uncertain` is set, we display this in the headword.
alternant_multiword_spec.slot_uncertain = {}
iut.map_word_specs(alternant_multiword_spec, function(base)
-- So arguments, etc. can be accessed. WARNING: Creates circular reference.
base.alternant_multiword_spec = alternant_multiword_spec
detect_indicator_spec(base)
if not base.nocat then
m_table.insertIfNot(alternant_multiword_spec.verb_forms, base.verb_form)
end
if base.passive_uncertain then
alternant_multiword_spec.passive_uncertain = true
end
for slot, _ in pairs(base.slot_explicitly_missing) do
alternant_multiword_spec.slot_explicitly_missing[slot] = true
end
end)
end
local function determine_slot_uncertainty_from_forms(alternant_multiword_spec)
iut.map_word_specs(alternant_multiword_spec, function(base)
-- If no verbal noun and verb form is not 'none' (manually-specified stems) — which currently only happens for
-- form I — and the verbal noun wasn't explicitly indicated as missing using <vn:->, we assume it's just
-- unknown/unspecified rather than missing. Same with active participles.
local vform = base.verb_form
for uncertain_slot, _ in pairs(slots_that_may_be_uncertain) do
if not base.forms[uncertain_slot] and vform ~= "none" and not skip_slot(base, uncertain_slot) then
base.slot_uncertain[uncertain_slot] = true
end
end
-- Propagate slot uncertainty up. Currently only the verbal noun can have this set but we write the code
-- generally.
for slot, _ in pairs(base.slot_uncertain) do
alternant_multiword_spec.slot_uncertain[slot] = true
end
end)
-- If slot is uncertain and has no value, explicitly set its value to "?".
for uncertain_slot, _ in pairs(slots_that_may_be_uncertain) do
if not alternant_multiword_spec.forms[uncertain_slot] and
alternant_multiword_spec.slot_uncertain[uncertain_slot] then
alternant_multiword_spec.forms[uncertain_slot] = {{form = "?"}}
end
end
end
-- Determine certain properties of the verb from the overall forms, such as whether the verb is active-only or
-- passive-only, is impersonal, lacks an imperative, etc.
local function determine_verb_properties_from_forms(alternant_multiword_spec)
alternant_multiword_spec.has_active = false
alternant_multiword_spec.has_passive = false
alternant_multiword_spec.has_non_impers_active = false
alternant_multiword_spec.has_non_impers_passive = false
alternant_multiword_spec.has_imp = false
alternant_multiword_spec.has_past = false
alternant_multiword_spec.has_nonpast = false
for slot, _ in pairs(alternant_multiword_spec.forms) do
if slot == "ap" or slot:find("[123]") and not slot:find("_pass") then
alternant_multiword_spec.has_active = true
end
if slot == "pp" or slot:find("[123]") and slot:find("_pass") then
alternant_multiword_spec.has_passive = true
end
if slot:find("[123]") and not slot:find("pass_[123]") and not slot:find("3ms") then
alternant_multiword_spec.has_non_impers_active = true
end
if slot:find("pass_[123]") and not slot:find("3ms") then
alternant_multiword_spec.has_non_impers_passive = true
end
if slot:find("^imp_") then
alternant_multiword_spec.has_imp = true
end
if slot:find("^past_") then
alternant_multiword_spec.has_past = true
end
if slot:find("^ind_") or slot:find("^sub_") or slot:find("^juss_") then
alternant_multiword_spec.has_nonpast = true
end
end
end
local function add_categories_and_annotation(alternant_multiword_spec, base, multiword_lemma, insert_ann, insert_cat)
-- Useful e.g. in constructing suppletive verbs out of parts. For a verb like جاء or أتى whose imperative comes
-- from the unrelated verb تعالى, we don't want the latter verb showing up in categories or annotations.
if base.nocat then
return
end
local vform = base.verb_form
if vform ~= "none" then
insert_ann("form", vform)
insert_cat("form-" .. vform .. " verbs")
end
if base.reduced then
insert_ann("reduced", "reduced")
if vform ~= "none" then
insert_cat("form-" .. vform .. " reduced verbs")
end
end
if base.quadlit then
insert_cat("verbs with quadriliteral roots")
end
if base.passive_defaulted then
insert_cat("verbs with defaulted passive")
end
for _, vowel_spec in ipairs(base.conj_vowels) do
local rad1, rad2, rad3, rad4 = get_radicals_4(vowel_spec)
local final_weak = is_final_weak(base, vowel_spec)
local weakness = vowel_spec.weakness
-- We have to distinguish weakness by form and weakness by conjugation. Weakness by form merely indicates the
-- presence of weak letters in certain positions in the radicals. Weakness by conjugation is related to how the
-- verbs are conjugated. For example, form-II verbs that are "hollow by form" (middle radical is wāw or yāʾ) are
-- conjugated as sound verbs. Another example: form-I verbs with initial wāw are "assimilated by form" and most
-- are assimilated by conjugation as well, but a few are sound by conjugation, e.g. wajuha yawjuhu "to be
-- distinguished" (rather than wajuha yajuhu); similarly for some hollow-by-form verbs in various forms, e.g.
-- form VIII izdawaja yazdawiju "to be in pairs" (rather than izdāja yazdāju). Categories referring to weakness
-- always refer to weakness by conjugation; weakness by form is distinguished only by categories such as
-- [[:Category:Arabic form-III verbs with و as second radical]].
insert_ann("weakness", weakness)
if vform ~= "none" then
insert_cat(("%s form-%s verbs"):format(weakness, vform))
end
local function radical_is_ambiguous(rad)
return type(rad) == "table" and rad.ambig
end
local function radical_is_unambiguous_weak(rad)
return not radical_is_ambiguous(rad) and (is_waw_ya(rad) or req(rad, HAMZA))
end
if vform ~= "none" then
local ur1, ur2, ur3, ur4 =
vowel_spec.unreg_rad1, vowel_spec.unreg_rad2, vowel_spec.unreg_rad3, vowel_spec.unreg_rad4
-- Create headword categories based on the radicals. Do the following before
-- converting the Latin radicals into Arabic ones so we distinguish
-- between ambiguous and non-ambiguous radicals.
if radical_is_ambiguous(ur1) or radical_is_ambiguous(ur2) or radical_is_ambiguous(ur3) or
ur4 and radical_is_ambiguous(ur4) then
insert_cat("verbs with ambiguous radicals")
end
if radical_is_unambiguous_weak(ur1) then
insert_cat("form-" .. vform .. " verbs with " .. rget(ur1) .. " as first radical")
end
if radical_is_unambiguous_weak(ur2) then
insert_cat("form-" .. vform .. " verbs with " .. rget(ur2) .. " as second radical")
end
if radical_is_unambiguous_weak(ur3) then
insert_cat("form-" .. vform .. " verbs with " .. rget(ur3) .. " as third radical")
end
if ur4 and radical_is_unambiguous_weak(ur4) then
insert_cat("form-" .. vform .. " verbs with " .. rget(ur4) .. " as fourth radical")
end
end
end
if vform == "I" and not is_passive_only(base.passive) then
for _, vowel_spec in ipairs(base.grouped_conj_vowels) do
insert_ann("vowels",
("%s ~ %s"):format(concat(vowel_spec.pasts, "/"), concat(vowel_spec.nonpasts, "/")))
for _, past in ipairs(vowel_spec.pasts) do
for _, nonpast in ipairs(vowel_spec.nonpasts) do
if past == "-" or nonpast == "-" then
error("Internal error: Saw form I past vowel %s and non-past vowel %s but - in place of vowel should have triggered an error earlier")
end
insert_cat(("form-I verbs with past vowel %s and non-past vowel %s"):format(past, nonpast))
end
end
end
end
for slot, name in pairs(slots_that_may_be_uncertain) do
if base.slot_uncertain[slot] then
-- An unspecified and non-defaulted verbal noun (form I) is considered uncertain rather than explicitly
-- missing. Use <vn:-> to explicitly indicate the lack of verbal noun. Same for form-I stative active
-- participles.
insert_cat(("verbs with unknown or uncertain %ss"):format(name))
end
end
if base.irregular then
insert_ann("irreg", "irregular")
insert_cat("irregular verbs")
end
end
-- Compute the categories to add the verb to, as well as the annotation to display in the conjugation title bar. We
-- combine the code to do these functions as both categories and title bar contain similar information.
local function compute_categories_and_annotation(alternant_multiword_spec)
alternant_multiword_spec.categories = {}
local ann = {}
alternant_multiword_spec.annotation = ann
ann.form = {}
ann.weakness = {}
ann.vowels = {}
ann.passive = nil
ann.reduced = {}
ann.irreg = {}
ann.defective = {}
local multiword_lemma = false
for _, slot in ipairs(export.potential_lemma_slots) do
if alternant_multiword_spec.forms[slot] then
for _, formobj in ipairs(alternant_multiword_spec.forms[slot]) do
if formobj.form:find(" ") then
multiword_lemma = true
break
end
end
break
end
end
local function insert_ann(anntype, value)
m_table.insertIfNot(alternant_multiword_spec.annotation[anntype], value)
end
local function insert_cat(cat, also_when_multiword)
-- Don't place multiword terms in categories like 'Arabic form-II verbs' to avoid spamming the categories with
-- such terms.
if also_when_multiword or not multiword_lemma then
m_table.insertIfNot(alternant_multiword_spec.categories, "Arabic " .. cat)
end
end
iut.map_word_specs(alternant_multiword_spec, function(base)
add_categories_and_annotation(alternant_multiword_spec, base, multiword_lemma, insert_ann, insert_cat)
end)
for slot, name in pairs(slots_that_may_be_uncertain) do
if alternant_multiword_spec.forms[slot] then
for _, form in ipairs(alternant_multiword_spec.forms[slot]) do
if form.uncertain then
if form.form == "?" then
insert_cat(("verbs with explicitly unknown %ss"):format(name))
else
insert_cat(("verbs needing %s checked"):format(name))
end
break
end
end
end
end
if alternant_multiword_spec.has_active then
if alternant_multiword_spec.has_passive and alternant_multiword_spec.has_non_impers_passive then
insert_cat("verbs with full passive")
ann.passive = "full passive"
elseif alternant_multiword_spec.has_passive then
insert_cat("verbs with impersonal passive")
ann.passive = "impersonal passive"
else
insert_cat("verbs lacking passive forms")
ann.passive = "no passive"
end
else
if alternant_multiword_spec.has_non_impers_passive then
insert_cat("passive verbs")
insert_cat("verbs with full passive")
ann.passive = "passive-only"
else
insert_cat("passive verbs")
insert_cat("impersonal verbs")
insert_cat("verbs with impersonal passive")
ann.passive = "impersonal (passive-only)"
end
end
if alternant_multiword_spec.passive_uncertain then
insert_cat("verbs needing passive checked")
ann.passive = ann.passive .. ' <abbr title="passive status uncertain">(?)</abbr>'
end
if alternant_multiword_spec.has_active and not alternant_multiword_spec.has_imp then
insert_ann("defective", "no imperative")
insert_cat("verbs lacking imperative forms")
end
if not alternant_multiword_spec.has_past then
insert_ann("defective", "no past")
insert_cat("verbs lacking past forms")
end
if not alternant_multiword_spec.has_nonpast then
insert_ann("defective", "no non-past")
insert_cat("verbs lacking non-past forms")
end
local ann_parts = {}
local function insert_ann_part(part, conj)
local val = concat(ann[part], conj or " or ")
if val ~= "" and val ~= "regular" then
table.insert(ann_parts, val)
end
end
insert_ann_part("form")
insert_ann_part("weakness")
insert_ann_part("reduced")
insert_ann_part("vowels")
if ann.passive then
table.insert(ann_parts, ann.passive)
end
insert_ann_part("irreg")
insert_ann_part("defective", ", ")
alternant_multiword_spec.annotation = concat(ann_parts, ", ")
end
local function show_forms(alternant_multiword_spec)
local lemmas = {}
for _, slot in ipairs(export.potential_lemma_slots) do
if alternant_multiword_spec.forms[slot] then
for _, formobj in ipairs(alternant_multiword_spec.forms[slot]) do
table.insert(lemmas, formobj)
end
break
end
end
alternant_multiword_spec.lemmas = lemmas -- save for later use in make_table()
alternant_multiword_spec.vn = alternant_multiword_spec.forms.vn -- save for later use in make_table()
-- Reconstruct the original verb spec without overrides for verbal nouns and participles, since those specific slots
-- are ignored by {{ar-verb form}}. Compute this once beforehand; `transform_accel_obj` is called repeatedly on each
-- form and we don't want to compute this repeatedly.
local reconstructed_verb_spec = iut.reconstruct_original_spec(alternant_multiword_spec, {
preprocess_angle_bracket_spec = function(spec)
spec = spec:match("^<(.*)>$")
assert(spec)
local segments = put.parse_multi_delimiter_balanced_segment_run(spec, {{"[", "]"}, {"<", ">"}})
local dot_separated_groups = put.split_alternating_runs_and_strip_spaces(segments, "%.")
-- Rejoin each dot-separated group into a single string, since we aren't actually going to do any parsing
-- of bracket-bounded textual runs; then filter out overrides for verbal nouns and participles.
local filtered_indicators = {}
for _, dot_separated_group in ipairs(dot_separated_groups) do
local indicator = concat(dot_separated_group)
-- FIXME: Do we want to filter out any other indicators?
if not (indicator:find("^vn:") or indicator:find("^[ap]p:")) then
table.insert(filtered_indicators, indicator)
end
end
return ("<%s>"):format(concat(filtered_indicators, "."))
end,
})
-- If we're dealing with a single word, no alternants and a single verb form, use the auto-conjugation-fetching
-- variant.
local reconstructed_lemma, inside = reconstructed_verb_spec:match("^([^ <>()]+)(%b<>)$")
if inside and alternant_multiword_spec.verb_forms[1] and not alternant_multiword_spec.verb_forms[2] then
reconstructed_verb_spec = ("+%s<%s>"):format(reconstructed_lemma, alternant_multiword_spec.verb_forms[1])
end
local function transform_accel_obj(slot, formobj, accel_obj)
if not accel_obj then
return accel_obj
end
if slot == "ap" or slot == "pp" or slot == "vn" then
-- FIXME: [[Module:accel]] can't correctly handle more than one verb form for participles and verbal nouns
accel_obj.form = slot .. "-" .. concat(alternant_multiword_spec.verb_forms, ",")
else
accel_obj.form = "verb-form-" .. reconstructed_verb_spec
end
return accel_obj
end
local function generate_link(data)
local form = data.form
local term = form.formval_for_link
local alt = form.alt
if term == "?" then
term = nil
alt = "?"
end
local link = m_links.full_link {
lang = lang, term = term, tr = "-", accel = form.accel_obj,
alt = alt, gloss = form.gloss, genders = form.genders, pos = form.pos, lit = form.lit, id = form.id,
} .. iut.get_footnote_text(form.footnotes, data.footnote_obj)
if form.q and form.q[1] or form.qq and form.qq[1] or form.l and form.l[1] or form.ll and form.ll[1] then
link = require(decorations_module).format_decorations {
lang = lang,
text = link,
q = form.q,
qq = form.qq,
l = form.l,
ll = form.ll,
}
end
return link
end
local props = {
lang = lang,
lemmas = lemmas,
transform_accel_obj = transform_accel_obj,
generate_link = generate_link,
slot_list = alternant_multiword_spec.verb_slots,
include_translit = true,
}
iut.show_forms(alternant_multiword_spec.forms, props)
end
-------------------------------------------------------------------------------
-- Functions to create inflection tables --
-------------------------------------------------------------------------------
-- Make the conjugation table. Called from export.show().
local function make_table(alternant_multiword_spec)
local text = m_inflection_table.make_top{
title = 'Pengkonjugatan bagi {title}',
tall = 'yes',
palette = "green",
category = 'Pengkonjugatan',
class = 'tr-alongside', -- temp hack to prevent extra line break
}
text = text .. [=[
! colspan="6" | kata nama kerjaan<br /><<الْمَصْدَر>>
| colspan="7" | {vn}
]=]
if alternant_multiword_spec.has_active then
text = text .. [=[
|-
! colspan="6" | active participle<br /><<اِسْم الْفَاعِل>>
| colspan="7" | {ap}
]=]
end
if alternant_multiword_spec.has_passive then
text = text .. [=[
|-
! colspan="6" | passive participle<br /><<اِسْم الْمَفْعُول>>
| colspan="7" | {pp}
]=]
end
text = text .. [=[
|-
! colspan="999" class="separator" |
]=]
if alternant_multiword_spec.has_active then
text = text .. [=[
|-
! colspan="13" class="outer" | active voice<br /><<الْفِعْل الْمَعْلُوم>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="12" class="separator" |
! colspan="3" | dual<br /><<الْمُثَنَّى>>
! rowspan="12" class="separator" |
! colspan="3"| plural<br /><<الْجَمْع>>
|-
! colspan="2"|
! 1<sup>st</sup> person<br /><<الْمُتَكَلِّم>>
! 2<sup>nd</sup> person<br /><<الْمُخَاطَب>>
! 3<sup>rd</sup> person<br /><<الْغَائِب>>
! 1<sup>st</sup> person<br /><<الْمُتَكَلِّم>>
! 2<sup>nd</sup> person<br /><<الْمُخَاطَب>>
! 3<sup>rd</sup> person<br /><<الْغَائِب>>
! 1<sup>st</sup> person<br /><<الْمُتَكَلِّم>>
! 2<sup>nd</sup> person<br /><<الْمُخَاطَب>>
! 3<sup>rd</sup> person<br /><<الْغَائِب>>
|-
! rowspan="2" | past indicative<br />(perfect)<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_1s}
| {past_2ms}
| {past_3ms}
| rowspan="2" | {past_1d}
| rowspan="2" | {past_2d}
| {past_3md}
| rowspan="2" | {past_1p}
| {past_2mp}
| {past_3mp}
|-
! class="secondary" | f
| {past_2fs}
| {past_3fs}
| {past_3fd}
| {past_2fp}
| {past_3fp}
|-
! rowspan="2" | non-past indicative<br />(imperfect)<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_1s}
| {ind_2ms}
| {ind_3ms}
| rowspan="2" | {ind_1d}
| rowspan="2" | {ind_2d}
| {ind_3md}
| rowspan="2" | {ind_1p}
| {ind_2mp}
| {ind_3mp}
|-
! class="secondary" | f
| {ind_2fs}
| {ind_3fs}
| {ind_3fd}
| {ind_2fp}
| {ind_3fp}
|-
! rowspan="2" | subjunctive<br /><<الْمُضَارِع الْمَنْصُوب>>
! class="secondary" | m
| rowspan="2" | {sub_1s}
| {sub_2ms}
| {sub_3ms}
| rowspan="2" | {sub_1d}
| rowspan="2" | {sub_2d}
| {sub_3md}
| rowspan="2" | {sub_1p}
| {sub_2mp}
| {sub_3mp}
|-
! class="secondary" | f
| {sub_2fs}
| {sub_3fs}
| {sub_3fd}
| {sub_2fp}
| {sub_3fp}
|-
! rowspan="2" | jussive<br /><<الْمُضَارِع الْمَجْزُوم>>
! class="secondary" | m
| rowspan="2" | {juss_1s}
| {juss_2ms}
| {juss_3ms}
| rowspan="2" | {juss_1d}
| rowspan="2" | {juss_2d}
| {juss_3md}
| rowspan="2" | {juss_1p}
| {juss_2mp}
| {juss_3mp}
|-
! class="secondary" | f
| {juss_2fs}
| {juss_3fs}
| {juss_3fd}
| {juss_2fp}
| {juss_3fp}
|-
! rowspan="2" | imperative<br /><<الْأَمْر>>
! class="secondary" | m
| rowspan="2" |
| {imp_2ms}
| rowspan="2" |
| rowspan="2" |
| rowspan="2" | {imp_2d}
| rowspan="2" |
| rowspan="2" |
| {imp_2mp}
| rowspan="2" |
|-
! class="secondary" | f
| {imp_2fs}
| {imp_2fp}
]=]
end
if alternant_multiword_spec.has_passive then
text = text .. [=[
|-
! colspan="999" class="separator" |
|-
! colspan="13" class="outer" | passive voice<br /><<الْفِعْل الْمَجْهُول>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="10" class="separator" |
! colspan="3" | dual<br /><<الْمُثَنَّى>>
! rowspan="10" class="separator" |
! colspan="3" | plural<br /><<الْجَمْع>>
|-
! colspan="2" |
! 1<sup>st</sup> person<br /><<الْمُتَكَلِّم>>
! 2<sup>nd</sup> person<br /><<الْمُخَاطَب>>
! 3<sup>rd</sup> person<br /><<الْغَائِب>>
! 1<sup>st</sup> person<br /><<الْمُتَكَلِّم>>
! 2<sup>nd</sup> person<br /><<الْمُخَاطَب>>
! 3<sup>rd</sup> person<br /><<الْغَائِب>>
! 1<sup>st</sup> person<br /><<الْمُتَكَلِّم>>
! 2<sup>nd</sup> person<br /><<الْمُخَاطَب>>
! 3<sup>rd</sup> person<br /><<الْغَائِب>>
|-
! rowspan="2" | past indicative<br />(perfect)<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_pass_1s}
| {past_pass_2ms}
| {past_pass_3ms}
| rowspan="2" | {past_pass_1d}
| rowspan="2" | {past_pass_2d}
| {past_pass_3md}
| rowspan="2" | {past_pass_1p}
| {past_pass_2mp}
| {past_pass_3mp}
|-
! class="secondary" | f
| {past_pass_2fs}
| {past_pass_3fs}
| {past_pass_3fd}
| {past_pass_2fp}
| {past_pass_3fp}
|-
! rowspan="2" | non-past indicative<br />(imperfect)<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_pass_1s}
| {ind_pass_2ms}
| {ind_pass_3ms}
| rowspan="2" | {ind_pass_1d}
| rowspan="2" | {ind_pass_2d}
| {ind_pass_3md}
| rowspan="2" | {ind_pass_1p}
| {ind_pass_2mp}
| {ind_pass_3mp}
|-
! class="secondary" | f
| {ind_pass_2fs}
| {ind_pass_3fs}
| {ind_pass_3fd}
| {ind_pass_2fp}
| {ind_pass_3fp}
|-
! rowspan="2" | subjunctive<br /><<الْمُضَارِع الْمَنْصُوب>>
! class="secondary" | m
| rowspan="2" | {sub_pass_1s}
| {sub_pass_2ms}
| {sub_pass_3ms}
| rowspan="2" | {sub_pass_1d}
| rowspan="2" | {sub_pass_2d}
| {sub_pass_3md}
| rowspan="2" | {sub_pass_1p}
| {sub_pass_2mp}
| {sub_pass_3mp}
|-
! class="secondary" | f
| {sub_pass_2fs}
| {sub_pass_3fs}
| {sub_pass_3fd}
| {sub_pass_2fp}
| {sub_pass_3fp}
|-
! rowspan="2" | jussive<br /><<الْمُضَارِع الْمَجْزُوم>>
! class="secondary" | m
| rowspan="2" | {juss_pass_1s}
| {juss_pass_2ms}
| {juss_pass_3ms}
| rowspan="2" | {juss_pass_1d}
| rowspan="2" | {juss_pass_2d}
| {juss_pass_3md}
| rowspan="2" | {juss_pass_1p}
| {juss_pass_2mp}
| {juss_pass_3mp}
|-
! class="secondary" | f
| {juss_pass_2fs}
| {juss_pass_3fs}
| {juss_pass_3fd}
| {juss_pass_2fp}
| {juss_pass_3fp}
]=]
end
text = text .. m_inflection_table.make_bottom{
notes = '{footnote}',
}
local forms = alternant_multiword_spec.forms
if not alternant_multiword_spec.lemmas then
forms.title = "—"
else
local linked_lemmas = {}
for _, form in ipairs(alternant_multiword_spec.lemmas) do
table.insert(linked_lemmas, link_term(form.form, "term"))
end
forms.title = concat(linked_lemmas, ", ")
end
local ann_parts = {}
if alternant_multiword_spec.annotation ~= "" then
table.insert(ann_parts, alternant_multiword_spec.annotation)
end
if alternant_multiword_spec.vn then
local linked_vns = {}
for _, form in ipairs(alternant_multiword_spec.vn) do
table.insert(linked_vns, link_term(form.form, "term"))
end
table.insert(ann_parts, (#linked_vns > 1 and "verbal nouns" or "verbal noun") .. " " ..
concat(linked_vns, ", "))
end
local annotation = concat(ann_parts, ", ")
if annotation ~= "" then
forms.title = forms.title .. " (" .. annotation .. ")"
end
-- Format the table.
local tagged_table = rsub(text, "<<(.-)>>", tag_text)
return m_string_utilities.format(tagged_table, forms)
end
-------------------------------------------------------------------------------
-- External entry points --
-------------------------------------------------------------------------------
-- Append two lists `l1` and `l2`, removing duplicates. If either is {nil}, just return the other.
local function combine_lists(l1, l2)
-- combine_footnotes() does exactly what we want.
return iut.combine_footnotes(l1, l2)
end
local function combine_metadata(data)
local src1 = data.form1
local src2 = data.form2
local dest = data.dest_form
dest.uncertain = src1.uncertain or src2.uncertain
if src1.genders and src2.genders and not m_table.deepEquals(src1.genders, src2.genders) then
-- do nothing
else
dest.genders = src1.genders or src2.genders
end
if src1.pos and src2.pos and src1.pos ~= src2.pos then
-- do nothing
else
dest.pos = src1.pos or src2.pos
end
-- Don't copy .alt, .gloss, .lit, .id, which describe a single term and don't extend to multiword terms.
dest.q = combine_lists(src1.q, src2.q)
dest.qq = combine_lists(src1.qq, src2.qq)
dest.l = combine_lists(src1.l, src2.l)
dest.ll = combine_lists(src1.ll, src2.ll)
end
-- Externally callable function to parse and conjugate a verb given user-specified arguments.
-- Return value is WORD_SPEC, an object where the conjugated forms are in `WORD_SPEC.forms`
-- for each slot. If there are no values for a slot, the slot key will be missing. The value
-- for a given slot is a list of objects {form=FORM, footnotes=FOOTNOTES}.
function export.do_generate_forms(args, source_template, headword_head)
local PAGENAME = mw.loadData("Module:headword/data").pagename
local function in_template_space()
return mw.title.getCurrentTitle().nsText == "Templat"
end
-- Determine the verb spec we're being asked to generate the conjugation of. This may be taken from the current page
-- title or the value of |pagename=; but not when called from {{ar-verb form}}, where the page title is a
-- non-lemma form. Note that the verb spec may omit the lemma; e.g. it may be "<II>". For this reason, we use the
-- value of `pagename` computed here down below, when calling normalize_all_lemmas().
local pagename = source_template ~= "ar-verb form" and args.pagename or PAGENAME
local head = headword_head or pagename
local arg1 = args[1]
if not arg1 then
if (pagename == "ar-conj" or pagename == "ar-kk" or pagename == "ar-verb form") and in_template_space() then
arg1 = "كتب<I/a~u.pass>"
else
arg1 = "<>"
end
end
-- When called from {{ar-verb form}}, determine the non-lemma form whose inflections we're being asked to
-- determine. This normally comes from the page title or the value of |pagename=.
local verb_form_of_form
if source_template == "ar-verb form" then
verb_form_of_form = args.pagename
if not verb_form_of_form then
if PAGENAME == "ar-verb form" and in_template_space() then
verb_form_of_form = "كتبت"
else
verb_form_of_form = PAGENAME
end
end
end
local incorporated_headword_head_into_lemma = false
if arg1:find("^<.*>$") then -- missing lemma
if head:find(" ") then
-- If multiword lemma, try to add arg spec after the first word.
-- Try to preserve the brackets in the part after the verb, but don't do it
-- if there aren't the same number of left and right brackets in the verb
-- (which means the verb was linked as part of a larger expression).
local first_word, post = rmatch(head, "^(.-)( .*)$")
local left_brackets = rsub(first_word, "[^%[]", "")
local right_brackets = rsub(first_word, "[^%]]", "")
if #left_brackets == #right_brackets then
arg1 = iut.remove_redundant_links(first_word) .. arg1 .. post
incorporated_headword_head_into_lemma = true
else
-- Try again using the form without links.
local linkless_head = m_links.remove_links(head)
if linkless_head:find(" ") then
first_word, post = rmatch(linkless_head, "^(.-)( .*)$")
arg1 = first_word .. arg1 .. post
else
error("Unable to incorporate <...> spec into explicit head due to a multiword linked verb or " ..
"unbalanced brackets; please include <> explicitly: " .. arg1)
end
end
else
-- Will be incorporated through `head` below in the call to normalize_all_lemmas().
incorporated_headword_head_into_lemma = true
end
end
local parse_props = {
parse_indicator_spec = parse_indicator_spec,
angle_brackets_omittable = true,
allow_blank_lemma = true,
}
local alternant_multiword_spec = iut.parse_inflected_text(arg1, parse_props)
alternant_multiword_spec.pos = pos or "verbs"
alternant_multiword_spec.args = args
alternant_multiword_spec.source_template = source_template
alternant_multiword_spec.verb_form_of_form = verb_form_of_form
alternant_multiword_spec.incorporated_headword_head_into_lemma = incorporated_headword_head_into_lemma
normalize_all_lemmas(alternant_multiword_spec, head)
detect_all_indicator_specs(alternant_multiword_spec)
local inflect_props = {
lang = lang,
slot_list = alternant_multiword_spec.verb_slots,
inflect_word_spec = conjugate_verb,
combine_metadata = combine_metadata,
-- We add links around the generated verbal forms rather than allow the entire multiword
-- expression to be a link, so ensure that user-specified links get included as well.
include_user_specified_links = true,
}
iut.inflect_multiword_or_alternant_multiword_spec(alternant_multiword_spec, inflect_props)
if debug_translit then
for slot, forms in pairs(alternant_multiword_spec.forms) do
for _, form in ipairs(forms) do
if form.translit then
local full_form_translit = (lang:transliterate(m_links.remove_links(form.form)))
if full_form_translit ~= form.translit then
error(("Internal error: For slot '%s', form '%s' incremental translit '%s' not same as full translit '%s'"):
format(slot, form.form, form.translit, full_form_translit))
end
end
form.form = iut.remove_redundant_links(form.form)
end
end
end
-- Remove redundant brackets around entire forms.
for slot, forms in pairs(alternant_multiword_spec.forms) do
for _, form in ipairs(forms) do
form.form = iut.remove_redundant_links(form.form)
end
end
determine_slot_uncertainty_from_forms(alternant_multiword_spec)
determine_verb_properties_from_forms(alternant_multiword_spec)
compute_categories_and_annotation(alternant_multiword_spec)
if args.json and source_template == "ar-conj" then
-- There is a circular reference in `base.alternant_multiword_spec`, which points back to top level.
iut.map_word_specs(alternant_multiword_spec, function(base)
base.alternant_multiword_spec = nil
end)
return require("Module:JSON").toJSON(alternant_multiword_spec)
end
return alternant_multiword_spec
end
-- Entry point for {{ar-conj}}. Template-callable function to parse and conjugate a verb given
-- user-specified arguments and generate a displayable table of the conjugated forms.
function export.show(frame)
local parent_args = frame:getParent().args
local params = {
[1] = {},
["noautolinktext"] = {type = "boolean"},
["noautolinkverb"] = {type = "boolean"},
["t"] = {}, -- for use by {{ar-verb form}}; otherwise ignored
["id"] = {}, -- for use by {{ar-verb form}}; otherwise ignored
["pagename"] = {}, -- for testing/documentation pages
["json"] = {type = "boolean"}, -- for bot use
}
local args = require("Module:parameters").process(parent_args, params)
local alternant_multiword_spec = export.do_generate_forms(args, "ar-conj")
if type(alternant_multiword_spec) == "string" then
-- JSON return value
return alternant_multiword_spec
end
show_forms(alternant_multiword_spec)
return make_table(alternant_multiword_spec) ..
require("Module:utilities").format_categories(alternant_multiword_spec.categories, lang, nil, nil, force_cat)
end
function export.verb_forms(frame)
local parargs = frame:getParent().args
local params = {
[1] = {},
[2] = {},
[3] = {},
[4] = {},
[5] = {},
pagename = {},
}
for _, form in ipairs(allowed_vforms) do
-- FIXME: We go up to 5 here. The code supports unlimited variants but it's unlikely we will ever see more than
-- 2.
for index = 1, 5 do
local prefix = index == 1 and form or form .. index
params[prefix .. "-pv"] = {}
for _, extn in ipairs { "", "-vn", "-ap", "-pp" } do
params[prefix .. extn] = {}
params[prefix .. extn .. "-head"] = {}
-- FIXME: No -tr?
params[prefix .. extn .. "-gloss"] = {}
end
end
end
local args = require("Module:parameters").process(parargs, params)
local i = 1
local past_vowel_re = "^[aui,]*$"
local combined_root = nil
if not args[i] or rfind(args[i], past_vowel_re) then
combined_root = args.pagename or mw.loadData("Module:headword/data").pagename
if not rfind(combined_root, "^([^ ]) ([^ ]) ([^ ])$") and not
rfind(combined_root, "^([^ ]) ([^ ]) ([^ ]) ([^ ])$") then
error("When inferring roots from page title, need three or four space-separated radicals: " .. combined_root)
end
elseif rfind(args[i], " ") then
combined_root = args[i]
i = i + 1
else
local separate_roots = {}
while args[i] and not rfind(args[i], past_vowel_re) do
table.insert(separate_roots, args[i])
i = i + 1
end
combined_root = concat(separate_roots, " ")
end
local past_vowel = args[i]
i = i + 1
if past_vowel and not rfind(past_vowel, past_vowel_re) then
error("Unrecognized past vowel, should be 'a', 'i', 'u', 'a,u', etc. or empty: " .. past_vowel)
end
-- Spaces interfere with parsing as a unit in [[Module:inflection utilities]], so replace with underscore.
combined_root = combined_root:gsub(" ", "_")
local split_root = rsplit(combined_root, "_")
-- Map from verb forms (I, II, etc.) to a table of verb properties,
-- which has entries e.g. for "verb" (either true to autogenerate the verb
-- head, or an explicitly specified verb head using e.g. argument "I-head"),
-- and for "verb-gloss" (which comes from e.g. the argument "I" or "I-gloss"),
-- and for "vn" and "vn-gloss", "ap" and "ap-gloss", "pp" and "pp-gloss".
local verb_properties = {}
for _, form in ipairs(allowed_vforms) do
local formpropslist = {}
local derivs = {{"verb", ""}, {"vn", "-vn"}, {"ap", "-ap"}, {"pp", "-pp"}}
local index = 1
while true do
local formprops = {}
local prefix = index == 1 and form or form .. index
if prefix == "I" then
formprops.pv = past_vowel
end
if args[prefix .. "-pv"] then
formprops.pv = args[prefix .. "-pv"]
end
for _, deriv in ipairs(derivs) do
local prop = deriv[1]
local extn = deriv[2]
if args[prefix .. extn] == "+" then
formprops[prop] = true
elseif args[prefix .. extn] == "-" then
formprops[prop] = false
elseif args[prefix .. extn] then
formprops[prop] = true
formprops[prop .. "-gloss"] = args[prefix .. extn]
end
if args[prefix .. extn .. "-head"] then
if formprops[prop] == nil then
formprops[prop] = true
end
formprops[prop] = args[prefix .. extn .. "-head"]
end
if args[prefix .. extn .. "-gloss"] then
if formprops[prop] == nil then
formprops[prop] = true
end
formprops[prop .. "-gloss"] = args[prefix .. extn .. "-gloss"]
end
end
if formprops.verb then
-- If a verb form specified, also turn on vn (unless form I, with
-- unpredictable vn) and ap, and maybe pp, according to form,
-- weakness and past vowel. But don't turn these on if there's
-- an explicit on/off specification for them (e.g. I-pp=-).
if form ~= "I" and formprops.vn == nil then
formprops.vn = true
end
if formprops.ap == nil then
formprops.ap = true
end
local weakness = weakness_from_radicals(form, split_root[1], split_root[2], split_root[3],
split_root[4])
if formprops.pp == nil and not vform_probably_no_passive(form,
weakness, rsplit(formprops.pv or "", ","), {}) then
formprops.pp = true
end
if formprops.verb == true or formprops.vn == true or formprops.ap == true or formprops.pp == true then
formprops.need_autogen = true
end
table.insert(formpropslist, formprops)
index = index + 1
else
break
end
end
table.insert(verb_properties, {form, formpropslist})
end
-- Go through and create the verb form derivations as necessary, when they haven't been explicitly given.
for _, vplist in ipairs(verb_properties) do
local vform = vplist[1]
for _, props in ipairs(vplist[2]) do
if props.need_autogen then
local form_with_vowels
if vform == "I" then
local pv = props.pv
if not pv then
-- Make up likely past vowels based on weakness and actual radical.
if split_root[3] == W then -- final-weak
form_with_vowels = "I/a~u"
elseif split_root[3] == Y then
form_with_vowels = "I/a~i"
elseif split_root[2] == W then --hollow
form_with_vowels = "I/u~u"
elseif split_root[2] == Y then
form_with_vowels = "I/i~i"
else
-- most common; doesn't matter so much since we're not displaying the non-past
form_with_vowels = "I/a~u"
end
else
local pvs = rsplit(pv, ",")
local vowel_sufs = {}
for _, pv in ipairs(pvs) do
local vowel_spec
if pv == "a" then
-- Make up likely past vowels based on weakness and actual radical.
if split_root[3] == W then -- final-weak
vowel_spec = "a~u"
elseif split_root[3] == Y then
vowel_spec = "a~i"
elseif split_root[2] == W then --hollow
vowel_spec = "a~u"
elseif split_root[2] == Y then
vowel_spec = "a~i"
else
-- most common; doesn't matter so much since we're not displaying the non-past
vowel_spec = "a~u"
end
elseif pv == "i" then
-- most common; doesn't matter so much since we're not displaying the non-past
vowel_spec = "i~a"
elseif pv == "u" then
-- most common; doesn't matter so much since we're not displaying the non-past
vowel_spec = "u~u"
else
error(("Internal error: Bad past vowel '%s' in {{ar-verb forms}}"):format(pv))
end
table.insert(vowel_sufs, vowel_spec)
end
form_with_vowels = "I/" .. concat(vowel_sufs, "/")
end
else
form_with_vowels = vform
end
local angle_bracket_spec = ("%s<%s.pass>"):format(combined_root, form_with_vowels)
local alternant_multiword_spec = export.do_generate_forms({angle_bracket_spec}, "ar-verb forms")
local function format_forms(forms)
if not forms then
return "-" -- FIXME: Throw an error?
end
local formatted = {}
for _, form in ipairs(forms) do
if form.translit then
table.insert(formatted, ("%s//%s"):format(form.form, form.translit))
else
table.insert(formatted, form.form)
end
end
return concat(formatted, ",")
end
if props.verb == true then
props.verb = format_forms(alternant_multiword_spec.forms.past_3ms)
end
for _, deriv in ipairs({"vn", "ap", "pp"}) do
if props[deriv] == true then
props[deriv] = format_forms(alternant_multiword_spec.forms[deriv])
end
end
end
end
end
-- Go through and output the result
local formtextarr = {}
for _, vplist in ipairs(verb_properties) do
local form = vplist[1]
for _, props in ipairs(vplist[2]) do
local textarr = {}
if props.verb then
local text = "* '''[[Lampiran:Kata kerja bahasa Arab#Bentuk " .. form .. "|Bentuk " .. form .. "]]''': "
local linktext = {}
local splitheads = rsplit(props.verb, "[,،]")
for _, head in ipairs(splitheads) do
table.insert(linktext, m_links.full_link({lang = lang, term = head, gloss = props["verb-gloss"]}))
end
text = text .. concat(linktext, ", ")
table.insert(textarr, text)
for _, derivengl in ipairs({{"vn", "Kata nama kerjaan"}, {"ap", "Active participle"}, {"pp", "Passive participle"}}) do
local deriv = derivengl[1]
local engl = derivengl[2]
if props[deriv] then
local text = "** " .. engl .. ": "
local linktext = {}
local splitheads = rsplit(props[deriv], "[,،]")
for _, head in ipairs(splitheads) do
local ar, translit = head:match("^(.*)//(.-)$")
if not ar then
ar = head
end
table.insert(linktext, m_links.full_link {lang = lang, term = ar, tr = translit,
gloss = props[deriv .. "-gloss"]} )
end
text = text .. concat(linktext, ", ")
table.insert(textarr, text)
end
end
table.insert(formtextarr, concat(textarr, "\n"))
end
end
end
return concat(formtextarr, "\n")
end
-- Infer radicals from lemma headword (i.e. 3rd masculine singular past) and verb form (I, II, etc.). Throw an error if
-- headword is malformed. A given returned radical may be actually be a list of possible radicals, where the first one
-- should be used if the user didn't explicitly give the radical. If the list contains a field `ambig = true`, the
-- radical is considered ambiguous and should not be categorized. `is_reduced` indicates that the user specified
-- `.reduced` to indicate that the verb form is reduced by assimilation and/or haplology (typically archaic Koranic
-- forms such as اِدَّارَأَ instead of تَدَارَأَ; or اِسْطَاعَ instead of اِسْتِطَاعَ; etc.
function export.infer_radicals(data)
local headword, vform, passive, past_vowel, nonpast_vowel, is_reduced =
data.headword, data.vform, data.passive, data.past_vowel, data.nonpast_vowel, data.is_reduced
past_vowel = past_vowel or "-"
nonpast_vowel = nonpast_vowel or "-"
local function verify_vowel(vowel, param)
if vowel ~= A and vowel ~= I and vowel ~= U and vowel ~= "-" then
error(("Internal error: Bad value for %s: %s (should be Arabic diacritic vowel or '-')"):format(
param, vowel))
end
end
verify_vowel(past_vowel, "past_vowel")
verify_vowel(nonpast_vowel, "nonpast_vowel")
local ch = {}
local form_viii_assim, variant
-- sub out alif-madda for easier processing
headword = rsub(headword, AMAD, HAMZA .. ALIF)
local function infer_err(msg, noann)
local anns = {}
local nohead, novform
if noann == "nohead" then
nohead = true
elseif noann == "novform" then
novform = true
elseif noann == "nohead-vform" then
nohead = true
novform = true
elseif noann then
error(("Internal error: Unrecognized value for 'noann': %s"):format(dump(noann)))
end
if not nohead then
table.insert(anns, ("headword=%s"):format(data.headword))
end
if not novform then
table.insert(anns, ("verb form=%s"):format(data.vform))
end
anns = concat(anns, ", ")
if anns ~= "" then
anns = ": " .. anns
end
error(msg .. anns)
end
local len = ulen(headword)
local expected_length
-- extract the headword letters into an array
for i = 1, len do
table.insert(ch, usub(headword, i, i))
end
-- check that the letter at the given index is the given string, or
-- is one of the members of the given array
local function check(index, must)
local letter = ch[index]
if type(must) == "string" then
if not letter then
infer_err("Letter " .. index .. " is nil")
end
if letter ~= must then
infer_err(("For verb form %s, letter %s must be %s, not %s"):format(vform, index, must, letter),
"novform")
end
elseif not m_table.contains(must, letter) then
infer_err("For verb form " .. vform .. ", radical " .. index ..
" must be one of " .. concat(must, " ") .. ", not " .. letter, "novform")
end
end
-- Check that length of headword is within [min, max]
local function check_len(min, max)
if min and len < min then
infer_err(("Not enough letters for verb form %s, expected at least %s"):format(vform, min), "novform")
end
if max and len > max then
infer_err(("Too many letters for verb form %s, expected at most %s"):format(vform, max), "novform")
end
end
-- If the vowels are i~a or u~u, a form I verb beginning with w- normally keeps the w in the non-past. Otherwise it
-- loses it (i.e. it is "assimilated").
local function form_I_w_non_assimilated()
return req(past_vowel, I) and req(nonpast_vowel, A) or req(past_vowel, U) and req(nonpast_vowel, U)
end
-- Convert radicals to canonical form (handle various hamza varieties and check for misplaced alif or alif maqṣūra;
-- legitimate cases of these letters are handled above).
local function convert(rad, index)
if type(rad) == "table" then
for i, r in ipairs(rad) do
rad[i] = convert(r, index)
end
return rad
elseif rad == HAMZA_ON_ALIF or rad == HAMZA_UNDER_ALIF or
rad == HAMZA_ON_W or rad == HAMZA_ON_Y then
return HAMZA
elseif rad == AMAQ then
infer_err("Radical " .. index .. " must not be alif maqṣūra")
elseif rad == ALIF then
infer_err("Radical " .. index .. " must not be alif")
else
return rad
end
end
local quadlit = vform:find("q$")
-- find first radical, start of second/third radicals, check for
-- required letters
local radstart, rad1, rad2, rad3, rad4
local weakness
if vform == "I" or vform == "II" then
rad1 = ch[1]
radstart = 2
elseif vform == "III" then
rad1 = ch[1]
check(2, {ALIF, W}) -- W occurs in passive-only verbs
radstart = 3
elseif vform == "IV" then
-- this would be alif-madda but we replaced it with hamza-alif above.
if ch[1] == HAMZA and ch[2] == ALIF then
rad1 = HAMZA
else
check(1, HAMZA_ON_ALIF)
rad1 = ch[2]
end
radstart = 3
elseif vform == "V" then
check(1, is_reduced and ALIF or T)
rad1 = ch[2]
radstart = 3
elseif vform == "VI" then
check(1, is_reduced and ALIF or T)
if ch[2] == AMAD then
rad1 = HAMZA
radstart = 3
else
rad1 = ch[2]
check(3, {ALIF, W}) -- W occurs in passive-only verbs
radstart = 4
end
elseif vform == "VII" then
check(1, ALIF)
if is_reduced then
check(2, M)
rad1 = M
radstart = 3
else
check(2, N)
rad1 = ch[3]
radstart = 4
end
elseif vform == "VIII" then
check(1, ALIF)
rad1 = ch[2]
if rad1 == "د" then
rad1 = {"د", "ذ"} -- not considered ambiguous since it's usually د
radstart = 3
form_viii_assim = "دّ"
elseif rad1 == "ظ" and ch[3] == "ط" and len >= 5 then
-- [[اظطلم]], variant of [[اظلم]]
radstart = 4
form_viii_assim = "ظْط"
elseif rad1 == "ذ" and ch[3] == "د" and len >= 5 then
-- [[اذدكر]], variant of [[اذكر]]
radstart = 4
form_viii_assim = "ذْد"
elseif rad1 == T or rad1 == "ث" or rad1 == "ذ" or rad1 == "ط" or rad1 == "ظ" then
radstart = 3
form_viii_assim = rad1 .. SH
elseif rad1 == "ز" then
check(3, "د")
radstart = 4
form_viii_assim = "زْد"
elseif rad1 == "ص" or rad1 == "ض" then
check(3, "ط")
radstart = 4
form_viii_assim = rad1 .. SK .. "ط"
else
check(3, T)
radstart = 4
rad1 = convert(rad1, 1)
form_viii_assim = rad1 .. SK .. "ت"
end
if rad1 == T then
-- Radical is ambiguous, might be ت or و or ي but doesn't affect conjugation. Note that there are no
-- form-VIII verbs with initial radical ي given in Hans Wehr but Lane mentions at least:
-- - (page 2973) اِتَّأَسَ, with assimilation of the ي to ت, from root ي ء س;
-- - (page 2975) اِتَّبَسَ non-past يَتَّبِسُ and alternative اِيتَبَسَ non-past يَاتَبِسُ from the root ي ب س;
-- - (page 2976) اِتَّسَرَ non-past يَتَّسِرُ or alternatively يَأْتَسِرُ with hamza preserved from the root ي س ر.
-- These alternative forms seem very rare and probably not worth worrying about, but if we want to handle
-- them, we can do it when the time comes.
rad1 = {T, W, Y, ambig = true}
-- اِتَّخَذَ irregularly has hamza as the radical but assimilates like و
if ch[3] == "خ" and ch[4] == "ذ" then
rad1[4] = HAMZA
end
end
elseif vform == "IX" then
check(1, ALIF)
rad1 = ch[2]
radstart = 3
elseif vform == "X" then
check(1, ALIF)
check(2, S)
if is_reduced then
rad1 = ch[3]
radstart = 4
else
check(3, T)
rad1 = ch[4]
radstart = 5
end
elseif vform == "Iq" then
rad1 = ch[1]
rad2 = ch[2]
radstart = 3
elseif vform == "IIq" then
check(1, T)
rad1 = ch[2]
rad2 = ch[3]
radstart = 4
elseif vform == "IIIq" then
check(1, ALIF)
rad1 = ch[2]
rad2 = ch[3]
check(4, N)
radstart = 5
elseif vform == "IVq" then
check(1, ALIF)
rad1 = ch[2]
rad2 = ch[3]
radstart = 4
elseif vform == "XI" then
check_len(5, 5)
check(1, ALIF)
rad1 = ch[2]
rad2 = ch[3]
check(4, ALIF)
rad3 = ch[5]
weakness = "sound"
elseif vform == "XII" then
check(1, ALIF)
rad1 = ch[2]
if ch[3] ~= ch[5] then
infer_err("For verb form XII, letters 3 and 5 should be the same", "novform")
end
check(4, W)
radstart = 5
elseif vform == "XIII" then
check_len(5, 5)
check(1, ALIF)
rad1 = ch[2]
rad2 = ch[3]
check(4, W)
rad3 = ch[5]
if rad3 == AMAQ then
weakness = "final-weak"
else
weakness = "sound"
end
elseif vform == "XIV" then
check_len(6, 6)
check(1, ALIF)
rad1 = ch[2]
rad2 = ch[3]
check(4, N)
rad3 = ch[5]
if ch[6] == AMAQ then
check_waw_ya(rad3)
weakness = "final-weak"
else
if ch[5] ~= ch[6] then
infer_err("For verb form XIV, letters 5 and 6 should be the same", "novform")
end
weakness = "sound"
end
elseif vform == "XV" then
check_len(6, 6)
check(1, ALIF)
rad1 = ch[2]
rad2 = ch[3]
check(4, N)
rad3 = ch[5]
if rad3 == Y then
check(6, ALIF)
else
check(6, AMAQ)
end
weakness = "sound"
else
error("Internal error: Unrecognized verb form " .. vform)
end
-- Process the last two radicals. RADSTART is the index of the first of the two. If it's nil then all radicals have
-- already been processed above, and we don't do anything.
if radstart then
-- There must (normally) be one or two letters left.
if len == radstart then
if vform == "I" and ch[len] == Y then
-- short form حَيَّ
weakness = "final-weak"
rad2 = Y
rad3 = Y
variant = "short"
elseif vform == "IV" and rad1 == "ر" and ch[len] == AMAQ then
-- irregular verb أَرَى
weakness = "final-weak"
rad2 = HAMZA
rad3 = Y
elseif vform == "X" and rad1 == "ح" and ch[len] == AMAQ then
-- irregular verb اِسْتَحَى
weakness = "final-weak"
rad2 = Y
rad3 = Y
variant = "short"
else
-- If one letter left, then it's a geminate verb. If the letter is alif or alif maqṣūra, it will trigger
-- an error down the line.
if vform_supports_geminate(vform) then
weakness = "geminate"
rad2 = ch[len]
rad3 = ch[len]
if vform == "III" or vform == "VI" then
variant = "short"
end
else
infer_err("Apparent geminate verb, but geminate verbs not allowed for this verb form")
end
end
elseif quadlit then
-- Process last two radicals of a quadriliteral verb form.
rad3 = ch[radstart]
rad4 = ch[radstart + 1]
expected_length = radstart + 1
check_len(expected_length)
if rad4 == AMAQ or rad4 == ALIF and rad3 == Y or rad4 == Y then
-- rad4 can be Y in passive-only verbs.
if vform_supports_final_weak(vform) then
weakness = "final-weak"
-- Ambiguous radical; randomly pick wāw as radical (but avoid two wāws in a row); it could be wāw or
-- yāʾ, but doesn't affect the conjugation.
rad4 = rad3 == W and {Y, W, ambig = true} or {W, Y, ambig = true}
else
infer_err("Last radical is " .. rad4 .. " but verb form " .. vform ..
" doesn't support final-weak verbs", "novform")
end
else
weakness = "sound"
end
else
-- Process last two radicals of a triliteral verb form.
rad2 = ch[radstart]
rad3 = ch[radstart + 1]
expected_length = radstart + 1
check_len(expected_length)
if vform == "I" and (is_waw_ya(rad3) or rad3 == ALIF or rad3 == AMAQ) then
local inferred_past_vowel, inferred_nonpast_vowel
-- Check for final-weak form I verb. It can end in tall alif (rad3 = wāw) or alif maqṣūra (rad3 = yāʾ)
-- or a wāw or yāʾ (with a past vowel of i or u, e.g. nasiya/yansā "forget" or with a passive-only
-- verb).
if rad1 == W and not form_I_w_non_assimilated() then
weakness = "assimilated+final-weak"
else
weakness = "final-weak"
end
if rad3 == ALIF then
rad3 = W
inferred_past_vowel = A
inferred_nonpast_vowel = U
if is_passive_only(passive) then
infer_err("Final-weak form-I passive verbs should end in yāʔ (ي), not tall alif (ا)", "novform")
end
elseif rad3 == AMAQ then
rad3 = Y
inferred_past_vowel = A
inferred_nonpast_vowel = I
if is_passive_only(passive) then
infer_err("Final-weak form-I passive verbs should end in yāʔ (ي), not alif maqṣūra (ى)",
"novform")
end
elseif rad1 == "ح" and rad2 == Y and rad3 == Y then
-- Long variant حَيِيَ.
inferred_past_vowel = I
inferred_nonpast_vowel = A
variant = "long"
else
if not is_passive_only(passive) then
-- does a non-passive final-weak verb in -uwa ever happen? (YES: e.g. [[رجو]] "to be slack")
inferred_past_vowel = rad3 == Y and I or U
inferred_nonpast_vowel = A
end
-- Ambiguous radical; randomly pick wāw as radical (but avoid two wāws); it could be wāw or yāʾ, but
-- doesn't affect the conjugation.
rad3 = (rad1 == W or rad2 == W) and {Y, W, ambig = true} or {W, Y, ambig = true} -- ambiguous
end
if inferred_past_vowel then
local raw_past_vowel = rget(past_vowel)
local raw_nonpast_vowel = rget(nonpast_vowel)
if raw_past_vowel ~= "-" then
if raw_past_vowel ~= inferred_past_vowel then
infer_err(("Final-weak form-I verb inferred past vowel %s, which disagrees with " ..
"explicitly specified %s"):format(undia[inferred_past_vowel], undia[raw_past_vowel]), "novform")
else
-- in case of footnote in past_vowel
inferred_past_vowel = past_vowel
end
end
if raw_nonpast_vowel ~= "-" and raw_nonpast_vowel ~= A and inferred_nonpast_vowel == U then
-- if inferred as I or A, the reality can be the reverse; form-I final-weak verbs with a~a and
-- i~i exist, e.g. سَعَى/يَسْعَى, وَلِيَ/يَلِي. Weird verb [[صها]] (also written [[صهى]]) has non-past
-- يصهى so we can't throw an error in this situation.
if raw_nonpast_vowel ~= inferred_nonpast_vowel then
infer_err(("Final-weak form-I verb inferred non-past vowel %s, which disagrees with " ..
"explicitly specified %s"):format(undia[inferred_nonpast_vowel], undia[raw_nonpast_vowel]), "novform")
else
-- in case of footnote in nonpast_vowel
inferred_nonpast_vowel = nonpast_vowel
end
end
end
if not is_passive_only(passive) then
if rget(past_vowel) == "-" then
past_vowel = inferred_past_vowel
end
if rget(nonpast_vowel) == "-" then
nonpast_vowel = inferred_nonpast_vowel
end
end
elseif vform == "IX" and is_waw_ya(rad3) and len == radstart + 2 and ch[len] == AMAQ then
-- Final-weak form IX verbs like اِرْعَوَى "to desist, to repent, to see the light".
weakness = "final-weak"
expected_length = radstart + 2
elseif vform == "X" and rad1 == "ح" and rad2 == Y and rad3 == ALIF then
-- Long variant اِسْتَحْيَا.
weakness = "final-weak"
rad3 = Y
variant = "long"
elseif rad3 == AMAQ or rad2 == Y and rad3 == ALIF or rad3 == Y then
-- rad3 == Y happens in passive-only verbs.
if vform_supports_final_weak(vform) then
weakness = "final-weak"
else
infer_err("Last radical is " .. rad3 .. " but verb form doesn't support final-weak verbs")
end
-- Ambiguous radical; randomly pick wāw as radical (but avoid two wāws); it could be wāw or yāʾ, but
-- doesn't affect the conjugation.
rad3 = (rad1 == W or rad2 == W) and {Y, W, ambig = true} or {W, Y, ambig = true}
elseif rad2 == ALIF then
if vform_supports_hollow(vform) then
weakness = "hollow"
local function set_past_to_a()
if req(past_vowel, A) then
-- already set
elseif req(past_vowel, "-") or req(past_vowel, rget(nonpast_vowel)) then
past_vowel = A
else
infer_err(("Form I hollow verb with nonpast vowel set to '%s' must have past vowel set to 'a' or the same value, not %s"):
format(undia[rget(nonpast_vowel)], undia[rget(past_vowel)]), "novform")
end
end
if vform == "I" and req(nonpast_vowel, U) then
rad2 = W
set_past_to_a()
elseif vform == "I" and req(nonpast_vowel, I) then
rad2 = Y
set_past_to_a()
else
if req(nonpast_vowel, A) and not req(past_vowel, I) then
infer_err(("Form I hollow verb with nonpast vowel set to 'a' must have past vowel set to 'i', not %s"):
format(undia[rget(past_vowel)]), "novform")
end
-- Ambiguous radical; could be wāw or yāʾ; if verb form I, it's critical to get this right, and
-- the caller checks for this situation and throws an error if non-past vowel is "a" and second
-- radical isn't explicitly given.
rad2 = {W, Y, ambig = true, need_radical = true}
end
else
infer_err("Second radical is alif but verb form doesn't support hollow verbs")
end
elseif vform == "I" and rad1 == W and not form_I_w_non_assimilated() then
weakness = "assimilated"
elseif rad2 == rad3 and (vform == "III" or vform == "VI") then
weakness = "geminate"
variant = "long"
else
weakness = "sound"
end
end
if expected_length then
check_len(expected_length, expected_length)
end
end
rad1 = convert(rad1, 1)
rad2 = convert(rad2, 2)
rad3 = convert(rad3, 3)
rad4 = convert(rad4, 4)
if not weakness then
error("Internal error: Returned weakness from infer_radicals() is nil")
end
return {
weakness = weakness,
rad1 = rad1,
rad2 = rad2,
rad3 = rad3,
rad4 = rad4,
past_vowel = past_vowel,
nonpast_vowel = nonpast_vowel,
form_viii_assim = form_viii_assim,
variant = variant,
}
end
-- bot interface to infer_radicals()
function export.infer_radicals_json(frame)
local iparams = {
headword = {},
vform = {},
passive = {},
past_vowel = {},
nonpast_vowel = {},
is_reduced = {type = "boolean"},
}
local iargs = require("Module:parameters").process(frame.args, iparams)
return require("Module:JSON").toJSON(export.infer_radicals(iargs))
end
-- Infer vocalization from participle headword (active or passive), verb form (I, II, etc.) and whether the headword is
-- active or passive. Throw an error if headword is malformed. Returned radicals may contain Latin letters "t", "w" or "y"
-- indicating ambiguous radicals guessed to be tāʾ, wāw or yāʾ respectively.
function export.infer_participle_vocalization(headword, vform, weakness, is_active)
local chars = {}
local orig_headword = headword
-- Sub out alif-madda for easier processing.
headword = rsub(headword, AMAD, HAMZA .. ALIF)
local len = ulen(headword)
-- Extract the headword letters into an array.
for i = 1, len do
table.insert(chars, usub(headword, i, i))
end
local function form_intro_error_msg()
return ("For verb form %s %s%s participle %s, "):format(vform, orig_headword ~= headword and "normalized " or
"", is_active and "active" or "passive", headword)
end
local function err(msg)
error(form_intro_error_msg() .. msg, 1)
end
-- Check that length of headword is within [min, max].
local function check_len(min, max)
if min and len < min then
err(("expected at least %s letters but saw %s"):format(min, len))
elseif max and len > max then
err(("expected at most %s letters but saw %s"):format(max, len))
end
end
-- Get the character at `ind`, making sure it exists.
local function c(ind)
check_len(ind)
return chars[ind]
end
-- Check that the letter at the given index is the given string, or is one of the members of the given array
local function check(index, must)
local letter = chars[index]
local function make_possible_values()
if type(must) == "string" then
return must
else
return list_to_text(must, nil, " or ")
end
end
if not letter then
err(("expected a letter (specifically %s) at position %s, but participle is too short"):format(
make_possible_values(), index))
end
local matches
if type(must) == "string" then
matches = letter == must
else
matches = m_table.contains(must, letter)
end
if not matches then
err(("letter %s at index %s must be %s"):format(letter, index, make_possible_values()))
end
end
local function check_weakness(values, allow_missing, invert_condition)
local function make_possible_weaknesses()
for i, val in ipairs(values) do
values[i] = "'" .. val .. "'"
end
return list_to_text(values, nil, " or ")
end
if allow_missing and invert_condition then
error("Internal error: Can't specify both allow_missing and invert_condition")
end
if not weakness then
if allow_missing or invert_condition then
return
else
err(("weakness is unspecified but must be %s"):format(make_possible_weaknesses()))
end
else
local matches = m_table.contains(values, weakness)
if invert_condition and matches then
err(("weakness '%s' must not be %s"):format(weakness, make_possible_weaknesses()))
elseif not invert_condition and not matches then
err(("weakness '%s' must be %s"):format(weakness, make_possible_weaknesses()))
end
end
end
local vocalized
local function handle_possibly_final_weak(sound_prefix, expected_length)
check_len(expected_length, expected_length)
if c(expected_length) == AMAQ then
-- passive final-weak
if is_active then
err("participle in -ِى only allowed for passive participles")
end
check_weakness({"final-weak", "assimilated+final-weak"}, "allow missing")
vocalized = sound_prefix .. AN .. AMAQ
else
-- all others behave as if sound
check_weakness({"final-weak", "assimilated+final-weak"}, nil, "invert condition")
vocalized = sound_prefix .. (is_active and I or A) .. c(expected_length)
end
end
if not (vform == "I" and is_active) then
-- all participles except verb form I active begin in م-.
check(1, M)
end
if vform == "I" then
if is_active then
check(2, ALIF)
local sound_prefix = c(1) .. AA .. c(3)
if len == 3 then
if c(3) == HAMZA then
-- Either hollow with hamzated third radical, e.g. [[شاء]] active participle 'شَاءٍ', or final-weak
-- with hamzated second radical, e.g. [[رأى]] active participle 'رَاءٍ'. Theoretically (?), also
-- geminate with hamzated second/third radical, but I don't know if any such verbs exist.
if weakness == "geminate" then
vocalized = sound_prefix .. SH
else
check_weakness({"hollow", "final-weak"}, "allow missing")
vocalized = sound_prefix .. IN
end
else
check_weakness({"final-weak", "geminate"})
if weakness == "geminate" then
vocalized = sound_prefix .. SH
else
vocalized = sound_prefix .. IN
end
end
else
check_len(4, 4)
-- we will convert back to alif maqṣūra below as needed
vocalized = sound_prefix .. I .. c(4)
end
else
-- assimilated verbs: regular, e.g. مَوْزُون "weighed"
-- geminate verbs: regular, e.g. مَبْلُول "moistened"
-- third-hamzated verbs: مَبْرُوء
-- hollow verbs: مَقُود "led, driven"; مَزِيد "added, increased"
-- hollow first-hamzated verbs: مَئِيض "returned, reverted"; مَأْيُوس "despaired" (NOTE: formation is sound);
-- مَأُود or مَؤُود "bent; depleted"
-- hollow third-hamzated verbs: مَشِيء "willed, intended", مَضُوء "glittered?"
-- final-weak: مَلْقِيّ "found, encountered"; مَصْغُوّ "inclined"
-- hollow + final-weak: مَشْوِيّ "fried, grilled", مَهْوِيّ "loved"
-- first-hamzated + hollow + final-weak: مَأْوِيّ "received hospitably"
local sound_prefix = MA .. c(2) .. SK .. c(3)
if len == 5 then
-- sound, assimilated or geminate
check(4, W)
vocalized = sound_prefix .. UU .. c(5)
else
check_len(4, 4)
if c(4) == W then
-- final-weak third-wāw
vocalized = sound_prefix .. U .. W .. SH
elseif c(4) == Y then
-- final-weak third-yāʾ
vocalized = sound_prefix .. I .. Y .. SH
else
-- hollow
check(3, {W, Y})
if c(3) == W then
vocalized = MA .. c(2) .. UU .. c(4)
else
vocalized = MA .. c(2) .. II .. c(4)
end
end
end
end
elseif vform == "II" or vform == "V" or vform == "XII" or vform == "XIII" or vform == "Iq" or vform == "IIq" or
vform == "IIIq" then
local sound_prefix, expected_length
if vform == "II" then
sound_prefix = MU .. c(2) .. A .. c(3) .. SH
expected_length = 4
elseif vform == "V" then
check(2, T)
sound_prefix = MU .. T .. A .. c(3) .. A .. c(4) .. SH
expected_length = 5
elseif vform == "XII" then
-- e.g. [[احدودب]] "to be or become convex or humpbacked", مُحْدَوْدِب (active);
-- [[اثنونى]] "to be bent; to be doubled up", مُثْنَوْنٍ (active)
check(4, W)
if c(3) ~= c(5) then
err(("third letter %s should be the same as the fifth letter %s"):format(c(3), c(5)))
end
sound_prefix = MU .. c(2) .. SK .. c(3) .. A .. W .. SK .. c(5)
expected_length = 6
elseif vform == "XIII" then
-- e.g. [[اخروط]] "to get entangled; to extend", مُخْرَوِّط (active), مُخْرَوَّط (passive)
check(4, W)
sound_prefix = MU .. c(2) .. SK .. c(3) .. A .. W .. SH
expected_length = 5
elseif vform == "Iq" then
sound_prefix = MU .. c(2) .. A .. c(3) .. SK .. c(4)
expected_length = 5
elseif vform == "IIq" then
check(2, T)
sound_prefix = MU .. T .. A .. c(3) .. A .. c(4) .. SK .. c(5)
expected_length = 6
elseif vform == "IIIq" then
-- e.g. [[اخرنطم]] "to be proud and angry"
check(4, T)
sound_prefix = MU .. c(2) .. SK .. c(3) .. A .. N .. SK .. c(5)
expected_length = 6
else
error("Internal error: Unhandled verb form " .. vform)
end
if len == expected_length - 1 then
-- active final-weak
if not is_active then
err(("length-%s participle only allowed for active participles"):format(len))
end
check_weakness({"final-weak", "assimilated+final-weak"}, "allow missing")
vocalized = sound_prefix .. IN
else
handle_possibly_final_weak(sound_prefix, expected_length)
end
elseif vform == "III" or vform == "VI" then
local sound_prefix, expected_length
if vform == "VI" then
check(2, T)
check(4, ALIF)
sound_prefix = MU .. T .. A .. c(3) .. AA .. c(5)
expected_length = 6
else
sound_prefix = MU .. c(2) .. AA .. c(4)
expected_length = 5
end
if len == expected_length - 1 then
-- active final-weak or active or passive geminate
if is_active then
check_weakness({"geminate", "final-weak", "assimilated+final-weak"})
if weakness == "geminate" then
vocalized = sound_prefix .. SH
else
vocalized = sound_prefix .. IN
end
else
check_weakness({"geminate"}, "allow missing")
vocalized = sound_prefix .. SH
end
else
handle_possibly_final_weak(sound_prefix, expected_length)
end
elseif vform == "IV" or vform == "X" then
-- form IV:
-- sound: مُرْسِخ (active, "entrenching"), مُرْسَخ (passive, "entrenched")
-- first-hamzated (like sound): مُؤْيِس (active, "causing to despair"), مُؤْيَس (passive, "caused to despair")
-- final-weak: مُكْرٍ (active, "renting out"), مُكْرًى (passive, "rented out")
-- assimilated: مُورِد (active, "transferring"), مُورَد (passive, "transferred"); same when first-Y, e.g.
-- أَيْقَنَ "to be certain of": مُوقِن (active), مُوقَن (passive)
-- assimilated + final-weak: مُورٍ (active, "setting fire, kindling"), مُورًى (passive, "set fire, kindled")
-- geminate: مُمِدّ (active, "granting, helping"), مُمَدّ (passive, "granted, helped")
-- hollow: مُزِيل (active, "eliminating"), مُزَال (passive, "eliminated")
-- hollow + final-weak: مُعْيٍ (active, "tiring"), مُعْيًى (passive, "tired")
local sound_prefix, expected_length
if vform == "X" then
check(2, S)
check(3, T)
sound_prefix = MU .. S .. SK .. T .. A .. c(4)
expected_length = 6
else
sound_prefix = MU .. c(2)
expected_length = 4
end
if len == expected_length and c(len - 1) == Y and c(len) ~= AMAQ then
-- active hollow
if not is_active then
err("this shape only allowed for active participles")
end
check_weakness({"hollow"}, "allow missing")
vocalized = sound_prefix .. II .. c(len)
elseif len == expected_length and c(len - 1) == ALIF then
-- passive hollow
if is_active then
err("this shape only allowed for passive participles")
end
check_weakness({"hollow"}, "allow missing")
vocalized = sound_prefix .. AA .. c(len)
elseif len == expected_length - 1 then
-- active final-weak or active or passive geminate
if is_active then
check_weakness({"geminate", "final-weak", "assimilated+final-weak"})
if weakness == "geminate" then
vocalized = sound_prefix .. I .. c(len) .. SH
elseif vform == "IV" and c(2) == W then
-- assimilated final-weak
vocalized = sound_prefix .. c(len) .. IN
else
vocalized = sound_prefix .. SK .. c(len) .. IN
end
else
check_weakness({"geminate"}, "allow missing")
vocalized = sound_prefix .. A .. c(len) .. SH
end
else
if vform == "IV" and c(2) == W then
-- assimilated, possibly final-weak
sound_prefix = sound_prefix .. c(expected_length - 1)
else
sound_prefix = sound_prefix .. SK .. c(expected_length - 1)
end
handle_possibly_final_weak(sound_prefix, expected_length)
end
elseif vform == "VII" or vform == "VIII" then
-- form VII (passive participles are fairly rare but do exist):
-- sound: مُنْكَتِب (active "subscribing"), مُنْكَتَب (passive "subscribed")
-- geminate: مُنْضَمّ (both active "joining, containing" and passive "joined, contained")
-- final-weak: مُنْطَلٍ (active "fooling (someone)"), مُنْطَلًى (passive "fooled")
-- final-weak with medial wāw: مُنْطَوٍ (active "involving"), مُنْطَوًى (passive "involved")
-- hollow: مُنْقَاد (both active "complying with" and passive "complied with")
--
-- for form VIII, the same variants exist but things are complicated by assimilations involving the template T.
-- sound third-hamzated no assimilation: مُبْتَدِئ (active "beginning"), مُبْتَدَأ (passive "begun")
-- geminate no assimilation: مُبْتَزّ (both active "robbing" and passive "robbed")
-- final-weak no assimilation: مُبْتَنٍ (active "building"), مُبْتَنًى (passive "built")
-- final-weak with medial wāw no assimilation: مُحْتَوٍ (active "containing"), مُحْتَوًى (passive "contained")
-- hollow no assimilation: مُخْتَار (both active "choosing" and passive "chosen")
--
-- sound with total assimilation: مُتَّبِع (active "following"), مُتَّبَع (passive "followed")
-- sound with total assimilation, assimilating wāw: مُتَّعِد (active "threatening"), مُتَّعَد (passive "threatened")
-- sound with total assimilation, irregularly assimilating hamza: مُتَّخِذ (active "taking"), مُتَّخَذ (passive "taken")
-- sound with total assimilation (to ḏāl, producing dāl): مُدَّخِر (active "reserving"), مُدَّخَر (passive "reserved")
-- sound with total assimilation (to ḏāl): مُذَّكِر (active "remembering"), مُذَّكَر (passive "remembered")
-- sound with total assimilation (to ṭāʔ): مُطَّرِح (active "discarding"), مُطَّرَح (passive "discarded")
-- sound with total assimilation (to ẓāʔ): مُظَّلِم (active "tolerating"), مُظَّلَم (passive "tolerated")
-- final-weak with total assimilation, assimilating wāw: مُتَّقٍ (active "guarding against"), مُتَّقًى (passive "guarded against")
-- final-weak with total assimilation (to ṯāʔ): مُثَّنٍ (active "undulating"), مُثَّنًى (passive "undulated")
-- final-weak with total assimilation (to dāl): مُدَّعٍ (active "claiming"), مُدَّعًى (passive "claimed")
-- sound with partial assimilation (to zayn): مُزْدَهِر (active "thriving"), مُزْدَهَر (passive "thrived")
-- sound with medial wāw with partial assimilation (to zayn): مُزْدَوِج (active "appearing twice")
-- sound with partial assimilation (to ṣād): مُصْطَبِح (active "illuminating"), مُصْطَبَح (passive, "illuminated")
-- sound with partial assimilation (to ḍād): مُضْطَرِب (active "to be disturbed"; no passive)
-- geminate with partial assimilation (to ṣād): مُصْطَبّ (both active "effusing" and passive "effused")
-- geminate with partial assimilation (to ḍād): مُضْطَرّ (both active "forcing" and passive "forced")
-- final-weak with partial assimilation (to ṣād): مُصْطَلٍ (active "warming"), مُصْطَلًى (passive "warmed")
-- hollow with partial assimilation (to zayn): مُزْدَاد (both active "increasing" and passive "increased")
-- hollow with partial assimilation (to ṣad): مُصْطَاد (both active "hunting" and passive "hunted")
local sound_prefix, sufind
if vform == "VII" then
check(2, N)
sound_prefix = MU .. N .. SK .. c(3)
sufind = 4
else
local c2 = c(2)
if c2 == T or c2 == "د" or c2 == "ث" or c2 == "ذ" or c2 == "ط" or c2 == "ظ" then
-- full assimilation
sound_prefix = MU .. c2 .. SH
sufind = 3
else
-- partial or no assimilation
if c2 == "ز" then
check(3, "د")
elseif c2 == "ص" or c2 == "ض" then
check(3, "ط")
else
check(3, T)
end
sound_prefix = MU .. c2 .. SK .. c(3)
sufind = 4
end
end
if c(sufind) == ALIF then
-- hollow, active or passive
check_len(sufind + 1, sufind + 1)
check_weakness({"hollow"}, "allow missing")
vocalized = sound_prefix .. AA .. c(sufind + 1)
elseif len == sufind then
-- active final-weak or active or passive geminate
if is_active then
check_weakness({"geminate", "final-weak", "assimilated+final-weak"})
if weakness == "geminate" then
vocalized = sound_prefix .. A .. c(len) .. SH
else
vocalized = sound_prefix .. A .. c(len) .. IN
end
else
check_weakness({"geminate"}, "allow missing")
vocalized = sound_prefix .. A .. c(len) .. SH
end
else
sound_prefix = sound_prefix .. A .. c(sufind)
handle_possibly_final_weak(sound_prefix, sufind + 1)
end
elseif vform == "IX" then
check_len(4, 4)
vocalized = MU .. c(2) .. SK .. c(3) .. A .. c(4) .. SH
elseif vform == "IVq" then
-- e.g. [[اذلعب]] "to scamper away", مُذْلَعِبّ (active), مُذْلَعَبّ (passive);
-- [[اطمأن]] "to remain quietly; to be certain", مُطْمَئِنّ (active), مُطْمَأَنّ (passive)
check_len(5, 5)
local sound_prefix = MU .. c(2) .. SK .. c(3) .. A .. c(4)
if is_active then
vocalized = sound_prefix .. I .. c(5) .. SH
else
vocalized = sound_prefix .. A .. c(5) .. SH
end
elseif vform == "XI" then
check_len(5, 5)
check(4, ALIF)
vocalized = MU .. c(2) .. SK .. c(3) .. AA .. c(5) .. SH
-- e.g. [[احمار]] "to turn red, to blush", مُحْمَارّ (active)
elseif vform == "XIV" or vform == "XV" then
-- FIXME: Implement. No examples in Wiktionary currently; need to look up in a grammar.
error("Support for verb form " .. vform .. " not implemented yet")
else
error("Don't recognize verb form " .. vform)
end
vocalized = rsub(vocalized, HAMZA .. AA, AMAD)
local reconstructed_headword = lang:stripDiacritics(vocalized)
if reconstructed_headword ~= orig_headword then
error(("Internal error: Vocalized participle %s doesn't match original participle %s"):format(
vocalized, orig_headword))
end
return vocalized
end
function export.infer_participle_vocalization_json(frame)
local iparams = {
[1] = {required = true},
[2] = {required = true},
["weakness"] = {},
["passive"] = {type = "boolean"}
}
local iargs = require("Module:parameters").process(frame.args, iparams)
return export.infer_participle_vocalization(iargs[1], iargs[2], iargs.weakness, not iargs.passive)
end
return export
36w7cf4w0jiu37r5sgjmfptm23dhwit
Templat:translit module documentation
10
57684
379722
184087
2026-10-19T23:36:15Z
Hakimi97
2668
Betulkan panggilan modul
379722
wikitext
text/x-wiki
<includeonly>{{#invoke:pendokumenan/translit|documentation_template}}</includeonly><noinclude>{{documentation}}</noinclude>
29tapgxtbbxo9w7og2dkwpq6re98ftd
379724
379722
2026-10-19T23:37:13Z
Hakimi97
2668
Kemas kini kod
379724
wikitext
text/x-wiki
<includeonly>{{#invoke:pendokumenan/translit-like|documentation_template|translit}}</includeonly><noinclude>{{documentation}}</noinclude>
4di7zp8gusenefzmjbxyi3vp5dbym3u
leboh
0
64203
379752
314663
2026-10-20T00:30:04Z
Hakimi97
2668
Tambah entri bahasa Melayu
379752
wikitext
text/x-wiki
{{:wt:ms/{{FULLPAGENAME}}}}
==Bahasa Semai==
===Kata nama===
{{inti|sea|kata nama}}
# luas
#: {{cp|sea|kawasan tek ku kampug Odak leboh.|kawasan tanah di kampung Odak luas.}}
93mqaosrruxhbbny6750e1b7lrzmt2r
Modul:pendokumenan/translit-like
828
76119
379765
373901
2026-10-20T08:05:14Z
Hakimi97
2668
Penyetempatan
379765
Scribunto
text/plain
local export = {}
local m_module_categorization = require("Module:module categorization")
local m_table = require("Module:table")
local title_exists = require("Module:title/exists")
local languages_module = "Module:languages"
local scripts_module = "Module:scripts"
local debug_track_module = "Module:debug/track"
local insert = table.insert
local concat = table.concat
local operations = {
translit = {
expected_suffix = "translit",
verb = "transliterate",
cap_verb_3s = "Transliterates",
action_noun = "transliteration",
cat_action_noun = "Transliteration",
template = "xlit",
funcname = "tr",
languages_func = "transliterate",
by_module = "byTranslitModule",
},
["strip diacritics"] = {
expected_suffix = "stripdiacritics",
verb = "generate diacritic-stripped text for",
cap_verb_3s = "Strips diacritics from",
action_noun = "diacritic stripping",
cat_action_noun = "Diacritic-stripping",
template = "strip diacritics",
funcname = "stripDiacritics",
languages_func = "stripDiacritics",
by_module = "byStripDiacriticsModule",
},
sortkey = {
expected_suffix = "sortkey",
verb = "sort",
cap_verb_3s = "Generates a sortkey for",
action_noun = "sort",
cat_action_noun = "Sortkey-generating",
template = "sortkey",
funcname = "makeSortKey",
languages_func = "makeSortKey",
by_module = "bySortkeyModule",
},
}
local function get_operation_data(operation)
if not operations[operation] then
error(("Internal error: Unrecognized operation '%s' in [[Module:documentation/translit-like]]"):format(operation))
end
return operations[operation]
end
local function get_by_code(code)
return require(languages_module).getByCode(code, nil, true, true) or require(scripts_module).getByCode(code)
end
local function get_code_from_title_without_namespace(title_without_namespace, expected_suffix)
local prefix = title_without_namespace:match("^(.+)%-" .. expected_suffix .. "%f[/%z]")
if not prefix then
error("Base segment of title should end in -" .. expected_suffix .. ": " .. title_without_namespace)
end
local code = prefix
local lang_or_family_or_script = get_by_code(code)
local secondary_script, tertiary_script
if not lang_or_family_or_script then
-- Accommodate modules with multiple codes in the title.
-- The first code should be used.
-- Right now it strips segments from the end until it finds a match:
-- Module:Deva-Beng-translit -> Deva-Beng -> Deva
-- Module:pra-Deva-translit -> pra-Deva -> pra
-- Module:Deva-mnc-Mong-translit -> Deva-mnc-Mong -> Deva-mnc -> Deva
while true do
local new_code, secondary_script_code = code:match("^(.+)%-([^%-]+)$")
if new_code then
code = new_code
else
break
end
lang_or_family_or_script = get_by_code(new_code)
if secondary_script_code then
if secondary_script then
tertiary_script = secondary_script
end
secondary_script = get_by_code(secondary_script_code)
end
if lang_or_family_or_script then
break
end
end
end
return code, lang_or_family_or_script, secondary_script, tertiary_script
end
local function generate_documentation(data)
local operation, opdata, code, lang_or_family_or_script, secondary_script, tertiary_script, explanation,
title_without_namespace =
data.operation, data.opdata, data.code, data.lang_or_family_or_script, data.secondary_script,
data.tertiary_script, data.explanation, data.title_without_namespace
local verb, cap_verb_3s, action_noun, cat_action_noun, template, funcname, languages_func, by_module =
opdata.verb, opdata.cap_verb_3s, opdata.action_noun, opdata.cat_action_noun, opdata.template, opdata.funcname,
opdata.languages_func, opdata.by_module
if not lang_or_family_or_script then
return "Language code in page name (<code>" .. code .. "</code>) not recognized."
end
local category_link = lang_or_family_or_script:makeCategoryLink()
local script_lang = lang_or_family_or_script:hasType("language") and lang_or_family_or_script or nil
local secondary_script_category_link = secondary_script and secondary_script:makeCategoryLink(script_lang) or nil
local tertiary_script_category_link = tertiary_script and tertiary_script:makeCategoryLink(script_lang) or nil
local action_input
if lang_or_family_or_script:hasType("script") then
if secondary_script_category_link then
action_input = "text between the " .. category_link .. " and the " ..
secondary_script_category_link
else
action_input = "text in the " .. category_link
end
else
if lang_or_family_or_script:hasType("family") then
action_input = "text in one of the " .. category_link
else
action_input = category_link .. " text"
end
if tertiary_script_category_link then
action_input = action_input .. " between the " .. secondary_script_category_link .. " and the " ..
tertiary_script_category_link
elseif secondary_script_category_link then
action_input = action_input .. " in the " .. secondary_script_category_link
end
end
local tr_page = operation == "translit" and "WT:" .. mw.ustring.upper(code) .. " TR" or nil
local parts = {}
local function ins(txt)
insert(parts, txt)
end
ins("This module will " .. verb .. " " .. action_input .. (explanation and " " .. explanation or ""))
if tr_page and title_exists(tr_page) then
ins(" per [[" .. tr_page .. "]]")
end
ins(". ")
title_without_namespace = title_without_namespace:gsub("/doc$", "")
local language_objects = require("Module:languages/" .. by_module)(title_without_namespace)
local categories = {}
if language_objects[1] then
local agreement = language_objects[2] and "s" or ""
insert(categories, "[[Category:" .. cat_action_noun .. " modules used by " ..
#language_objects .. " language" .. agreement .. "]]")
end
local function insert_script_cat(script)
-- If language objects available, use the appropriate script name(s). Otherwise,
-- fall back to the default script name.
if language_objects[1] then
for _, langobj in ipairs(language_objects) do
m_table.insertIfNot(categories,
"[[Kategori:" .. script:getCategoryName(nil, langobj) .. "]]")
end
else
insert(categories, "[[Kategori:" .. script:getCategoryName() .. "]]")
end
end
if lang_or_family_or_script:hasType("script") then
insert_script_cat(lang_or_family_or_script)
end
if secondary_script then
insert_script_cat(secondary_script)
end
if tertiary_script then
insert_script_cat(tertiary_script)
end
if not title_exists("Modul:" .. title_without_namespace .. "/testcases") then
insert(categories, "[[Category:" .. cat_action_noun .. " modules without a testcases subpage]]")
end
-- Filter out the language code in the module name, since we list that language first.
local new_language_objects = {}
local code_in_pagename_in_list = false
for _, langobj in ipairs(language_objects) do
local result = langobj:getCode() ~= code
code_in_pagename_in_list = code_in_pagename_in_list or result
if result then
insert(new_language_objects, langobj)
end
end
language_objects = new_language_objects
if language_objects[1] then
table.sort(language_objects, function(lang1, lang2)
return lang1:getCode() < lang2:getCode()
end)
local lang_category_links = {}
for _, langobj in ipairs(language_objects) do
insert(lang_category_links, langobj:makeCategoryLink())
end
local langs = m_table.serialCommaJoin(lang_category_links)
ins("It is " .. (code_in_pagename_in_list and "also" or "") ..
" used to " .. verb .. " " .. langs .. ".")
end
ins(([=[
The module should preferably not be called directly from templates or other modules.
To use it from a template, use <code>{{[[Template:%s|%s]]}}</code>.
Within a module, use [[Module:languages#Language:%s]].
For testcases, see [[Module:]=]):format(template, template, languages_func) ..
title_without_namespace .. ([=[/testcases]].
== Functions ==
; <code>%s(text, lang, sc)</code>
: %s a given piece of <code>text</code> written in the script specified by the code <code>sc</code>, and language specified by the code <code>lang</code>.
: When %s fails, returns <code>nil</code>.]=]):format(funcname, cap_verb_3s, action_noun)
)
ins(concat(categories))
local langlist_codes = {}
insert(langlist_codes, code)
for _, langobj in ipairs(language_objects) do
insert(langlist_codes, langobj:getCode())
end
ins(m_module_categorization.categorize_module {
pagename = title_without_namespace,
langlist = concat(langlist_codes, ","),
})
return concat(parts)
end
function export.documentation(data)
local operation, code, lang_or_family_or_script, secondary_script, tertiary_script, title_without_namespace,
explanation =
data.operation, data.code, data.lang_or_family_or_script, data.secondary_script, data.tertiary_script,
data.title_without_namespace, data.explanation
local opdata = get_operation_data(operation)
if not code then
code, lang_or_family_or_script, secondary_script, tertiary_script =
get_code_from_title_without_namespace(title_without_namespace, opdata.expected_suffix)
end
local docdata = {
operation = operation,
opdata = opdata,
code = code,
lang_or_family_or_script = lang_or_family_or_script,
secondary_script = secondary_script,
tertiary_script = tertiary_script,
explanation = explanation,
title_without_namespace = title_without_namespace,
}
return generate_documentation(docdata)
end
function export.documentation_template(frame)
local iparams = frame.args
local operation = iparams[1]
local opdata = get_operation_data(operation)
-- Parameters to {{OPERATION module documentation}}:
-- |code|description
-- Ignore code because we get it from the page name.
local pagename = mw.title.getCurrentTitle().text -- DO NOT replace with mw.loadData("Module:headword/data").pagename as we need the root portion
local args = frame:getParent().args
if args[1] and get_code_from_title_without_namespace(pagename, opdata.expected_suffix) ~= args[1] then
-- [[Special:WhatLinksHere/Wiktionary:Tracking/OPERATION/input different from title]]
require(debug_track_module)(operation .. "/input different from title")
end
if args[1] then
return export.documentation {
operation = operation,
code = args[1],
lang_or_family_or_script = get_by_code(args[1]),
secondary_script = args.sc and get_by_code(args.sc) or nil,
tertiary_script = args.sc2 and get_by_code(args.sc2) or nil,
explanation = args[2],
title_without_namespace = pagename,
}
else
return export.documentation {
operation = operation,
explanation = args[2],
title_without_namespace = pagename,
}
end
end
return export
3as85onsav3oqks1fojieecsu3mjyql
Wikikamus:zlm-pah/kabo
4
92021
379755
253589
2026-10-20T02:49:58Z
Zeeum02
5028
/* Bahasa Melayu Pahang */
379755
wikitext
text/x-wiki
==Bahasa Melayu Pahang==
===Kata kerja===
{{inti|ms|kata kerja}}
# [[beritahu]]
#: {{cp|zlm-pah|aboh '''kabo''' die nok meli baju de bando .|Ayah '''beritahu''' yang dia mahu membeli baju di bandar.}}
=== Sebutan ===
* {{AFA|zlm-pah|[ka.bɔ]}}
===Pautan luar===
* {{R:PRPM|gdpah|keyword=kabo memberitahu}}
73a3drsme6fncejmxks4ys3vw4acxql
379760
379755
2026-10-20T04:44:52Z
Hakimi97
2668
ms > zlm-pah
379760
wikitext
text/x-wiki
==Bahasa Melayu Pahang==
===Kata kerja===
{{inti|zlm-pah|kata kerja}}
# [[beritahu]]
#: {{cp|zlm-pah|aboh '''kabo''' die nok meli baju de bando .|Ayah '''beritahu''' yang dia mahu membeli baju di bandar.}}
=== Sebutan ===
* {{AFA|zlm-pah|[ka.bɔ]}}
===Pautan luar===
* {{R:PRPM|gdpah|keyword=kabo memberitahu}}
14dzsxnqcjwnr202fq0uw8poacnjaof
deme
0
136677
379743
363703
2026-10-20T00:25:39Z
Hakimi97
2668
Tambah kandungan
379743
wikitext
text/x-wiki
{{:wt:ms/{{FULLPAGENAME}}}}
{{:wt:mfa/{{FULLPAGENAME}}}}
36l66xlp75h9mq3gtg4uf9crv67n4co
tembakul
0
148243
379754
379666
2026-10-20T00:37:49Z
Hakimi97
2668
Betulkan entri
379754
wikitext
text/x-wiki
==Bahasa {{bahasa|ms}}==
===Kata nama===
{{ms-kn|j=تمباکول}}
# belacak, ''Periophthalmus koelreuteri''
77mn46dcf4gupueqz8xi2zcad9gwhhz
Wikikamus:ms/teman
4
148266
379711
379691
2026-10-19T14:59:28Z
Uwais Athrun
11607
379711
wikitext
text/x-wiki
==Bahasa {{bahasa|ms}}==
===Kata nama===
{{inti|ms|kata nama}}
# {{label|1=ms|2=dialek|3=Perak}} saya (sopan, sedikit berbeza dengan [[aok]], yang lebih kasar, '''aku)'''
#: {{cp|ms|'''teman''' nak gi ke teluk intan, jumpe sedare|
'''saya''' mahu pergi ke Teluk Intan, berjumpa saudara}}
fcg19f46a1dnq4isiiov9zemhgae9kw
379739
379711
2026-10-20T00:23:53Z
Hakimi97
2668
Gabungkan daripada kandungan di [[teman]]
379739
wikitext
text/x-wiki
===Etimologi===
Pinjaman {{bor|ms|ta|தமன்}}.
===Sebutan===
* {{dewan|te|man}}
* {{IPA|ms|/təman/}}
* {{rhymes|ms|əman|man|an}}
===Kata nama===
{{ms-kn|j=تمن}}
# Orang yang mengikut seseorang bersama-sama.
# Orang yang rapat dengan seseorang; [[kawan]].
#: {{syn|ms|kawan|rakan|sahabat}}
# {{label|1=ms|2=dialek|3=Perak}} saya (sopan, sedikit berbeza dengan [[aok]], yang lebih kasar, '''aku)'''
#: {{cp|ms|'''teman''' nak gi ke teluk intan, jumpe sedare|
'''saya''' mahu pergi ke Teluk Intan, berjumpa saudara}}
====Kata terbitan====
* {{l|ms|teman lelaki}}
* {{l|ms|teman wanita}}
===Kata kerja===
{{ms-kk|j=تمن}}
# Mengikuti seseorang ke suatu tempat.
===Rujukan===
* {{R:KD4}}
===Pautan luar===
* {{R:PRPM}}
kgw3oiaznjo7ml8ewgpur2s4ig3oz7t
379741
379739
2026-10-20T00:24:32Z
Hakimi97
2668
Betulkan format
379741
wikitext
text/x-wiki
===Etimologi===
Pinjaman {{bor|ms|ta|தமன்}}.
===Sebutan===
* {{dewan|te|man}}
* {{IPA|ms|/təman/}}
* {{rhymes|ms|əman|man|an}}
===Kata nama===
{{ms-kn|j=تمن}}
# Orang yang mengikut seseorang bersama-sama.
# Orang yang rapat dengan seseorang; [[kawan]].
#: {{syn|ms|kawan|rakan|sahabat}}
# {{label|1=ms|2=dialek|3=Perak}} saya (sopan, sedikit berbeza dengan [[aok]], yang lebih kasar, '''aku)'''
#: {{cp|ms|'''teman''' nak gi ke teluk intan, jumpe sedare|
'''saya''' mahu pergi ke Teluk Intan, berjumpa saudara}}
====Kata terbitan====
* {{l|ms|teman lelaki}}
* {{l|ms|teman wanita}}
===Kata kerja===
{{ms-kk|j=تمن}}
# Mengikuti seseorang ke suatu tempat.
===Rujukan===
* {{R:KD4}}
===Pautan luar===
* {{R:PRPM}}
i838mm66ma4x4whgnshp29phqo4734c
Wikikamus:ms/deme
4
148267
379713
379692
2026-10-19T15:01:42Z
Uwais Athrun
11607
379713
wikitext
text/x-wiki
==Bahasa {{bahasa|ms}}==
===Kata nama===
{{inti|ms|kata nama}}
# {{label|1=ms|2=dialek|3=Perak}} mereka, diorang, dia (bebas)
#: {{cp|ms|'''deme''' duk terkinja-kinja kat parit tu udah rope kere}} diorang/mereka sedang terkinja-kinja di parit itu seumpama monyet
#: {{cp|ms|adeh '''deme''' ni, udah kejan pulak, ajenye bute}} aduhai dia ni, dah menekan pulak, sungguh lah buta
o8drzcrf8f790dyb6l65tz92dcl9e7j
379742
379713
2026-10-20T00:25:26Z
Hakimi97
2668
Betulkan format
379742
wikitext
text/x-wiki
==Bahasa {{bahasa|ms}}==
===Kata nama===
{{inti|ms|kata nama}}
# {{label|1=ms|2=dialek|3=Perak}} mereka, diorang, dia (bebas)
#: {{cp|ms|'''deme''' duk terkinja-kinja kat parit tu udah rope kere|diorang/mereka sedang terkinja-kinja di parit itu seumpama monyet}}
#: {{cp|ms|adeh '''deme''' ni, udah kejan pulak, ajenye bute|aduhai dia ni, dah menekan pulak, sungguh lah buta}}
t27u6hxspo1e7f88ri0n09z5azga1rr
Wikikamus:ms/daba
4
148269
379744
379695
2026-10-20T00:26:26Z
Hakimi97
2668
Betulkan format
379744
wikitext
text/x-wiki
==Bahasa {{bahasa|ms}}==
===Kata nama===
{{inti|ms|kata nama}}
# {{label|1=ms|2=dialek|3=Perak}} beg duit, dompet
# {{cp|ms|miker tolong amikkan '''daba''' aok di tepi gerobok tu, ate tangan kosong udah haup|kau tolong ambilkan dompet aku di tepi almari tu, nanti tangan kosong, matilah}}
346098efknww4m3u04ndkr77nw2bipv
379746
379744
2026-10-20T00:26:53Z
Hakimi97
2668
Betulkan format (2)
379746
wikitext
text/x-wiki
==Bahasa {{bahasa|ms}}==
===Kata nama===
{{inti|ms|kata nama}}
# {{label|1=ms|2=dialek|3=Perak}} beg duit, dompet
#: {{cp|ms|miker tolong amikkan '''daba''' aok di tepi gerobok tu, ate tangan kosong udah haup|kau tolong ambilkan dompet aku di tepi almari tu, nanti tangan kosong, matilah}}
jzru58cam3eef99vnzxdmpbr8hha1a4
Wikikamus:ms/haup
4
148271
379747
379698
2026-10-20T00:28:04Z
Hakimi97
2668
Betulkan format
379747
wikitext
text/x-wiki
==Bahasa {{bahasa|ms}}==
===Kata kerja===
{{inti|ms|kata kerja}}
# {{label|1=ms|2=dialek|3=Perak}} tumbang sepenuhnya akibat keletihan melampau, ataupun kalah sepenuhnya. "mati" (humor gelap)
#: {{cp|ms|aok udah kene buang koje dah, ate '''haup''' le|aku dah kena dibuang kerja, nanti mati lah}}
#: {{cp|ms|miker nak lawan ngan koba? '''haup''' le jawabnye.|kau nak lawan dengan kerbau? matilah jawapannya.}}
jf88kipaeo42d89g8an4g4nq98vaavk
Wikikamus:ms/tumpor
4
148272
379749
379699
2026-10-20T00:28:51Z
Hakimi97
2668
Betulkan format
379749
wikitext
text/x-wiki
==Bahasa {{bahasa|ms}}==
===Kata kerja===
{{inti|ms|kata kerja}}
# {{label|1=ms|2=dialek|3=Perak}} tumbang, letih sangat, hujung nyawa (secara humor gelap), tak larat. "Mampus lah"
# {{cp|ms|'''tumpor''' le, ngadap koje takde cuti dalam dua minggu ni.|Letih lah, menghadap kerja takde cuti dalam dua minggu ni.}}
7p5ygd16ytn6wr9gbcwqcu3svxcgqfy
379750
379749
2026-10-20T00:29:11Z
Hakimi97
2668
Betulkan format (2)
379750
wikitext
text/x-wiki
==Bahasa {{bahasa|ms}}==
===Kata kerja===
{{inti|ms|kata kerja}}
# {{label|1=ms|2=dialek|3=Perak}} tumbang, letih sangat, hujung nyawa (secara humor gelap), tak larat. "Mampus lah"
#: {{cp|ms|'''tumpor''' le, ngadap koje takde cuti dalam dua minggu ni.|Letih lah, menghadap kerja takde cuti dalam dua minggu ni.}}
escc1hxk0x5fy43zt2girsdp5nt5cg4
Wikikamus:bdr/indam
4
148279
379708
2026-10-19T14:52:29Z
Sipatung
10839
Membuat terjemahan baru
379708
wikitext
text/x-wiki
==Bahasa {{bahasa|bdr}}==
===Kata kerja===
{{inti|bdr|kata kerja}}
# {{label|1=bdr|2=dialek|3=Sabah}} pinjam {{cp|bdr|'''Indam''' ku do' sin nu deringgit.|Saya '''[[pinjam]]''' dulu duit kau seringgit.}}
bi6jn2yasjriy8u3e4s82fokgt5q113
379718
379708
2026-10-19T15:52:04Z
Mirlim
8057
379718
wikitext
text/x-wiki
==Bahasa {{bahasa|bdr}}==
===Kata kerja===
{{inti|bdr|kata kerja}}
# {{label|1=bdr|2=dialek|3=Sabah}} [[pinjam]]
#: {{cp|bdr|'''Indam''' ku do' sin nu deringgit.|Saya '''pinjam''' dulu duit kau seringgit.}}
===Bacaan lanjut===
* {{R:PRPM|bsv1}}
m5wblhip7ou6x0ycnp1506cshx8tp8m
Wikikamus:bdr/pendawan
4
148280
379709
2026-10-19T14:55:22Z
Sipatung
10839
Membuat terjemahan baru
379709
wikitext
text/x-wiki
==Bahasa {{bahasa|bdr}}==
===Kata nama===
{{inti|bdr|kata nama}}
# {{label|1=bdr|2=dialek|3=Sabah}} tingkap {{cp|bdr|Aku mantau sedi '''pendawan''' rumah.|Saya berdiri dekat '''[[tingkap]]''' rumah.}}
0ym4qxd16gm1lhlt9mwdkx9s26mtrcv
Wikikamus:bdr/keket
4
148281
379710
2026-10-19T14:58:49Z
Sipatung
10839
Mencipta laman
379710
wikitext
text/x-wiki
==Bahasa {{bahasa|bdr}}==
===Kata kerja===
{{inti|bdr|kata kerja}}
# {{label|1=bdr|2=dialek|3=Sabah}} gigit {{cp|bdr|'''Keket''' namuk kekanak diki a.|Nyamuk '''[[gigit]]'' bayi itu.}}
s2842mxe08ic75063yaiwh0ih7t2ytp
Wikikamus:bdr/nguar
4
148282
379712
2026-10-19T15:01:06Z
Sipatung
10839
Mencipta laman
379712
wikitext
text/x-wiki
==Bahasa {{bahasa|bdr}}==
===Kata kerja===
{{inti|bdr|kata kerja}}
# {{label|1=bdr|2=dialek|3=Sabah}} goreng {{cp|bdr|Eyang ku '''nguar''' buas.|Emak saya '''[[goreng]]''' nasi.}}
bf4kkyv4yc7kt1ev1d2xwn30et1g8jj
Wikikamus:bdr/ngusea'
4
148283
379714
2026-10-19T15:04:32Z
Sipatung
10839
Mencipta laman
379714
wikitext
text/x-wiki
==Bahasa {{bahasa|bdr}}==
===Kata kerja===
{{inti|bdr|kata kerja}}
# {{label|1=bdr|2=dialek|3=sabah}} cuci {{cp|bdr|Aku '''ngusea'''' betis sebelum menik ruma.|Saya '''[[cuci]]''' kaki sebelum naik rumah.}}
sdv8gzqq6ioj5tc0qu6az0irppqu6eg
Wikikamus:ms/leboh
4
148284
379715
2026-10-19T15:14:06Z
Uwais Athrun
11607
Sumbangan perkataan Perak
379715
wikitext
text/x-wiki
==Bahasa {{bahasa|ms}}==
===Kata kerja===
{{inti|ms|kata kerja}}
# {{label|1=ms|2=dialek|3=Perak}} hilang, lesap, terlepas.
#:{{cp|ms|mampuih le, barang aok udah '''leboh'''.| Matilah, barang ku sudah '''lesap'''}}
4x6pt5nl3em76ty6sd5okchs1vnbdek
Wikikamus:ms/khabor
4
148285
379716
2026-10-19T15:22:55Z
Uwais Athrun
11607
Sumbangan perkataan Perak
379716
wikitext
text/x-wiki
==Bahasa {{bahasa|ms}}==
===Kata kerja===
{{inti|ms|kata kerja}}
# {{label|1=ms|2=dialek|3=Perak}} beritahu
#: {{cp|ms|aok udah '''khabor''' banyak kali dah|aku dah '''beritahu''' banyak kali dah}}
nek0e4q6lhno579c8wsg0lmwtkkvnm7
379717
379716
2026-10-19T15:47:35Z
Mirlim
8057
379717
wikitext
text/x-wiki
==Bahasa {{bahasa|ms}}==
===Kata kerja===
{{inti|ms|kata kerja}}
# {{label|1=ms|2=dialek|3=Perak}} beritahu
#: {{cp|ms|Aok udah '''khabor''' banyak kali dah.|Aku dah '''beritahu''' banyak kali dah.}}
62f0pj19r0tjm9i0bgquuw15guk028u
langgar jangan beragak
0
148286
379719
2026-10-19T16:04:56Z
Mirlim
8057
Mencipta laman baru dengan kandungan '==Bahasa Melayu== ===Frasa=== {{head|ms|frasa|ejaan Jawi|لڠݢر جاڠن براݢق}} # menyerang secara bersungguh-sungguh ===Rujukan=== * {{R:KD4}} ===Bacaan lanjut=== * {{R:PRPM}}'
379719
wikitext
text/x-wiki
==Bahasa Melayu==
===Frasa===
{{head|ms|frasa|ejaan Jawi|لڠݢر جاڠن براݢق}}
# menyerang secara bersungguh-sungguh
===Rujukan===
* {{R:KD4}}
===Bacaan lanjut===
* {{R:PRPM}}
dgu4nz2ln5rrjk6sfkztnnywd73cyh8
Modul:kv-translit/doc
828
148287
379723
2026-10-19T23:36:30Z
Hakimi97
2668
Mencipta laman baru dengan kandungan '{{translit module documentation|kpv}} {{module cat|kpv,koi,urj-kya}}'
379723
wikitext
text/x-wiki
{{translit module documentation|kpv}}
{{module cat|kpv,koi,urj-kya}}
70z7znqcd9s7jbms0e76tnxfvtpgnbx
Templat:kk-alt
10
148288
379731
2026-10-20T00:05:42Z
Hakimi97
2668
Mencipta laman baru dengan kandungan '{{inflection-table-top|title=Tulisan alternatif scripts|class=floatright|palette=blue}} ! {{w|Abjad bahasa Kazakh#Tulisan Arab|Arab}} | {{#if:{{{ar|}}}{{{Arab|}}}|{{l-self|kk|{{{ar|}}}{{{Arab|}}}|tr=-}}|{{l-self|kk|{{#invoke:kk-Cyrl-Arab-translit|tr|{{{Cyrl|{{pagename}}}}}}}|tr=-}}}}{{#if:{{{ar2|}}}{{{Arab2|}}}|, {{l-self|kk|{{{ar2|}}}{{{Arab2|}}}|tr=-}}|}} |- {{#if: {{{old|}}}{{{ar-old|}}}{{{old2|}}}{{{ar-old2|}}} | {{!-}} ! {{w|Abjad İske imlâ|İsk...'
379731
wikitext
text/x-wiki
{{inflection-table-top|title=Tulisan alternatif scripts|class=floatright|palette=blue}}
! {{w|Abjad bahasa Kazakh#Tulisan Arab|Arab}}
| {{#if:{{{ar|}}}{{{Arab|}}}|{{l-self|kk|{{{ar|}}}{{{Arab|}}}|tr=-}}|{{l-self|kk|{{#invoke:kk-Cyrl-Arab-translit|tr|{{{Cyrl|{{pagename}}}}}}}|tr=-}}}}{{#if:{{{ar2|}}}{{{Arab2|}}}|, {{l-self|kk|{{{ar2|}}}{{{Arab2|}}}|tr=-}}|}}
|- {{#if: {{{old|}}}{{{ar-old|}}}{{{old2|}}}{{{ar-old2|}}} |
{{!-}}
! {{w|Abjad İske imlâ|İske imlâ}}
{{!}} {{#if: {{{old|}}}{{{ar-old|}}} | {{l-self|kk|{{{old|}}}{{{ar-old|}}}|tr=-}}}}|
}}{{#if:{{{old2|}}}{{{ar-old2|}}}|, {{l-self|kk|{{{old2|}}}{{{ar-old2|}}}|tr=-}}|}}
|-
! style="min-width:4em;max-width:7em"| {{w|Abjad bahasa Kazakh#Tulisan Siril|Siril}}
| {{#if:{{{cy|}}}{{{Cyrl|}}}|{{l-self|kk|{{{cy|}}}{{{Cyrl|}}}|tr=-}}|{{l-self|kk|{{{Cyrl|{{pagename}}}}}|tr=-}}}}{{#if:{{{cy2|}}}{{{Cyrl2|}}}|, {{l-self|kk|{{{cy2|}}}{{{Cyrl2|}}}|tr=-}}|}}
|-
! {{w|Abjad bahasa Kazakh#Tulisan Latin|Latin}}
| {{#if:{{{la|}}}{{{Latn|}}}|{{l-self|kk|{{{la|}}}{{{Latn|}}}|tr=-}}|{{l-self|kk|{{xlit|kk|module=kk-convert|{{{Cyrl|{{pagename}}}}}}}|tr=-}}}}{{#if:{{{la2|}}}{{{Latn2|}}}|, {{l-self|kk|{{{la2|}}}{{{Latn2|}}}|tr=-}}|}} <!--
yañalif is disabled for now, needs to be fixed before being reenabled
|-
{{#ifeq:{{xlit|kk|module=kk-translit|{{{Cyrl|{{PAGENAME}}}}}}}|{{#invoke:kk-translit|yanalif|{{{Cyrl|{{pagename}}}}}}}||! {{w|Yañalif}}
{{!}} {{#if: {{{yan|}}}{{{ya-Latn|}}} {{!}} {{l-self|kk|{{{yan|}}}{{{ya-Latn|}}}|tr=-}}|{{l-self|kk|{{#invoke:kk-translit|yanalif|{{{Cyrl|{{pagename}}}}}|tr=-}}}}}}}} -->
{{inflection-table-bottom}}<noinclude>{{documentation}}</noinclude>
1klhhizm63i81bq5bny8y8s2u22k4ly
379733
379731
2026-10-20T00:07:42Z
Hakimi97
2668
379733
wikitext
text/x-wiki
{{inflection-table-top|title=Tulisan alternatif scripts|class=floatright|palette=blue}}
! {{w|Abjad bahasa Kazakh#Tulisan Arab|Arab}}
| {{#if:{{{ar|}}}{{{Arab|}}}|{{l-self|kk|{{{ar|}}}{{{Arab|}}}|tr=-}}|{{l-self|kk|{{#invoke:kk-Cyrl-Arab-translit|tr|{{{Cyrl|{{pagename}}}}}}}|tr=-}}}}{{#if:{{{ar2|}}}{{{Arab2|}}}|, {{l-self|kk|{{{ar2|}}}{{{Arab2|}}}|tr=-}}|}}
|- {{#if: {{{old|}}}{{{ar-old|}}}{{{old2|}}}{{{ar-old2|}}} |
{{!-}}
! {{w|Abjad İske imlâ|İske imlâ}}
{{!}} {{#if: {{{old|}}}{{{ar-old|}}} | {{l-self|kk|{{{old|}}}{{{ar-old|}}}|tr=-}}}}|
}}{{#if:{{{old2|}}}{{{ar-old2|}}}|, {{l-self|kk|{{{old2|}}}{{{ar-old2|}}}|tr=-}}|}}
|-
! style="min-width:4em;max-width:7em"| {{w|Abjad bahasa Kazakh#Tulisan Cyril|Cyril}}
| {{#if:{{{cy|}}}{{{Cyrl|}}}|{{l-self|kk|{{{cy|}}}{{{Cyrl|}}}|tr=-}}|{{l-self|kk|{{{Cyrl|{{pagename}}}}}|tr=-}}}}{{#if:{{{cy2|}}}{{{Cyrl2|}}}|, {{l-self|kk|{{{cy2|}}}{{{Cyrl2|}}}|tr=-}}|}}
|-
! {{w|Abjad bahasa Kazakh#Tulisan Latin|Latin}}
| {{#if:{{{la|}}}{{{Latn|}}}|{{l-self|kk|{{{la|}}}{{{Latn|}}}|tr=-}}|{{l-self|kk|{{xlit|kk|module=kk-convert|{{{Cyrl|{{pagename}}}}}}}|tr=-}}}}{{#if:{{{la2|}}}{{{Latn2|}}}|, {{l-self|kk|{{{la2|}}}{{{Latn2|}}}|tr=-}}|}} <!--
yañalif is disabled for now, needs to be fixed before being reenabled
|-
{{#ifeq:{{xlit|kk|module=kk-translit|{{{Cyrl|{{PAGENAME}}}}}}}|{{#invoke:kk-translit|yanalif|{{{Cyrl|{{pagename}}}}}}}||! {{w|Yañalif}}
{{!}} {{#if: {{{yan|}}}{{{ya-Latn|}}} {{!}} {{l-self|kk|{{{yan|}}}{{{ya-Latn|}}}|tr=-}}|{{l-self|kk|{{#invoke:kk-translit|yanalif|{{{Cyrl|{{pagename}}}}}|tr=-}}}}}}}} -->
{{inflection-table-bottom}}<noinclude>{{documentation}}</noinclude>
aie3oxqgz5jger16noruywgg2siopbw
379735
379733
2026-10-20T00:09:06Z
Hakimi97
2668
Buang "scripts"
379735
wikitext
text/x-wiki
{{inflection-table-top|title=Tulisan alternatif|class=floatright|palette=blue}}
! {{w|Abjad bahasa Kazakh#Tulisan Arab|Arab}}
| {{#if:{{{ar|}}}{{{Arab|}}}|{{l-self|kk|{{{ar|}}}{{{Arab|}}}|tr=-}}|{{l-self|kk|{{#invoke:kk-Cyrl-Arab-translit|tr|{{{Cyrl|{{pagename}}}}}}}|tr=-}}}}{{#if:{{{ar2|}}}{{{Arab2|}}}|, {{l-self|kk|{{{ar2|}}}{{{Arab2|}}}|tr=-}}|}}
|- {{#if: {{{old|}}}{{{ar-old|}}}{{{old2|}}}{{{ar-old2|}}} |
{{!-}}
! {{w|Abjad İske imlâ|İske imlâ}}
{{!}} {{#if: {{{old|}}}{{{ar-old|}}} | {{l-self|kk|{{{old|}}}{{{ar-old|}}}|tr=-}}}}|
}}{{#if:{{{old2|}}}{{{ar-old2|}}}|, {{l-self|kk|{{{old2|}}}{{{ar-old2|}}}|tr=-}}|}}
|-
! style="min-width:4em;max-width:7em"| {{w|Abjad bahasa Kazakh#Tulisan Cyril|Cyril}}
| {{#if:{{{cy|}}}{{{Cyrl|}}}|{{l-self|kk|{{{cy|}}}{{{Cyrl|}}}|tr=-}}|{{l-self|kk|{{{Cyrl|{{pagename}}}}}|tr=-}}}}{{#if:{{{cy2|}}}{{{Cyrl2|}}}|, {{l-self|kk|{{{cy2|}}}{{{Cyrl2|}}}|tr=-}}|}}
|-
! {{w|Abjad bahasa Kazakh#Tulisan Latin|Latin}}
| {{#if:{{{la|}}}{{{Latn|}}}|{{l-self|kk|{{{la|}}}{{{Latn|}}}|tr=-}}|{{l-self|kk|{{xlit|kk|module=kk-convert|{{{Cyrl|{{pagename}}}}}}}|tr=-}}}}{{#if:{{{la2|}}}{{{Latn2|}}}|, {{l-self|kk|{{{la2|}}}{{{Latn2|}}}|tr=-}}|}} <!--
yañalif is disabled for now, needs to be fixed before being reenabled
|-
{{#ifeq:{{xlit|kk|module=kk-translit|{{{Cyrl|{{PAGENAME}}}}}}}|{{#invoke:kk-translit|yanalif|{{{Cyrl|{{pagename}}}}}}}||! {{w|Yañalif}}
{{!}} {{#if: {{{yan|}}}{{{ya-Latn|}}} {{!}} {{l-self|kk|{{{yan|}}}{{{ya-Latn|}}}|tr=-}}|{{l-self|kk|{{#invoke:kk-translit|yanalif|{{{Cyrl|{{pagename}}}}}|tr=-}}}}}}}} -->
{{inflection-table-bottom}}<noinclude>{{documentation}}</noinclude>
k8vztqdrywvs1flpqnwla8ea9kko509
Modul:kk-Cyrl-Arab-translit
828
148289
379732
2026-10-20T00:06:54Z
Hakimi97
2668
Mencipta laman baru dengan kandungan 'local export = {} local rsplit = mw.text.split local U = mw.ustring.char local rsubn = mw.ustring.gsub local ulen = mw.ustring.len local frontvowels = "үҮәӘіІөӨ" local backvowels = "ӯӮАаоОЫыҰұ" local allvowels = "үҮиИеЕәӘөӨӯӮАаоОЫыІіЯяЮюЁё" local allchar = "АаӘәБбВвГгҒғДдЕеЁёЖжЗзИиЙйКкҚқЛлМмНнҢңОоӨөПпРрСсТтУуҰұҮүФфХхҺһЦцЧчШшЩщЪъЫыІі...'
379732
Scribunto
text/plain
local export = {}
local rsplit = mw.text.split
local U = mw.ustring.char
local rsubn = mw.ustring.gsub
local ulen = mw.ustring.len
local frontvowels = "үҮәӘіІөӨ"
local backvowels = "ӯӮАаоОЫыҰұ"
local allvowels = "үҮиИеЕәӘөӨӯӮАаоОЫыІіЯяЮюЁё"
local allchar = "АаӘәБбВвГгҒғДдЕеЁёЖжЗзИиЙйКкҚқЛлМмНнҢңОоӨөПпРрСсТтУуҰұҮүФфХхҺһЦцЧчШшЩщЪъЫыІіЬьЭэЮюЯя"
local hmzh = U(0x674)
local mapping = {
["ү"] = "ۇ"
,["Ү"] = "ۇ"
,["т"] = "ت"
,["Т"] = "ت"
,["һ"] = "ھ"
,["Һ"] = "ھ"
,["р"] = "ر"
,["Р"] = "ر"
,["ф"] = "ف"
,["Ф"] = "ف"
,["ш"] = "ش"
,["Ш"] = "ش"
,["н"] = "ن"
,["Н"] = "ن"
,["п"] = "پ"
,["П"] = "پ"
,["й"] = "ي"
,["Й"] = "ي"
,["л"] = "ل"
,["Л"] = "ل"
,["з"] = "ز"
,["З"] = "ز"
,["е"] = "ە"
,["Е"] = "ە"
,["г"] = "گ"
,["Г"] = "گ"
,["б"] = "ب"
,["Б"] = "ب"
,["у"] = "ۋ"
,["У"] = "ۋ"
,["с"] = "س"
,["С"] = "س"
,["х"] = "ح"
,["Х"] = "ح"
,["ы"] = "ى"
,["Ы"] = "ى"
,["м"] = "م"
,["М"] = "م"
,["о"] = "و"
,["О"] = "و"
,["и"] = "ي"
,["И"] = "ي"
,["ж"] = "ج"
,["Ж"] = "ج"
,["к"] = "ك"
,["К"] = "ك"
,["д"] = "د"
,["Д"] = "د"
,["в"] = "ۆ"
,["В"] = "ۆ"
,["а"] = "ا"
,["А"] = "ا"
,["ң"] = "ڭ"
,["Ң"] = "ڭ"
,["ә"] = "ا"
,["Ә"] = "ا"
,["э"] = "ە"
,["Э"] = "ە"
,["ұ"] = "ۇ"
,["Ұ"] = "ۇ"
,["қ"] = "ق"
,["Қ"] = "ق"
,["ғ"] = "ع"
,["Ғ"] = "ع"
,["і"] = "ى"
,["І"] = "ى"--?
,["ө"] = "و"
,["Ө"] = "و"
--mostly in loanwords from Russian
,["ё"] = "يو"
,["Ё"] = "يو"
,["ц"] = "تس"
,["Ц"] = "تس"
,["ч"] = "چ"
,["Ч"] = "چ"
,["щ"] = "شش"
,["Щ"] = "شش"
,["ъ"] = ""
,["Ъ"] = ""
,["ь"] = ""
,["Ь"] = ""
,["э"] = "ە"
,["Э"] = "ە"
,["ю"] = "يۋ"
,["Ю"] = "يۋ"
,["я"] = "يا"
,["Я"] = "يا"
--хостинг - hostiñ
,["нг"] = "ڭ"
,["Нг"] = "ڭ"
--is it used?
,["ӯ"] = "ۇ"
,["Ӯ"] = "ۇ"
--punctuation
,[","] = "،"
,["%"] = "٪"
,["?"] = "؟"
}
--[==[ disabled: hamza ligatures have been depricated by unicode
local function fvowels(text)
-- front vowels
text = rsubn(text, hmzh .. "[үҮ]", "ٷ")
text = rsubn(text, hmzh .. "[Әә]", "ٵ")
text = rsubn(text, hmzh .. "[Іі]", "ٸ")
text = rsubn(text, hmzh .. "[Өө]", "ٶ")
text = rsubn(text, hmzh .. "([" .. backvowels .. "])", "%1")
return text
end ]==]
function export.tr(text, lang, sc)
if type(text) == "table" then
options = {}
text, script = text.args[1], text.args[2]
end
if not sc then
sc = require("Module:languages").getByCode("kk"):findBestScript(text):getCode()
end
if sc ~= "Cyrl" then
return nil
end
text = rsubn(text, "^" .. "[-]", "-")
text = rsubn(text, "([" .. allchar .. "])([^" .. allchar .. "])", "%1#_#%2")
text = rsubn(text, "([^" .. allchar .. "])([" .. allchar .. "])", "%1#_#%2")
text = rsubn(text, " | ", "# | #")
text = "##" .. rsubn(text, " ", "# #") .. "##"
-- treat every word as an individual string
rsplit( "#" .."%s" .. "#", ",")
rsplit( "#" .."_" .. "#", ",")
text = rsubn(text, "[иИ][яЯ]", "йа")
text = rsubn(text, "[иИ]([".. allvowels .."])", "й%1")
text = rsubn(text, "([".. allvowels .."])[яЯ]", "%1йа")
text = rsubn(text, "#[яЯ]", "#йа")
text = rsubn(text, "[яЯ]", "ә")
text = rsubn(text, "#([" .. allchar .. "]+)([" .. frontvowels .. "])", "#" .. hmzh .. "%1%2")
text = rsubn(text, "#([" .. frontvowels .. "])", "#" .. hmzh .. "%1")
text = rsubn(text, "#" .. hmzh .. "([" .. allchar .. allvowels .. "]+)([эЭЕеГгҒғКкҚқ])", "#%1%2")
text = rsubn(text, "#" .. hmzh .. "([эЭЕеГгКк])", "#%1")
-- text = fvowels(text) -- hamza ligatures have been depricated by unicode
text = rsubn(text, '.', mapping)
text = rsubn(text,"-", "ـ") -- Kashida
text = rsubn(text, "#", "")
text = rsubn(text, "_", "")
return text
end
return export
bzwzjzwd56zbuefqt2zll6gw9gpobvf
Modul:kk-convert
828
148290
379734
2026-10-20T00:08:21Z
Hakimi97
2668
Mencipta laman baru dengan kandungan 'local export = {} local vowels = "үҮиИеЕәӘөӨӯӮАаоОЫыІіЯяЮюЁё" local rsubn = mw.ustring.gsub local tt = { ["ү"]="ü",["Ү"]="Ü",["т"]="t",["Т"]="T",["һ"]="h",["Һ"]="H",["р"]="r",["Р"]="R" ,["ф"]="f",["Ф"]="F",["ш"]="ş",["Ш"]="Ş",["н"]="n",["Н"]="N",["п"]="p",["П"]="P" ,["й"]="i",["Й"]="İ",["л"]="l",["Л"]="L",["з"]="z",["З"]="Z",["е"]="e",["Е"]="E" ,["г"]="g",["Г"]="G",["б"]="b",["Б"]="B",["у"]=...'
379734
Scribunto
text/plain
local export = {}
local vowels = "үҮиИеЕәӘөӨӯӮАаоОЫыІіЯяЮюЁё"
local rsubn = mw.ustring.gsub
local tt = {
["ү"]="ü",["Ү"]="Ü",["т"]="t",["Т"]="T",["һ"]="h",["Һ"]="H",["р"]="r",["Р"]="R"
,["ф"]="f",["Ф"]="F",["ш"]="ş",["Ш"]="Ş",["н"]="n",["Н"]="N",["п"]="p",["П"]="P"
,["й"]="i",["Й"]="İ",["л"]="l",["Л"]="L",["з"]="z",["З"]="Z",["е"]="e",["Е"]="E"
,["г"]="g",["Г"]="G",["б"]="b",["Б"]="B",["у"]="u",["У"]="U",["с"]="s",["С"]="S"
,["х"]="x",["Х"]="X",["ы"]="y",["Ы"]="Y",["м"]="m",["М"]="M",["о"]="o",["О"]="O"
,["и"]="i",["И"]="I",["ж"]="j",["Ж"]="J",["к"]="k",["К"]="K",["д"]="d",["Д"]="D"
,["в"]="v",["В"]="V",["а"]="a",["А"]="A",["ң"]="ñ",["Ң"]="Ñ",["ә"]="ä",["Ә"]="Ä"
,["э"]="e",["Э"]="E",["ұ"]="ū",["Ұ"]="Ū",["қ"]="q",["Қ"]="Q",["ғ"]="ğ",["Ғ"]="Ğ"
,["і"]="ı",["І"]="ı",["ө"]="ö",["Ө"]="Ö"
--mostly in loanwords from Russian
,["ё"]="io",["Ё"]="İo",["ц"]="ts",["Ц"]="TS",["ч"]="tş",["Ч"]="Tş",["щ"]="ştş",["Щ"]="Ştş"
,["ъ"]="",["Ъ"]="",["э"]="e",["Э"]="E",["ю"]="iu",["Ю"]="İu"
,["я"]="ia",["Я"]="İa",["ӯ"]="u",["Ӯ"]="U"
};
-- Main translit
function export.tr(text, lang, sc)
if type(text) == "table" then
options = {}
text, script = text.args[1], text.args[2]
end
if not sc then
sc = require("Module:languages").getByCode("kk"):findBestScript(text):getCode()
end
if sc ~= "Cyrl" then
return nil
end
text = rsubn(text, "нг([^" .. vowels .. "])", "ң%1")
text = rsubn(text, "Нг([^" .. vowels .. "])", "Ң%1")
text = rsubn(text, "нг$", "ң")
text = rsubn(text, "([^" .. vowels .. "])я", "%1ә")
text = rsubn(text, "([^" .. vowels .. "])Я", "%1Ә")
text = rsubn(text, "[иИ]([яЯ])", "%1")
--currently it is not clear how these two are romanized
text = rsubn(text, "щ", "ш")
text = rsubn(text, "Щ", "Ш")
text = rsubn(text,"[ьЬ]" , "" )
--remove them if their romanization changes
text = rsubn(text, '.', tt)
return text
end
-- NOT MAIN TRANSLIT
-- Yañalif 1938 version
local yanalif = {
["ү"]="y",["Ү"]="Y",["ш"]="c",["Ш"]="C",["й"]="i",["Й"]="I",["б"]="ʙ"
,["ы"]="ь",["Ы"]="Ь",["и"]="i",["И"]="I",["ж"]="ç",["Ж"]="Ç"
,["ң"]="ꞑ",["Ң"]="Ꞑ",["ә"]="ə",["Ә"]="Ə",["ғ"]="ƣ",["Ғ"]="Ƣ"
,["і"]="j",["І"]="J",["ө"]="ɵ",["Ө"]="Ɵ"
--mostly in loanwords from Russian
,["ё"]="io",["Ё"]="Io",["ч"]="c",["Ч"]="Tc",["щ"]="cc",["Щ"]="Cc"
,["ъ"]="ʼ",["Ъ"]="ʼ",["ю"]="iu",["Ю"]="Iu"
,["я"]="ia",["Я"]="Ia"}
--Yañalif 1927 version
local oldyan = {
["ү"]="y",["Ү"]="Y",["ш"]="c",["Ш"]="C",["й"]="i",["Й"]="I",["б"]="ʙ"
,["ы"]="ь",["Ы"]="Ь",["и"]="j",["И"]="J",["ж"]="ç",["Ж"]="Ç"
,["ң"]="ꞑ",["Ң"]="Ꞑ",["ә"]="ə",["Ә"]="Ə",["ғ"]="ƣ",["Ғ"]="Ƣ"
,["і"]="i",["І"]="I",["ө"]="ɵ",["Ө"]="Ɵ",["в"]="ʙ",["В"]="B"
,["ф"]="p",["Ф"]="P",["х"]="h",["Х"]="H",["у"]="v",["У"]="V"
,["ұ"]="u",["Ұ"]="U"
--mostly in loanwords from Russian
,["ё"]="io",["Ё"]="Io",["ч"]="c",["Ч"]="Tc",["щ"]="cc",["Щ"]="Cc"
,["ъ"]="ʼ",["Ъ"]="ʼ",["ю"]="iu",["Ю"]="Iu"
,["я"]="ia",["Я"]="Ia"}
--Pinyin
local pinyin = {
["ү"]="ü",["Ү"]="Ü",["ш"]="x",["Ш"]="X",["й"]="y",["Й"]="Y"
,["ы"]="e",["Ы"]="E",["и"]="y",["И"]="Y",["у"]="w",["У"]="W"
,["ң"]="ng",["Ң"]="ng",["ә"]="ə",["Ә"]="Ə",["ғ"]="ƣ",["Ғ"]="Ƣ"
,["і"]="i",["І"]="I",["ө"]="ɵ",["Ө"]="Ɵ",["һ"]="ⱨ",["Һ"]="Ⱨ"
,["х"]="h",["Х"]="H",["қ"]="ⱪ",["Қ"]="Ⱪ",["е"]="ê",["Е"]="ê"
,["ұ"]="u",["Ұ"]="U"
--mostly in loanwords from Russian
,["ё"]="io",["Ё"]="Io",["ч"]="q",["Ч"]="Q",["щ"]="x",["Щ"]="X"
,["ъ"]="ʼ",["Ъ"]="ʼ",["ю"]="iu",["Ю"]="Iu",["ь"]="",["Ь"]=""
,["я"]="ia",["Я"]="Ia"}
-- Yanalif 1938 version
function export.yanalif(text, lang, sc)
if type(text) == "table" then
options = {}
text, script = text.args[1], text.args[2]
end
if not sc then
sc = require("Module:languages").getByCode("kk"):findBestScript(text):getCode()
end
if sc ~= "Cyrl" then
return nil
end
text = rsubn(text, "нг([^" .. vowels .. "])", "ң%1")
text = rsubn(text, "Нг([^" .. vowels .. "])", "Ң%1")
text = rsubn(text, "нг$", "ң")
text = rsubn(text, "([^" .. vowels .. "])я", "%1ә")
text = rsubn(text, "([^" .. vowels .. "])Я", "%1Ә")
text = rsubn(text, "[иИ]([яЯ])", "%1")
text = rsubn(text, '.', yanalif)
text = rsubn(text, '.', tt)
return text
end
local frontur = "әӘіІ"
local backur = "АаЫы"
local frontround = "үҮөӨ"
local backround = "ӯӮоОҰұ"
local consonant = "[^" .. vowels .. ". -]"
-- Yanalif pre 1927 version
function export.oldyan(text, lang, sc)
if type(text) == "table" then
options = {}
text, script = text.args[1], text.args[2]
end
if not sc then
sc = require("Module:languages").getByCode("kk"):findBestScript(text):getCode()
end
if sc ~= "Cyrl" then
return nil
end
text = rsubn(text, "нг([^" .. vowels .. "])", "ң%1")
text = rsubn(text, "Нг([^" .. vowels .. "])", "Ң%1")
text = rsubn(text, "нг$", "ң")
text = rsubn(text, "([^" .. vowels .. "])я", "%1ә")
text = rsubn(text, "([^" .. vowels .. "])Я", "%1Ә")
-- check preceeding vowel
-- for w
text = rsubn(text, "([" .. backur .. "])(" .. consonant .. ")у", "%1%2ыу")
text = rsubn(text, "([" .. backround .. "])(" .. consonant .. ")у", "%1%2ұу")
text = rsubn(text, "([" .. frontur .. "])(" .. consonant .. ")у", "%1%2іу")
text = rsubn(text, "([" .. frontround .. "])(" .. consonant .. ")у", "%1%2үу")
-- for j
text = rsubn(text, "([" .. backur .. backround .. "])(" .. consonant .. "?)и", "%1%2ыи")
text = rsubn(text, "([" .. frontur .. frontround .. "])(" .. consonant .. "?)и", "%1%2іи")
-- if none, check following vowel
-- for w
text = rsubn(text, "([^" .. vowels .. "])У(" .. consonant .. "?)([" .. backur .. "])", "%1ЫУ%2%3")
text = rsubn(text, "([^" .. vowels .. "])у(" .. consonant .. "?)([" .. backur .. "])", "%1ыу%2%3")
text = rsubn(text, "([^" .. vowels .. "])У(" .. consonant .. "?)([" .. backround .. "])", "%1ҰУ%2%3")
text = rsubn(text, "([^" .. vowels .. "])у(" .. consonant .. "?)([" .. backround .. "])", "%1ұу%2%3")
text = rsubn(text, "([^" .. vowels .. "])У(" .. consonant .. "?)([" .. frontur .. "])", "%1ІУ%2%3")
text = rsubn(text, "([^" .. vowels .. "])у(" .. consonant .. "?)([" .. frontur .. "])", "%1іу%2%3")
text = rsubn(text, "([^" .. vowels .. "])У(" .. consonant .. "?)([" .. frontround .. "])", "%1ҮУ%2%3")
text = rsubn(text, "([^" .. vowels .. "])у(" .. consonant .. "?)([" .. frontround .. "])", "%1үу%2%3")
-- for j
text = rsubn(text, "([^" .. vowels .. "])и(" .. consonant .. "?)([" .. backur .. backround .. "])", "%1ыи%2%3")
text = rsubn(text, "([^" .. vowels .. "])и(" .. consonant .. "?)([" .. frontur .. frontround .. "])", "%1іи%2%3")
text = rsubn(text, "([^" .. vowels .. "])И(" .. consonant .. "?)([" .. backur .. backround .. "])", "%1ЫИ%2%3")
text = rsubn(text, "([^" .. vowels .. "])И(" .. consonant .. "?)([" .. frontur .. frontround .. "])", "%1ІИ%2%3")
text = rsubn(text, "^И(" .. consonant .. "?)([" .. backur .. backround .. "])", "ЫИ%1%2")
text = rsubn(text, "^И(" .. consonant .. "?)([" .. frontur .. frontround .. "])", "ІИ%1%2")
-- final touches
text = rsubn(text, "[иИ]([яЯ])", "%1")
text = rsubn(text, '.', oldyan)
text = rsubn(text, '.', tt)
return text
end
-- Pinyin
function export.pinyin(text, lang, sc)
if type(text) == "table" then
options = {}
text, script = text.args[1], text.args[2]
end
if not sc then
sc = require("Module:languages").getByCode("kk"):findBestScript(text):getCode()
end
if sc ~= "Cyrl" then
return nil
end
text = rsubn(text, "([^" .. vowels .. "])я", "%1ә")
text = rsubn(text, "([^" .. vowels .. "])Я", "%1Ә")
text = rsubn(text, "[иИ]([яЯ])", "%1")
text = rsubn(text, "([^" .. vowels .. "])[у]", "%1ұу")
text = rsubn(text, "([^" .. vowels .. "])[У]", "%1ұУ")
text = rsubn(text, "^[у]([^" .. vowels .. "])", "ұу%1")
text = rsubn(text, "^[У]([^" .. vowels .. "])", "ұУ%1")
text = rsubn(text, '.', pinyin)
text = rsubn(text, '.', tt)
return text
end
return export
j8c91uxbs7hb9rpdqvnc1g97kj38gl5
Templat:R:LSJ
10
148291
379738
2026-10-20T00:10:54Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Templat:R:LSJ]] ke [[Templat:R:grc:LSJ]]: Penyelarasan nama seperti versi Wikikamus Bahasa Inggeris
379738
wikitext
text/x-wiki
#LENCONG [[Templat:R:grc:LSJ]]
embxa8cuo82w89uak7re6udr3pqr2jj
daba
0
148292
379745
2026-10-20T00:26:36Z
Hakimi97
2668
Mencipta laman baru dengan kandungan '{{:wt:ms/{{FULLPAGENAME}}}}'
379745
wikitext
text/x-wiki
{{:wt:ms/{{FULLPAGENAME}}}}
d7lukscm6n92v7odnzp02a4uq8jtyuf
haup
0
148293
379748
2026-10-20T00:28:10Z
Hakimi97
2668
Mencipta laman baru dengan kandungan '{{:wt:ms/{{FULLPAGENAME}}}}'
379748
wikitext
text/x-wiki
{{:wt:ms/{{FULLPAGENAME}}}}
d7lukscm6n92v7odnzp02a4uq8jtyuf
tumpor
0
148294
379751
2026-10-20T00:29:23Z
Hakimi97
2668
Mencipta laman baru dengan kandungan '{{:wt:ms/{{FULLPAGENAME}}}}'
379751
wikitext
text/x-wiki
{{:wt:ms/{{FULLPAGENAME}}}}
d7lukscm6n92v7odnzp02a4uq8jtyuf
khabor
0
148295
379753
2026-10-20T00:30:25Z
Hakimi97
2668
Mencipta laman baru dengan kandungan '{{:wt:ms/{{FULLPAGENAME}}}}'
379753
wikitext
text/x-wiki
{{:wt:ms/{{FULLPAGENAME}}}}
d7lukscm6n92v7odnzp02a4uq8jtyuf
harapkan pegar, pegar makan padi
0
148296
379756
2026-10-20T04:33:28Z
SNN95
2113
Mencipta laman baru dengan kandungan '==Bahasa Melayu== ===Bentuk alternatif=== * {{l|ms|harapkan pagar, pagar makan padi}} {{qualifier|salah ejaan}} ===Kata kerja=== {{ms-kerja|j=هارڤکن ڤݢر، ڤݢر ماکن ڤادي}} # {{lb|ms|simpulan bahasa}} Satu situasi di mana seseorang yang dipercayai untuk melindungi atau menguruskan sesuatu akhirnya mengkhianati kepercayaan itu dan memusnahkan atau mencurinya sendiri.'
379756
wikitext
text/x-wiki
==Bahasa Melayu==
===Bentuk alternatif===
* {{l|ms|harapkan pagar, pagar makan padi}} {{qualifier|salah ejaan}}
===Kata kerja===
{{ms-kerja|j=هارڤکن ڤݢر، ڤݢر ماکن ڤادي}}
# {{lb|ms|simpulan bahasa}} Satu situasi di mana seseorang yang dipercayai untuk melindungi atau menguruskan sesuatu akhirnya mengkhianati kepercayaan itu dan memusnahkan atau mencurinya sendiri.
ij1ghfy25qq7qrz01ewtg90cx9q03xw
379761
379756
2026-10-20T05:02:04Z
SNN95
2113
379761
wikitext
text/x-wiki
==Bahasa Melayu==
===Etimologi===
Berasal daripada salah terjemah daripada Jawi kepada Rumi {{l|ms|هارڤکن ڤݢر، ڤݢر ماکن ڤادي|t=harapkan pagar, pagar makan padi}}.<ref name="Rusli">[https://www.researchgate.net/profile/Nur-Mohd-Rusli/publication/351915821_Analisis_Kefahaman_Makna_Peribahasa_dalam_Kalangan_Murid_Berdasarkan_Teori_Relevans/links/60afc884299bf13438efbf77/Analisis-Kefahaman-Makna-Peribahasa-dalam-Kalangan-Murid-Berdasarkan-Teori-Relevans.pdf?_sg%5B0%5D=started_experiment_milestone&origin=journalDetail Analisis Kefahaman Makna Peribahasa dalam Kalangan Murid Berdasarkan Teori Relevans]</ref><ref name="Huszka">[https://www.researchgate.net/profile/Balazs-Huszka/publication/393461374_The_Metaphorical_Construction_of_Cultural_Identity_in_Malay_Proverbs/links/686ba7d1e9b6c13c89e71c3f/The-Metaphorical-Construction-of-Cultural-Identity-in-Malay-Proverbs.pdf ''The Metaphorical Construction of Cultural Identity in Malay Proverbs'' (Pembinaan Metafora Identiti Budaya dalam Peribahasa Melayu)]</ref>
===Kata kerja===
{{head|ms|kata kerja}}
# {{lb|ms|simpulan bahasa}} {{misspelling of|ms|harapkan pagar, pagar makan padi}}<ref name="Rusli" /><ref name="Huszka" />
==Rujukan==
{{reflist}}
nkk3jdodoo99daao489d1yv3abql4lh
379764
379761
2026-10-20T05:32:41Z
SNN95
2113
kemaskini dgn rujukan
379764
wikitext
text/x-wiki
==Bahasa Melayu==
===Etimologi===
Berasal daripada salah terjemah daripada Jawi kepada Rumi {{l|ms|هارڤکن ڤݢر، ڤݢر ماکن ڤادي|t=harapkan pagar, pagar makan padi}}.<ref name="Rusli">[https://www.researchgate.net/profile/Nur-Mohd-Rusli/publication/351915821_Analisis_Kefahaman_Makna_Peribahasa_dalam_Kalangan_Murid_Berdasarkan_Teori_Relevans/links/60afc884299bf13438efbf77/Analisis-Kefahaman-Makna-Peribahasa-dalam-Kalangan-Murid-Berdasarkan-Teori-Relevans.pdf?_sg%5B0%5D=started_experiment_milestone&origin=journalDetail Analisis Kefahaman Makna Peribahasa dalam Kalangan Murid Berdasarkan Teori Relevans]</ref><ref name="Huszka">[https://www.researchgate.net/profile/Balazs-Huszka/publication/393461374_The_Metaphorical_Construction_of_Cultural_Identity_in_Malay_Proverbs/links/686ba7d1e9b6c13c89e71c3f/The-Metaphorical-Construction-of-Cultural-Identity-in-Malay-Proverbs.pdf ''The Metaphorical Construction of Cultural Identity in Malay Proverbs'' (Pembinaan Metafora Identiti Budaya dalam Peribahasa Melayu)]</ref><ref name="UKM">[https://journalarticle.ukm.my/9956/1/14135-38839-1-SM.pdf Padi sebagai Tanda dalam Peribahasa Melayu]</ref>
===Kata kerja===
{{head|ms|kata kerja}}
# {{lb|ms|simpulan bahasa}} {{misspelling of|ms|harapkan pagar, pagar makan padi}}<ref name="Rusli" /><ref name="Huszka" /><ref name="UKM" />
==Rujukan==
{{reflist}}
p3kwm3xpxucctpn5usx9yd3lcmz47vp
harapkan pagar, pagar makan padi
0
148297
379757
2026-10-20T04:36:30Z
SNN95
2113
Mencipta laman baru dengan kandungan '==Bahasa Melayu== ===Etimologi=== Berasal daripada salah terjemah daripada Jawi kepada Rumi {{l|ms|هارڤکن ڤݢر، ڤݢر ماکن ڤادي|t=harapkan pegar, pegar makan padi}}. ===Kata kerja=== {{head|ms|verb}} # {{lb|ms|simpulan bahasa}} {{misspelling of|ms|harapkan pegar, pegar makan padi}}'
379757
wikitext
text/x-wiki
==Bahasa Melayu==
===Etimologi===
Berasal daripada salah terjemah daripada Jawi kepada Rumi {{l|ms|هارڤکن ڤݢر، ڤݢر ماکن ڤادي|t=harapkan pegar, pegar makan padi}}.
===Kata kerja===
{{head|ms|verb}}
# {{lb|ms|simpulan bahasa}} {{misspelling of|ms|harapkan pegar, pegar makan padi}}
oo73286cf0aod2i5gnyvxvbngacwrc0
379758
379757
2026-10-20T04:37:02Z
SNN95
2113
/* Kata kerja */
379758
wikitext
text/x-wiki
==Bahasa Melayu==
===Etimologi===
Berasal daripada salah terjemah daripada Jawi kepada Rumi {{l|ms|هارڤکن ڤݢر، ڤݢر ماکن ڤادي|t=harapkan pegar, pegar makan padi}}.
===Kata kerja===
{{head|ms|kata kerja}}
# {{lb|ms|simpulan bahasa}} {{misspelling of|ms|harapkan pegar, pegar makan padi}}
i1lz56vvs96q4yuspwb8jmceix9av00
379762
379758
2026-10-20T05:03:13Z
SNN95
2113
379762
wikitext
text/x-wiki
==Bahasa Melayu==
===Bentuk alternatif===
* {{l|ms|harapkan pegar, pegar makan padi}} {{qualifier|salah ejaan}}<ref>[https://www.researchgate.net/profile/Nur-Mohd-Rusli/publication/351915821_Analisis_Kefahaman_Makna_Peribahasa_dalam_Kalangan_Murid_Berdasarkan_Teori_Relevans/links/60afc884299bf13438efbf77/Analisis-Kefahaman-Makna-Peribahasa-dalam-Kalangan-Murid-Berdasarkan-Teori-Relevans.pdf?_sg%5B0%5D=started_experiment_milestone&origin=journalDetail Analisis Kefahaman Makna Peribahasa dalam Kalangan Murid Berdasarkan Teori Relevans]</ref><ref>[https://www.researchgate.net/profile/Balazs-Huszka/publication/393461374_The_Metaphorical_Construction_of_Cultural_Identity_in_Malay_Proverbs/links/686ba7d1e9b6c13c89e71c3f/The-Metaphorical-Construction-of-Cultural-Identity-in-Malay-Proverbs.pdf ''The Metaphorical Construction of Cultural Identity in Malay Proverbs'' (Pembinaan Metafora Identiti Budaya dalam Peribahasa Melayu)]</ref>
===Kata kerja===
{{ms-kerja|j=هارڤکن ڤݢر، ڤݢر ماکن ڤادي}}
# {{lb|ms|simpulan bahasa}} Satu situasi di mana seseorang yang dipercayai untuk melindungi atau menguruskan sesuatu akhirnya mengkhianati kepercayaan itu dan memusnahkan atau mencurinya sendiri.
==Rujukan==
{{reflist}}
4germa67bka2f5m8qm2k5y6lcb42b8m
379763
379762
2026-10-20T05:32:32Z
SNN95
2113
kemaskini dgn rujukan
379763
wikitext
text/x-wiki
==Bahasa Melayu==
===Bentuk alternatif===
* {{l|ms|harapkan pegar, pegar makan padi}} {{qualifier|salah ejaan}}<ref name="Rusli">[https://www.researchgate.net/profile/Nur-Mohd-Rusli/publication/351915821_Analisis_Kefahaman_Makna_Peribahasa_dalam_Kalangan_Murid_Berdasarkan_Teori_Relevans/links/60afc884299bf13438efbf77/Analisis-Kefahaman-Makna-Peribahasa-dalam-Kalangan-Murid-Berdasarkan-Teori-Relevans.pdf?_sg%5B0%5D=started_experiment_milestone&origin=journalDetail Analisis Kefahaman Makna Peribahasa dalam Kalangan Murid Berdasarkan Teori Relevans]</ref><ref name="Huszka">[https://www.researchgate.net/profile/Balazs-Huszka/publication/393461374_The_Metaphorical_Construction_of_Cultural_Identity_in_Malay_Proverbs/links/686ba7d1e9b6c13c89e71c3f/The-Metaphorical-Construction-of-Cultural-Identity-in-Malay-Proverbs.pdf ''The Metaphorical Construction of Cultural Identity in Malay Proverbs'' (Pembinaan Metafora Identiti Budaya dalam Peribahasa Melayu)]</ref><ref name="UKM">[https://journalarticle.ukm.my/9956/1/14135-38839-1-SM.pdf Padi sebagai Tanda dalam Peribahasa Melayu]</ref>
===Kata kerja===
{{ms-kerja|j=هارڤکن ڤݢر، ڤݢر ماکن ڤادي}}
# {{lb|ms|simpulan bahasa}} Satu situasi di mana seseorang yang dipercayai untuk melindungi atau menguruskan sesuatu akhirnya mengkhianati kepercayaan itu dan memusnahkan atau mencurinya sendiri.
==Rujukan==
{{reflist}}
811r0xw9cgc2vmnza8dfurw9pv1l629
Modul:bnt-com-sortkey/doc
828
148298
379766
2026-10-20T08:06:14Z
Hakimi97
2668
Mencipta laman baru dengan kandungan '{{documentation needed}}<!-- Replace this with a short description of the purpose of the module, and how to use it. --> {{module cat|fam:bnt-com}}'
379766
wikitext
text/x-wiki
{{documentation needed}}<!-- Replace this with a short description of the purpose of the module, and how to use it. -->
{{module cat|fam:bnt-com}}
999fsvkj7i6griav629qz8nvegno8xv
penyepuhlindapan
0
148299
379774
2026-10-20T11:13:33Z
Tofeiku
2269
cipta
379774
wikitext
text/x-wiki
==Bahasa Melayu==
===Kata nama===
{{ms-noun|j=ڤڽڤوهليندڤن}}
# {{lb|ms|kejuruteraan}} Proses atau perbuatan menyepuh lindap.
===Etimologi===
{{af|ms|sepuh lindap|peN- -an}}
===Rujukan===
* {{R:KDP|2=2061}}
===Bacaan lanjut===
* {{R:PRPM}}
6p8q0gsikhgrg3whsixrusqimspiqk0