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;">&nbsp;</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>&nbsp;</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] = "&mdash;" 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 "&mdash;" args.short_n = args.short_n or "&mdash;" args.short_f = args.short_f or "&mdash;" args.short_p = args.short_p or "&mdash;" 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 "&mdash;" args.short_n = args.short_n or "&mdash;" args.short_f = args.short_f or "&mdash;" args.short_p = args.short_p or "&mdash;" 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, "^" .. "[-]", "&#45;") 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,"&#45;", "ـ") -- 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