Mô đun:grc-decl/decl
Giao diện
local module_path = 'Module:grc-decl/decl'
local m_classes = mw.loadData(module_path .. '/classes')
local m_paradigms = mw.loadData(module_path .. '/staticdata/paradigms')
local m_dialect_groups = mw.loadData(module_path .. '/staticdata/dialects')
local m_decl_data = require(module_path .. '/data')
local m_accent = require('Module:grc-accent')
local usub = mw.ustring.sub
local decompose = mw.ustring.toNFD
local compose = mw.ustring.toNFC
-- Equivalent to mw.ustring.len.
local function ulen(str)
local _, length = string.gsub(str, '[\1-\127\194-\244][\128-\191]*', '')
return length
end
-- Basic string functions can be used when there are no character sets or
-- quantifiers.
local ufind = mw.ustring.find
local ugsub = mw.ustring.gsub
local export = {
inflections = m_decl_data.inflections,
adjinflections = m_decl_data.adjinflections,
adjinflections_con = m_decl_data.adjinflections_con,
}
local function quote(text)
return "“" .. text .. "”"
end
local function get_accent_info(form)
-- Get position of accent (nth vowel from beginning of word).
local accent = {}
local is_suffix = form:sub(1, 1) == '-'
accent.position, accent.type = m_accent.detect_accent(form)
-- Position: position of accent from beginning of word (number) or nil.
-- Accent: accent name (string) or nil.
-- Form must have an accent, unless it is a suffix.
if not is_suffix and not next(accent) then
error('No accent detected on ' .. quote(form) ..
'. Please add an accent by copying this template and placing ' ..
quote('/') .. ' for acute or ' .. quote('~') ..
' for circumflex after the vowel that should be accented: {{subst:chars|grc|' .. form .. '}}.')
end
-- Accent term as proxy for distinguishing between oxytone and perispomenon.
accent.term = m_accent.get_accent_term(form)
return accent, is_suffix
end
local form_redirects = {
AS = 'NS', VS = 'NS',
DD = 'GD', AD = 'ND', VD = 'ND',
AP = 'NP', VP = 'NP',
}
local form_metatable = {
__index = function (self, form_code)
if type(form_code) ~= 'string' then return nil end
if form_redirects[form_code] then
return self[form_redirects[form_code]]
elseif form_redirects[form_code:sub(2)] then
return self[form_code:sub(1, 1) .. form_redirects[form_code:sub(2)]]
-- If this is a neuter form but not in the nominative case,
-- use the corresponding masculine form.
elseif form_code:match('N[^N].') then
return self['M' .. form_code:sub(2)]
end
end,
}
local function add_redirects(form_table)
return setmetatable(form_table, form_metatable)
end
local function add_forms(args)
if not args.irregular then
--add stem to forms
local function add_stem(forms)
return forms:gsub('%$', args.stem)
end
-- args.suffix indicates that this is a paradigm for an unaccented suffix,
-- such as [[-εια]].
if args.indeclinable then
for k, v in pairs(args.ctable) do
if k:find('[NGDAV][SDP]') then -- only format case-number forms
args.ctable[k] = args[2]
end
end
elseif args.suffix and not next(args.accent) then
for k, v in pairs(args.ctable) do
if k:find('[NGDAV][SDP]') then -- only format case-number forms
args.ctable[k] = add_stem(v)
end
end
else
-- If the term is not a suffix and no accent was detected, then
-- get_accent_info above must throw an error,
-- or else there will be an uncaught error here.
local add_circumflex = args.accent.type == 'circumflex'
local recessive = -3
-- Force recessive accent in the Lesbian dialect.
local accent_position = args.dial == 'les' and recessive
or args.accent.position
-- Circumflex on monosyllabic DS and AS in consonant-stem third-
-- declension nouns: for example, Τρῷ and Τρῶ, DS and AS of Τρώς.
local DS_AS = args.accent_alternating == true
-- Added by "kles" function: for example, Περίκλεις.
local VS = args.recessive_VS and recessive
local synaeresis = args.synaeresis
local add_accent = m_accent.add_accent
local function add_accent_to_forms(forms, code)
return ugsub(forms,
'[^/]+',
function(form)
return add_accent(form,
code == 'VS' and VS or accent_position,
{
synaeresis = synaeresis,
circumflex = (code == 'DS' or code == 'AS') and DS_AS or add_circumflex,
short_diphthong = true,
force_antepenult = args.force_antepenult,
})
end)
end
for k, v in pairs(args.ctable) do
if k:find('[NGDAV][SDP]') then -- only format case-number forms
args.ctable[k] = add_accent_to_forms(add_stem(v), k)
end
end
end
end
end
local gender_codes = { 'M', 'F', 'N' }
local case_codes = { 'N', 'G', 'D', 'A', 'V' }
local number_codes = { 'S', 'D', 'P' }
local function handle_noun_overrides(form_table, override_table)
for _, case in ipairs(case_codes) do
for _, number in ipairs(number_codes) do
local key = case .. number
if override_table[key] then
require('Module:debug').track('grc-decl/form-override')
end
form_table[key] = override_table[key] or form_table[key]
end
end
end
local function handle_adjective_overrides(form_table, override_table)
for _, gender in ipairs(gender_codes) do
for _, case in ipairs(case_codes) do
for _, number in ipairs(number_codes) do
local key = gender .. case .. number
if override_table[key] then
require('Module:debug').track('grc-adecl/form-override')
end
form_table[key] = override_table[key] or form_table[key]
end
end
end
end
--[=[
Gets stem-ending combinations from [[Module:grc-decl/decl/data]]
and [[Module:grc-decl/decl/staticdata]]. Called a single time to get forms
of a noun, and two or three times by make_decl_adj for each of the genders
of an adjective.
]=]
function export.make_decl(args, decl, root, is_adjective)
if not export.inflections[decl] then
error('Inflection type ' .. quote(decl) .. ' not found in [[' .. module_path .. "/data]].")
end
if args.adjective and args.irregular then
error('Irregular adjectives are not handled by make_decl.')
end
if not root then
error('No root for ' .. args[1] .. '.')
end
args.stem = root
export.inflections[decl](args)
args.gender[1] = args.gender[1] or args.ctable['g']
args.declheader = args.declheader or args.ctable['decl']
add_forms(args)
if not is_adjective then
handle_noun_overrides(args.ctable, args)
end
add_redirects(args.ctable)
end
-- String (comparative ending), function (input: root; output: comparative),
-- false (the declension has no comparative form), or nil (use the if-statements
-- to determine a comparative form).
local decl_to_comp = {
['1&3-ᾰν'] = 'ᾰ́ντερος',
['1&3-εν'] = 'έντερος',
['1&3-εσσ'] = 'έστερος', -- hopefully this is general?
['1&3-ups'] = 'ῠ́τερος',
['3rd-cons'] = function(root)
local last2 = usub(root, -2)
if last2 == 'ον' then
return root .. 'έστερος'
elseif last2 == 'εσ' then
return root:gsub('εσ$', 'έστερος')
else
return false
end
end,
['1&3-οτ'] = false,
['3rd-εσ'] = 'έστερος',
['3rd-εσ-open'] = 'έστερος',
['1&2-alp-con'] = 'εώτερος', -- I assume—though I can't find examples
['1&2-eta-con'] = 'εώτερος',
}
local function retrieve_comp(root, decl_type)
local data = decl_to_comp[decl_type]
if not data then
return data
elseif type(data) == 'string' then
return root .. data
elseif type(data) == 'function' then
return data(root)
else
error('Data for ' .. decl_type .. ' is invalid.')
end
end
-- Constructs an adverb, comparative, and superlative.
function export.make_acs(args)
-- input:
-- strings
local root, decl_type = args.root, args.decl_type
-- tables
local accent, atable = args.accent, args.atable
-- output:
local comp = retrieve_comp(root, decl_type)
local super, adv
if comp == nil then
local alpha_nonultima = decl_type == '1&2-alp' and
accent.term ~= 'oxytone' and
accent.term ~= 'perispomenon'
local last3 = usub(root, -3)
if alpha_nonultima and last3 == 'τερ' then
comp = '(' .. atable['MNS'] .. ')'
adv = atable['NNS']
-- ?
-- comp = nil
elseif alpha_nonultima and last3 == 'τᾰτ' then
super = '(' .. atable['MNS'] .. ')'
adv = atable['NNP']
-- ?
-- super = nil
elseif decl_type:find('ντ') then
comp = nil -- participles
super = nil
elseif (m_accent.get_weight(root, 1) == "light" or decl_type:find('att$')) then
comp = root .. 'ώτερος'
else
comp = root .. 'ότερος'
end
end
atable.adv = adv
-- Actually neuter accusative singular. This is correct for -τερος and
-- for μείζων; also for all comparatives in -ων?
or args.comparative and atable.NNS
-- actually neuter accusative plural
or args.superlative and atable.NNP
or atable.MGP and atable.MGP:gsub('ν$', 'ς'):gsub('ν<', 'ς<')
atable.comp = comp
atable.super = super or comp and comp:gsub('ερος$', 'ᾰτος')
-- Remove comparative and superlative if adjective is a comparative or superlative.
-- Parameters that trigger this condition are |deg=comp, |deg=super, and the
-- deprecated |form=comp.
if args.comparative or args.superlative then
atable.comp, atable.super = nil, nil
end
for _, form in ipairs { 'adv', 'comp', 'super' } do
if args[form] == "-" then
atable[form] = nil
elseif args[form] then
atable[form] = args[form]
end
end
end
--[[
noun_table contains case-number forms.
adjective_table will contain gender-case-number forms.
override_table contains gender-case-number forms that will override the
forms in noun_table.
]]
local function transfer_forms_to_adjective_table(adjective_table, noun_table, gender_code)
for case_and_number_code, form in pairs(noun_table) do
adjective_table[gender_code .. case_and_number_code] = form
end
end
--[=[
Interprets the table for the adjective's inflection type
in [[Module:grc-decl/decl/staticdata]].
]=]
function export.make_decl_adj(args, ct)
if args.irregular then
return export.inflections['irreg-adj'](args)
end
--[[
Two possibilities, with the indices of the table of endings
and the stem augmentation that they use:
- masculine–feminine (1, a1), neuter (2, a2)
- masculine (1, a1), feminine (2, a2), neuter (3, a1)
]]
-- Masculine or masculine and feminine forms.
export.make_decl(args, ct[1], args.root .. (ct.a1 or ''), true)
transfer_forms_to_adjective_table(args.atable, args.ctable, 'M')
-- Feminine or neuter forms.
if ct[2] then
export.make_decl(args, ct[2], args.fstem or (args.root .. (ct.a2 or '')), true)
transfer_forms_to_adjective_table(args.atable, args.ctable, 'F')
end
export.make_decl(args, ct[3], args.root .. (ct.a1 or ''), true)
transfer_forms_to_adjective_table(args.atable, args.ctable, 'N')
add_redirects(args.atable)
args.ctable = nil
export.make_acs(args)
handle_adjective_overrides(args.atable, args)
args.adeclheader = ct.adeclheader or 'Declension'
end
-- This function requires NFC forms for [[Module:grc-decl/decl/classes]],
-- but NFD forms for [[Module:grc-decl/decl/data]].
function export.get_decl(args)
if args.indeclinable then
if not args[2] then error("Specify the indeclinable form in the 2nd parameter.") end
args.decl_type, args.root = 'indecl', ''
return
elseif args.irregular then
if args.gender[1] == "N" then
args.decl_type, args.root = 'irregN', ''
else
args.decl_type, args.root = 'irreg', ''
end
return
elseif not (args[1] and args[2]) then
error("Use the 1st and 2nd parameters for the nominative and genitive singular.")
end
local infl_info = m_classes.infl_info.noun
args[1] = compose(args[1])
args[2] = compose(args[2])
local arg1, arg2 = args[1], args[2]
local nom_without_accent = compose(m_accent.strip_tone(arg1))
local gen_without_accent = compose(m_accent.strip_tone(arg2))
local decl_types_by_genitive_ending, decl_type, root
local nominative_matches = {}
for i = -infl_info.longest_nominative_ending, -1 do
local nominative_ending = usub(nom_without_accent, i)
local decl_types_by_genitive_ending = infl_info[nominative_ending]
-- If decl_types_by_genitive_ending is a string, then it is the key of
-- another table in infl_info, a nominative ending with a macron or
-- breve (ι → ῐ).
if type(decl_types_by_genitive_ending) == "string" then
decl_types_by_genitive_ending = infl_info[decl_types_by_genitive_ending]
end
if decl_types_by_genitive_ending then
table.insert(nominative_matches, decl_types_by_genitive_ending)
root = usub(nom_without_accent, 1, -1 - ulen(nominative_ending))
for i = -6, -1 do
local genitive_ending = usub(gen_without_accent, i)
local name = decl_types_by_genitive_ending[genitive_ending]
if name then
decl_type = name
break
end
end
if decl_type then
break
end
end
end
args.accent, args.suffix = get_accent_info(arg1)
if decl_type and root then
if args.contracted == 'false' and not decl_type:find('open') then
decl_type = decl_type .. '-open'
end
args.decl_type, args.root = decl_type, decompose(root)
return
elseif gen_without_accent:find('ος$') then
local root = decompose(usub(gen_without_accent, 1, -3))
if args.gender[1] == "N" or ufind(root, 'α[̆̄]τ$') and not (args.gender[1] == "M" and args.gender[2] == "F") then
args.decl_type, args.root = '3rd-N-cons', root
else
args.decl_type, args.root = '3rd-cons', root
end
return
end
if nominative_matches[1] then
local m_table = require 'Module:table'
local fun, grc =
require 'Module:fun', require 'Module:languages'.getByCode 'grc'
local make_sort_key = fun.memoize(
function (term)
return (grc:makeSortKey(term))
end)
if nominative_matches[2] then
local new_nominative_matches = {}
for _, matches in ipairs(nominative_matches) do
for k, v in pairs(matches) do
new_nominative_matches[k] = v
end
end
nominative_matches = new_nominative_matches
else
nominative_matches = nominative_matches[1]
end
local gens = require 'Module:fun'.map(
function (gen)
return quote("-" .. gen)
end,
m_table.keysToList(
nominative_matches,
function (gen1, gen2)
local sort_key1, sort_key2 =
make_sort_key(gen1), make_sort_key(gen2)
if sort_key1 == sort_key2 then
return gen1 < gen2
else
return sort_key1 < sort_key2
end
end))
local agreement
if #gens > 1 then
agreement = { "Declensions were", "s ", " do" }
else
agreement = { "A declension was", " ", " does" }
end
gens = table.concat(gens, ", ")
error(agreement[1] .. " found that matched the ending of the nominative form " .. quote(arg1) ..
", but the genitive ending" .. agreement[2] .. gens ..
agreement[3] .. " not match the genitive form " .. quote(arg2) .. ".")
else
for nom, gens in pairs(m_classes.ambig_forms) do
if arg1:find(nom .. "$") then
for gen, _ in pairs(gens) do
if arg2:find(gen .. "$") then
error("No declension found for nominative " .. quote(arg1) .. " and genitive " .. quote(arg2) ..
". There are two declensions with nominative " .. quote("-" .. nom) ..
" and genitive " .. quote("-" .. gen) ..
". To indicate which one you mean, mark the vowel length of the endings with a macron or breve.")
end
end
end
end
error("Can’t find a declension type for nominative " .. quote(arg1) .. " and genitive " .. quote(arg2) .. ".")
end
end
-- This function requires NFC forms for [[Module:grc-decl/decl/classes]],
-- but NFD forms for [[Module:grc-decl/decl/data]].
function export.get_decl_adj(args)
if args.irregular then
args.decl_type, args.root = 'irreg', ''
return
elseif not args[1] then
error('Use the 1st and 2nd parameters for the masculine and the ' ..
'feminine or neuter nominative singular, or the first parameter ' ..
' alone for the 3rd declension stem.')
end
args[1] = compose(args[1])
if args[2] then
args[2] = compose(args[2])
end
local arg1, arg2 = args[1], args[2]
local mstrip = compose(m_accent.strip_tone(arg1))
local fstrip
if arg2 then
fstrip = compose(m_accent.strip_tone(arg2))
else
args.accent, args.suffix = get_accent_info(arg1)
args.decl_type, args.root = '3rd-cons', decompose(mstrip)
return
end
local infl_info = m_classes.infl_info.adj
-- See if last three or two characters of masc have an entry.
local masc, decl
for i = -infl_info.longest_masculine_ending, -2 do
local ending = usub(mstrip, i)
local data = infl_info[ending]
if data then
masc = ending
decl = data
break
end
end
-- Allows redirecting, so that macrons or breves can be omitted for instance.
if type(decl) == "string" then
decl = infl_info[decl]
end
if decl then
-- Look for a feminine ending that matches the end of the feminine form.
local fem, name
for feminine, decl_name in pairs(decl) do
if fstrip:find(feminine .. '$') then
fem = feminine
name = decl_name:gsub("%d$", "")
break
end
end
if fem then
args.accent, args.suffix = get_accent_info(arg1)
-- The only indication that λέγων, λέγουσᾰ (stem λεγοντ-) and
-- ποιῶν, ποιοῦσᾰ (stem ποιουντ-) have different stems is the
-- accentuation of the masculine form.
if name == '1&3-οντ' and args.accent.term == 'perispomenon' then
name = '1&3-οντ-con'
end
if not export.adjinflections[name] then
error('Inflection recognition failed. Function for generated inflection code ' ..
quote(name) .. ' not found in [[' .. module_path .. "/data]].")
end
args.decl_type, args.root = name, decompose(mstrip:gsub(masc .. "$", ""))
return
else
-- No declension type found.
local fems = {}
local is_neuter = false
for fem in pairs(decl) do
if fem == "ον" then
is_neuter = true
end
table.insert(fems, quote("-" .. fem))
end
fems = table.concat(fems, ", ")
local agreement = { "A declension was", " ", " does" }
if #fems > 1 then
agreement = { "Declensions were", "s ", " do" }
end
error(agreement[1] .. " found that matched the ending of the masculine " .. quote(arg1) ..
", but the corresponding feminine" .. (is_neuter and " and neuter" or "") .. " ending" .. agreement[2] .. fems ..
agreement[3] .. " not match the feminine " .. quote(arg2) .. ".")
end
end
error("Can’t find a declension type for masculine " .. quote(arg1) .. " and feminine or neuter " .. quote(arg2) .. ".")
end
--[[
Returns a table containing the inflected forms of the article,
to be placed before each inflected noun form.
]]
function export.infl_art(args)
if args.dial == 'epi' or args.adjective or args.no_article then
return {}
end
local art = {}
local arttable
if args.gender[1] then
arttable = m_paradigms.art_att[args.gender[1]]
else
error('Gender not specified.')
end
for code, suffix in pairs(arttable) do
if (args.gender[1] == "M" and args.gender[2] == "F") and
m_paradigms.art_att.M[code] ~= m_paradigms.art_att.F[code] then
art[code] = m_paradigms.art_att.M[code] .. ', ' .. m_paradigms.art_att.F[code]
else
art[code] = suffix
end
end
if args.gender[1] == 'F' then
if m_dialect_groups['nonIA'][args.dial] then
art['NS'] = 'ᾱ̔' -- 104.1-4
art['GS'] = 'τᾶς'
art['DS'] = 'τᾷ'
art['AS'] = 'τᾱ̀ν'
end
if args.dial == 'the' or args.dial == 'les' then
art['DS'] = 'τᾶ' -- 39
elseif args.dial == 'boi' or args.dial == 'ara' or args.dial == 'ele' then
art['DS'] = 'ται' -- 104.3
end
if m_dialect_groups['nonIA'][args.dial] then
art['GP'] = 'τᾶν' -- 104.6
end
if args.dial == 'ato' then
art['DP'] = 'τῆσῐ(ν)' -- 104.7
elseif args.dial == 'ion' then
art['DP'] = 'τῇσῐ(ν)' -- 104.7
end
if m_dialect_groups['buck78'][args.dial] then
art['AP'] = 'τᾰ̀ς' -- 104.8
elseif args.dial == 'kre' or args.dial == 'arg' then
art['AP'] = 'τὰνς'
elseif args.dial == 'les' then
art['AP'] = 'ταῖς'
elseif args.dial == 'ele' then
art['AP'] = 'ταὶρ'
end
if args.dial == 'kre' or args.dial == 'les' or args.dial == 'kyp' then
art['NS'] = 'ᾱ̓' -- 57
art['NP'] = 'αἰ'
elseif args.dial == 'ele' then
art['NS'] = 'ᾱ̓'
art['NP'] = 'ταὶ'
elseif args.dial == 'boi' then
art['NP'] = 'τὴ' -- 104.5
elseif m_dialect_groups['west'][args.dial] then --boeotian is covered above
art['NP'] = 'ταὶ'
end
elseif args.gender[1] == 'M' or args.gender[1] == 'N' then
if args.dial == 'the' then
art['GS'] = 'τοῖ' -- 106.1
art['DS'] = 'τοῦ' -- 23
art['ND'] = 'τοὺ'
art['GP'] = 'τοῦν'
end
if args.dial == 'les' then
art['DS'] = 'τῶ' -- 106.2
elseif args.dial == 'boi' or args.dial == 'ara' or args.dial == 'ele' or args.dial == 'eub' then
art['DS'] = 'τοι' -- 106.2
end
if args.dial == 'ato' or args.dial == 'ion' then
art['DP'] = 'τοῖσῐ(ν)' -- 106.4
end
if args.gender[1] == 'M' then
if m_dialect_groups['buck78'][args.dial] then
art['AP'] = 'τὸς' -- 106.5
elseif args.dial == 'kre' or args.dial == 'arg' then
art['AP'] = 'τὸνς'
elseif args.dial == 'les' then
art['AP'] = 'τοῖς'
elseif args.dial == 'ele' then
art['AP'] = 'τοὶρ'
elseif m_dialect_groups['severe'][args.dial] or args.dial == 'boi' then
art['AP'] = 'τὼς'
end
if args.dial == 'kre' or args.dial == 'les' or args.dial == 'kyp' then
art['NS'] = 'ὀ' -- 57
art['NP'] = 'οἰ'
elseif args.dial == 'ele' then
art['NS'] = 'ὀ'
art['NP'] = 'τοὶ'
elseif m_dialect_groups['west'][args.dial] or args.dial == 'boi' then
art['NP'] = 'τοὶ'
end
end
if args.dial == 'ele' then
art['GD'] = 'τοίοις'
-- elseif args.dial == 'ara' then
-- art['GD'] = 'τοιυν'
end
end
return art
end
local lang = require("Module:languages").getByCode("grc")
local function tag(text)
return require("Module:script utilities").tag_text("-" .. text, lang)
end
local function print_detection_table(detection_table, labels, noun)
local out = require('Module:array')()
local function sort(item1, item2)
-- Put 'longest_nominative_ending' and 'longest_masculine_ending' first.
if item1:find '^longest' or item2:find '^longest' then
return item1:find '^longest' ~= nil
end
local sort1, sort2 = (lang:makeSortKey(item1)), (lang:makeSortKey(item2))
local decomp_length1, decomp_length2 = ulen(decompose(item1)), ulen(decompose(item2))
if sort1 == sort2 then
-- Sort ᾱ or ᾰ before α.
if decomp_length1 > decomp_length2 then
return true
else
return false
end
else
return sort1 < sort2
end
end
for key1, value1 in require("Module:table").sortedPairs(detection_table, sort) do
if key1:find '^longest' then
out:insert('* ' .. key1:gsub('_', ' ') .. ': ' .. value1 .. ' characters')
else
table.insert(out, "\n* " .. labels[1] .. " " .. tag(key1))
if type(value1) == "string" then
out:insert(" → " .. tag(value1))
elseif type(value1) == "table" then
for key2, value2 in require("Module:table").sortedPairs(value1, sort) do
-- mw.log(len(key1), len(key2))
out:insert("\n** " ..
(noun and labels[2] or key2 == "ον" and "neuter" or "feminine") ..
" " .. tag(key2) .. ": <code>" .. value2 .. "</code>")
if noun then
out:insert(" (<code>" .. (m_classes.conversion[value2] or "?") .. "</code>)")
end
end
end
end
end
return out:concat()
end
function export.show_noun_categories(frame)
return print_detection_table(m_classes.infl_info.noun, { "nominative", "genitive" }, true)
end
function export.show_adj_categories(frame)
return print_detection_table(m_classes.infl_info.adj, { "masculine", "feminine or neuter" })
end
return export