Wikikamus
mswiktionary
https://ms.wiktionary.org/wiki/Wikikamus:Laman_Utama
MediaWiki 1.47.0-wmf.21
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:it-noun
10
6414
375794
112704
2026-09-24T13:15:20Z
Hakimi97
2668
Mengemas kini templat
375794
wikitext
text/x-wiki
<includeonly>{{#invoke:it-headword|show|nouns}}</includeonly><!--
--><noinclude>{{documentation}}</noinclude>
ipg871j9ib1r70d8zwe6abt8khnonbf
375796
375794
2026-09-24T13:24:46Z
Hakimi97
2668
375796
wikitext
text/x-wiki
<includeonly>{{#invoke:it-headword|show|Kata nama}}</includeonly><!--
--><noinclude>{{documentation}}</noinclude>
56x7l4zltaed9or3u6lvj05vx2od8cl
volontario
0
6415
375826
106609
2026-09-25T04:10:35Z
Hakimi97
2668
Membetulkan templat
375826
wikitext
text/x-wiki
== Bahasa Itali ==
===Kata sifat===
{{it-adj}}
# [[sukarela]].
===Kata nama===
{{it-noun|m}}
# [[sukarelawan]], [[sukarelawati]].
t9u074v3fa3enliq1kf7ihr7ypox1em
Modul:utilities
828
9758
375841
265760
2026-09-25T08:59:56Z
SNN95
2113
kemaskini
375841
Scribunto
text/plain
local export = {}
local headword_data_module = "Module:headword/data"
local languages_module = "Module:languages"
local links_module = "Module:links"
local load_module = "Module:load"
local pages_module = "Module:pages"
local script_utilities_module = "Module:script utilities"
local scripts_module = "Module:scripts"
local string_utilities_module = "Module:string utilities"
local utilities_data_module = "Module:utilities/data"
local mw = mw
local anchor_encode = mw.uri.anchorEncode
local concat = table.concat
local format_categories -- Defined below.
local ipairs = ipairs
local require = require
local type = type
local unstrip = mw.text.unstrip
--[==[
Loaders for functions in other modules, which overwrite themselves with the target function when called. This ensures modules are only loaded when needed, retains the speed/convenience of locally-declared pre-loaded functions, and has no overhead after the first call, since the target functions are called directly in any subsequent calls.]==]
local function decode_entities(...)
decode_entities = require(string_utilities_module).decode_entities
return decode_entities(...)
end
local function get_script(...)
get_script = require(scripts_module).getByCode
return get_script(...)
end
local function is_content_page(...)
is_content_page = require(pages_module).is_content_page
return is_content_page(...)
end
local function load_data(...)
load_data = require(load_module).load_data
return load_data(...)
end
local function remove_links(...)
remove_links = require(links_module).remove_links
return remove_links(...)
end
local function tag_text(...)
tag_text = require(script_utilities_module).tag_text
return tag_text(...)
end
local function trim(...)
trim = require(string_utilities_module).trim
return trim(...)
end
local function uupper(...)
uupper = require(string_utilities_module).upper
return uupper(...)
end
--[==[
Loaders for objects, which load data (or some other object) into some variable, which can then be accessed as "foo or get_foo()", where the function get_foo sets the object to "foo" and then returns it. This ensures they are only loaded when needed, and avoids the need to check for the existence of the object each time, since once "foo" has been set, "get_foo" will not be called again.]==]
local catfix_scripts
local function get_catfix_scripts()
catfix_scripts, get_catfix_scripts = load_data(utilities_data_module).catfix_scripts, nil
return catfix_scripts
end
local current_title
local function get_current_title()
current_title, get_current_title = mw.title.getCurrentTitle(), nil
return current_title
end
local defaultsort
local function get_defaultsort()
defaultsort, get_defaultsort = load_data(headword_data_module).page.pagename_defaultsort, nil
return defaultsort
end
local pagename
local function get_pagename()
pagename, get_pagename = load_data(headword_data_module).page.encoded_pagename, nil
return pagename
end
local und
local function get_und()
und, get_und = require(languages_module).getByCode("und"), nil
return und
end
do
local function handle_url(capture)
return capture:match("https?://[^%s%]]+%s([^%]]+)") or ""
end
--[==[
A helper function to strip wiki markup, giving the plaintext of what is displayed on the page.]==]
function export.get_plaintext(text)
text = text
:gsub("%[%[", "\1")
:gsub("%]%]", "\2")
-- Remove strip markers and HTML tags.
text = unstrip(text):gsub("<[^<>\1\2]+>", "")
-- Parse internal links for the display text, and remove categories.
text = remove_links(text)
-- Remove files.
text = text:gsub("\1[Ff][Ii][Ll][Ee]:[^\1\2]+\2", "")
:gsub("\1[Ii][Mm][Aa][Gg][Ee]:[^\1\2]+\2", "")
-- Parse external links for the display text.
text = text:gsub("%[(https?://[^%[%]]+)%]", handle_url)
-- Any remaining square brackets aren't involved in links, but must be escaped to avoid creating new links.
:gsub("\1", "[[")
:gsub("\2", "]]")
:gsub("%[", "[")
:gsub("]", "]")
-- Strip bold, italics and soft hyphens.
:gsub("('*)'''(.-'*)'''", "%1%2")
:gsub("('*)''(.-'*)''", "%1%2")
:gsub("", "")
-- Get any HTML entities and trim.
-- Note: don't decode URL percent encoding, as it shouldn't be used in display text and may cause problems if % is used.
return trim(decode_entities(text))
end
end
do
local function generate_sortkey(lang, sc, sort_key, sort_base)
-- If the sort key is "-", treat the language as undetermined (the default). This is desirable when categorising (e.g.) translation requests, as the pages to be categorised are always in English/Translingual.
if sort_key == "-" then
lang, sort_key = und or get_und(), nil
elseif not lang then
lang = und or get_und()
end
-- Generate the automatic sort key.
local auto = lang:makeSortKey(sort_base or pagename or get_pagename(), sc)
-- Use the page defaultsort if necessary. Try hard to avoid empty sort key, which causes categories to be
-- displayed as raw text; fall back to pagename if necessary.
if not auto or auto == "" then
auto = defaultsort or get_defaultsort()
end
if not auto or auto == "" then
auto = sort_base
end
if not auto or auto == "" then
auto = pagename or get_pagename()
end
-- If not sort key specified, use the automatic one.
if not sort_key or sort_key == "" then
return auto
-- Otherwise, if the language is not "und", categorize the manual sort key as either redundant or non-redundant.
-- FIXME: we should do this for "und" as well, but "Undetermined terms..." does not make sense for translations etc.
elseif lang:getCode() ~= "und" then
return sort_key, "[[Kategori:Perkataan bahasa " .. lang:getFullName() .. " dengan kunci isih " .. (
uupper(sort_key) == auto and "lewah" or
"tidak lewah dan tidak automatik"
) .. "|" .. sort_key .. "]]"
end
return sort_key
end
--[==[
Format the categories with the appropriate sort key.
* `cat` can take two forms:
** A string (the full category, minus the {"Kategori:"} prefix);
** A list of categories. Each category in the list can be either:
*** A string in the same format as above;
*** An object with the fields:
**** `cat`: a string in the same format as above (required);
**** `lang`: an optional language object to override the overall `lang`;
**** `sc`: an optional script object to override the overall `sc`.
**** `sort_key`: an optional sort key to override the overall `sort_key`;
**** `sort_base`: an optional sort base to override the overall `sort_base`;
* `lang` is an object encapsulating a language; if {nil}, the object for language code {"und"} (undetermined) will
be used. `lang` is used when computing the sort key (either from the subpage name or sort base).
* `sort_key` is placed in the category invocation, and indicates how the page will sort in the respective category.
Normally '''do not use this'''. Instead, leave it {nil}, and if you need to a control the sort order, use
{sort_base}, so that language-specific normalization is applied on top of the specified sort base. If neither
{sort_key} nor {sort_base} is specified, the default is to apply language-specific normalization to the subpage
name; see below.
* `sort_base` lets you override the default sort key while still maintaining appropriate language-specific
normalization. If {nil} is specified, this defaults to the subpage name, which is the portion of the full pagename
after subtracting the namespace prefix (and, in certain namespaces such as {User:}, but notably not in the
mainspace, after subtracting anything up through the final slash). The actual sort key is derived from the sort
base approximately by lowercasing, applying language-specific normalization and then uppercasing; note that the
same process is applied in deriving the sort key when no sort base is specified. For example, for French, Spanish,
etc. the normalization process maps accented letters to their unaccented equivalents, so that e.g. in French,
{{m|fr|ça}} sorts after {{m|fr|ca}} (instead of after the default Wikimedia sort order, which is approximately
based on Unicode sort order and places ç after z) and {{m|fr|côté}} sorts after {{m|fr|coté}} (instead of between
c and d). Similarly, in Russian the normalization process converts Cyrillic ё to a string consisting of Cyrillic е
followed by U+10FFFF, so that effectively ё sorts after е instead of the default Wikimedia sort, which (I think)
puts ё after я, the last letter of the Cyrillic alphabet.
* `force_output` forces normal output in all namespaces. Normally, nothing is output if the page isn't in the main,
Appendix:, Thesaurus:, Reconstruction: or Citations: namespaces.
* `sc` is a script object; if nil, the default will be derived from the sort base (or its default value, the
subpage name) by calling {lang:findBestScript()}. The value of `sc` is used during the sort base normalization
process; for example, languages with multiple scripts will often have script-specific normalization processes.]==]
function export.format_categories(cat, lang, sort_key, sort_base, force_output, sc)
if not (
force_output or
is_content_page(current_title or get_current_title()) or
current_title.prefixedText == "Wikikamus:Kotak pasir"
) then
return ""
elseif type(cat) == "string" then
local this_sort_key, extra = generate_sortkey(lang, sc, sort_key, sort_base)
ret[n] = "[[Kategori:" .. category .. "|" .. this_sort_key .. "]]" .. (extra or "")
end
local ret, i, n, default = {}, 0, 0
-- Iterate over all categories in `cat`.
while true do
i = i + 1
local category = cat[i]
if category == nil then
return concat(ret)
end
local this_sort_key, extra
-- If the category type is a table, use any custom options in it.
if type(category) == "table" then
category, this_sort_key, extra = category.cat, generate_sortkey(
category.lang or lang,
category.sc or sc,
category.sort_key or sort_key,
category.sort_base or sort_base
)
-- If `default` has already been determined, use it.
elseif default then
this_sort_key = default
-- Otherwise, calculate `default` and use it.
else
this_sort_key, extra = generate_sortkey(lang, sc, sort_key, sort_base)
default = this_sort_key
end
n = n + 1
ret[n] = "[[Kategori:" .. category .. "|" .. this_sort_key .. "]]" .. (extra or "")
end
end
format_categories = export.format_categories
end
--[==[
Add a "catfix", which is used on language-specific category pages to add language attributes and often script
classes to all entry names. The addition of language attributes and script classes makes the entry names display
better (using the language- or script-specific styles specified in [[MediaWiki:Common.css]]), which is particularly
important for non-English languages that do not have consistent font support in browsers.
Language attributes are added for all languages, but script classes are only added for languages with one script
listed in their data file, or for languages that have a default script listed in the {catfix_script} list in
[[Module:utilities/data]]. Some languages clearly have a default script, but still have other scripts listed in
their data file. If those other scripts are not simply scripts like {{cd|Brai}} (Braille), their default script
needs to be specified. Others do not have a default script.
* Serbo-Croatian is regularly written in both the Latin and Cyrillic scripts. Because it uses two scripts,
Serbo-Croatian cannot have a script class applied to entries in its category pages, as only one script class
can be specified at a time.
* German is usually written in the Latin script ({{cd|Latn}}), but Fraktur ({{cd|Latf}}) is also listed in
its data file. So German needs an entry in the {catfix_script} list, so that the {{cd|Latn}} (Latin) script
class will be applied to entries in its category pages.
To find the scripts listed for a language, go to [[Module:languages]] and use the search box to find the data file
for the language. To find out what a script code means, search the script code in [[Module:scripts/data]].]==]
function export.catfix(lang, sc)
if not lang or not lang.getCanonicalName then
error('The first argument to the function "catfix" should be a language object from [[Module:languages]] or [[Module:etymology languages]].')
end
if sc and not sc.getCode then
error('The second argument to the function "catfix" should be a script object from [[Module:scripts]].')
end
-- To add script classes to links on pages created by category boilerplate templates.
if not sc then
local code = (catfix_scripts or get_catfix_scripts())[lang:getCode()] or catfix_scripts[lang:getFullCode()]
if code then
sc = get_script(code)
end
end
-- If the language only has a single valid candidate script, apply it as the default.
if not sc then
local scripts = lang:getScripts()
if #scripts == 1 then
sc = scripts[1]
else
-- Iterate over scripts. If there is only one when ignoring
-- scripts like Brai, then pick that one.
for _, script in ipairs(scripts) do
if script:getCode() ~= "Brai" then
if sc then
-- Multiple candidates - fail.
sc = nil
break
else
sc = script
end
end
end
end
end
-- Hack: using a <ul> tag prevents the parser from automatically generating a <p> tag around the catfix element.
return "<ul class=\"catfix\" data-anchor=\"" ..
anchor_encode(lang:getFullName()) .. "\">" ..
tag_text("", lang, sc) .. "</ul>" ..
format_categories("Laman yang menggunakan catfix", nil, nil, nil, true)
end
--[==[
Given a type (as a string) and an arbitrary number of entities, checks whether all of those entities are language,
family, script, writing system or Wikimedia language objects. Useful for error handling in functions that require
one of these kinds of object.
If `noErr` is set, the function returns false instead of throwing an error, which allows customised error handling to
be done in the calling function.]==]
function export.check_object(typ, noErr, ...)
if ... == nil then
if noErr then
return false
end
error("Must provide at least one object to check.", 2)
end
for _, obj in ipairs{...} do
if type(obj) ~= "table" or type(obj.hasType) ~= "function" then
if noErr then
return false
end
error("Function expected a " .. typ .. " object, but received a " .. type(obj) .. " instead.", 2)
elseif not (typ == "object" or obj:hasType(typ)) then
for _, wrong_type in ipairs{"family", "language", "script", "Wikimedia language", "writing system"} do
if obj:hasType(wrong_type) then
if noErr then
return false
end
error("Function expected a " .. typ .. " object, but received a " .. wrong_type .. " object instead.", 2)
end
end
if noErr then
return false
end
error("Function expected a " .. typ .. " object, but received another type of object instead.", 2)
end
end
return true
end
return export
17ioxx6vrb8r3zrxzgglfc7jafq7fex
375845
375841
2026-09-25T09:10:15Z
SNN95
2113
baiki ralat
375845
Scribunto
text/plain
local export = {}
local headword_data_module = "Module:headword/data"
local languages_module = "Module:languages"
local links_module = "Module:links"
local load_module = "Module:load"
local pages_module = "Module:pages"
local script_utilities_module = "Module:script utilities"
local scripts_module = "Module:scripts"
local string_utilities_module = "Module:string utilities"
local utilities_data_module = "Module:utilities/data"
local mw = mw
local anchor_encode = mw.uri.anchorEncode
local concat = table.concat
local format_categories -- Defined below.
local ipairs = ipairs
local require = require
local type = type
local unstrip = mw.text.unstrip
--[==[
Loaders for functions in other modules, which overwrite themselves with the target function when called. This ensures modules are only loaded when needed, retains the speed/convenience of locally-declared pre-loaded functions, and has no overhead after the first call, since the target functions are called directly in any subsequent calls.]==]
local function decode_entities(...)
decode_entities = require(string_utilities_module).decode_entities
return decode_entities(...)
end
local function get_script(...)
get_script = require(scripts_module).getByCode
return get_script(...)
end
local function is_content_page(...)
is_content_page = require(pages_module).is_content_page
return is_content_page(...)
end
local function load_data(...)
load_data = require(load_module).load_data
return load_data(...)
end
local function remove_links(...)
remove_links = require(links_module).remove_links
return remove_links(...)
end
local function tag_text(...)
tag_text = require(script_utilities_module).tag_text
return tag_text(...)
end
local function trim(...)
trim = require(string_utilities_module).trim
return trim(...)
end
local function uupper(...)
uupper = require(string_utilities_module).upper
return uupper(...)
end
--[==[
Loaders for objects, which load data (or some other object) into some variable, which can then be accessed as "foo or get_foo()", where the function get_foo sets the object to "foo" and then returns it. This ensures they are only loaded when needed, and avoids the need to check for the existence of the object each time, since once "foo" has been set, "get_foo" will not be called again.]==]
local catfix_scripts
local function get_catfix_scripts()
catfix_scripts, get_catfix_scripts = load_data(utilities_data_module).catfix_scripts, nil
return catfix_scripts
end
local current_title
local function get_current_title()
current_title, get_current_title = mw.title.getCurrentTitle(), nil
return current_title
end
local defaultsort
local function get_defaultsort()
defaultsort, get_defaultsort = load_data(headword_data_module).page.pagename_defaultsort, nil
return defaultsort
end
local pagename
local function get_pagename()
pagename, get_pagename = load_data(headword_data_module).page.encoded_pagename, nil
return pagename
end
local und
local function get_und()
und, get_und = require(languages_module).getByCode("und"), nil
return und
end
do
local function handle_url(capture)
return capture:match("https?://[^%s%]]+%s([^%]]+)") or ""
end
--[==[
A helper function to strip wiki markup, giving the plaintext of what is displayed on the page.]==]
function export.get_plaintext(text)
text = text
:gsub("%[%[", "\1")
:gsub("%]%]", "\2")
-- Remove strip markers and HTML tags.
text = unstrip(text):gsub("<[^<>\1\2]+>", "")
-- Parse internal links for the display text, and remove categories.
text = remove_links(text)
-- Remove files.
text = text:gsub("\1[Ff][Ii][Ll][Ee]:[^\1\2]+\2", "")
:gsub("\1[Ii][Mm][Aa][Gg][Ee]:[^\1\2]+\2", "")
-- Parse external links for the display text.
text = text:gsub("%[(https?://[^%[%]]+)%]", handle_url)
-- Any remaining square brackets aren't involved in links, but must be escaped to avoid creating new links.
:gsub("\1", "[[")
:gsub("\2", "]]")
:gsub("%[", "[")
:gsub("]", "]")
-- Strip bold, italics and soft hyphens.
:gsub("('*)'''(.-'*)'''", "%1%2")
:gsub("('*)''(.-'*)''", "%1%2")
:gsub("", "")
-- Get any HTML entities and trim.
-- Note: don't decode URL percent encoding, as it shouldn't be used in display text and may cause problems if % is used.
return trim(decode_entities(text))
end
end
do
local function generate_sortkey(lang, sc, sort_key, sort_base)
-- If the sort key is "-", treat the language as undetermined (the default). This is desirable when categorising (e.g.) translation requests, as the pages to be categorised are always in English/Translingual.
if sort_key == "-" then
lang, sort_key = und or get_und(), nil
elseif not lang then
lang = und or get_und()
end
-- Generate the automatic sort key.
local auto = lang:makeSortKey(sort_base or pagename or get_pagename(), sc)
-- Use the page defaultsort if necessary. Try hard to avoid empty sort key, which causes categories to be
-- displayed as raw text; fall back to pagename if necessary.
if not auto or auto == "" then
auto = defaultsort or get_defaultsort()
end
if not auto or auto == "" then
auto = sort_base
end
if not auto or auto == "" then
auto = pagename or get_pagename()
end
-- If not sort key specified, use the automatic one.
if not sort_key or sort_key == "" then
return auto
-- Otherwise, if the language is not "und", categorize the manual sort key as either redundant or non-redundant.
-- FIXME: we should do this for "und" as well, but "Undetermined terms..." does not make sense for translations etc.
elseif lang:getCode() ~= "und" then
return sort_key, "[[Kategori:Perkataan bahasa " .. lang:getFullName() .. " dengan kunci isih " .. (
uupper(sort_key) == auto and "lewah" or
"tidak lewah dan tidak automatik"
) .. "|" .. sort_key .. "]]"
end
return sort_key
end
--[==[
Format the categories with the appropriate sort key.
* `cat` can take two forms:
** A string (the full category, minus the {"Kategori:"} prefix);
** A list of categories. Each category in the list can be either:
*** A string in the same format as above;
*** An object with the fields:
**** `cat`: a string in the same format as above (required);
**** `lang`: an optional language object to override the overall `lang`;
**** `sc`: an optional script object to override the overall `sc`.
**** `sort_key`: an optional sort key to override the overall `sort_key`;
**** `sort_base`: an optional sort base to override the overall `sort_base`;
* `lang` is an object encapsulating a language; if {nil}, the object for language code {"und"} (undetermined) will
be used. `lang` is used when computing the sort key (either from the subpage name or sort base).
* `sort_key` is placed in the category invocation, and indicates how the page will sort in the respective category.
Normally '''do not use this'''. Instead, leave it {nil}, and if you need to a control the sort order, use
{sort_base}, so that language-specific normalization is applied on top of the specified sort base. If neither
{sort_key} nor {sort_base} is specified, the default is to apply language-specific normalization to the subpage
name; see below.
* `sort_base` lets you override the default sort key while still maintaining appropriate language-specific
normalization. If {nil} is specified, this defaults to the subpage name, which is the portion of the full pagename
after subtracting the namespace prefix (and, in certain namespaces such as {User:}, but notably not in the
mainspace, after subtracting anything up through the final slash). The actual sort key is derived from the sort
base approximately by lowercasing, applying language-specific normalization and then uppercasing; note that the
same process is applied in deriving the sort key when no sort base is specified. For example, for French, Spanish,
etc. the normalization process maps accented letters to their unaccented equivalents, so that e.g. in French,
{{m|fr|ça}} sorts after {{m|fr|ca}} (instead of after the default Wikimedia sort order, which is approximately
based on Unicode sort order and places ç after z) and {{m|fr|côté}} sorts after {{m|fr|coté}} (instead of between
c and d). Similarly, in Russian the normalization process converts Cyrillic ё to a string consisting of Cyrillic е
followed by U+10FFFF, so that effectively ё sorts after е instead of the default Wikimedia sort, which (I think)
puts ё after я, the last letter of the Cyrillic alphabet.
* `force_output` forces normal output in all namespaces. Normally, nothing is output if the page isn't in the main,
Appendix:, Thesaurus:, Reconstruction: or Citations: namespaces.
* `sc` is a script object; if nil, the default will be derived from the sort base (or its default value, the
subpage name) by calling {lang:findBestScript()}. The value of `sc` is used during the sort base normalization
process; for example, languages with multiple scripts will often have script-specific normalization processes.]==]
function export.format_categories(cat, lang, sort_key, sort_base, force_output, sc)
if not (
force_output or
is_content_page(current_title or get_current_title()) or
current_title.prefixedText == "Wikikamus:Kotak pasir"
) then
return ""
elseif type(cat) == "string" then
local this_sort_key, extra = generate_sortkey(lang, sc, sort_key, sort_base)
return "[[Kategori:" .. cat .. "|" .. this_sort_key .. "]]" .. (extra or "")
end
local ret, i, n, default = {}, 0, 0
-- Iterate over all categories in `cat`.
while true do
i = i + 1
local category = cat[i]
if category == nil then
return concat(ret)
end
local this_sort_key, extra
-- If the category type is a table, use any custom options in it.
if type(category) == "table" then
category, this_sort_key, extra = category.cat, generate_sortkey(
category.lang or lang,
category.sc or sc,
category.sort_key or sort_key,
category.sort_base or sort_base
)
-- If `default` has already been determined, use it.
elseif default then
this_sort_key = default
-- Otherwise, calculate `default` and use it.
else
this_sort_key, extra = generate_sortkey(lang, sc, sort_key, sort_base)
default = this_sort_key
end
n = n + 1
ret[n] = "[[Kategori:" .. category .. "|" .. this_sort_key .. "]]" .. (extra or "")
end
end
format_categories = export.format_categories
end
--[==[
Add a "catfix", which is used on language-specific category pages to add language attributes and often script
classes to all entry names. The addition of language attributes and script classes makes the entry names display
better (using the language- or script-specific styles specified in [[MediaWiki:Common.css]]), which is particularly
important for non-English languages that do not have consistent font support in browsers.
Language attributes are added for all languages, but script classes are only added for languages with one script
listed in their data file, or for languages that have a default script listed in the {catfix_script} list in
[[Module:utilities/data]]. Some languages clearly have a default script, but still have other scripts listed in
their data file. If those other scripts are not simply scripts like {{cd|Brai}} (Braille), their default script
needs to be specified. Others do not have a default script.
* Serbo-Croatian is regularly written in both the Latin and Cyrillic scripts. Because it uses two scripts,
Serbo-Croatian cannot have a script class applied to entries in its category pages, as only one script class
can be specified at a time.
* German is usually written in the Latin script ({{cd|Latn}}), but Fraktur ({{cd|Latf}}) is also listed in
its data file. So German needs an entry in the {catfix_script} list, so that the {{cd|Latn}} (Latin) script
class will be applied to entries in its category pages.
To find the scripts listed for a language, go to [[Module:languages]] and use the search box to find the data file
for the language. To find out what a script code means, search the script code in [[Module:scripts/data]].]==]
function export.catfix(lang, sc)
if not lang or not lang.getCanonicalName then
error('The first argument to the function "catfix" should be a language object from [[Module:languages]] or [[Module:etymology languages]].')
end
if sc and not sc.getCode then
error('The second argument to the function "catfix" should be a script object from [[Module:scripts]].')
end
-- To add script classes to links on pages created by category boilerplate templates.
if not sc then
local code = (catfix_scripts or get_catfix_scripts())[lang:getCode()] or catfix_scripts[lang:getFullCode()]
if code then
sc = get_script(code)
end
end
-- If the language only has a single valid candidate script, apply it as the default.
if not sc then
local scripts = lang:getScripts()
if #scripts == 1 then
sc = scripts[1]
else
-- Iterate over scripts. If there is only one when ignoring
-- scripts like Brai, then pick that one.
for _, script in ipairs(scripts) do
if script:getCode() ~= "Brai" then
if sc then
-- Multiple candidates - fail.
sc = nil
break
else
sc = script
end
end
end
end
end
-- Hack: using a <ul> tag prevents the parser from automatically generating a <p> tag around the catfix element.
return "<ul class=\"catfix\" data-anchor=\"" ..
anchor_encode(lang:getFullName()) .. "\">" ..
tag_text("", lang, sc) .. "</ul>" ..
format_categories("Laman yang menggunakan catfix", nil, nil, nil, true)
end
--[==[
Given a type (as a string) and an arbitrary number of entities, checks whether all of those entities are language,
family, script, writing system or Wikimedia language objects. Useful for error handling in functions that require
one of these kinds of object.
If `noErr` is set, the function returns false instead of throwing an error, which allows customised error handling to
be done in the calling function.]==]
function export.check_object(typ, noErr, ...)
if ... == nil then
if noErr then
return false
end
error("Must provide at least one object to check.", 2)
end
for _, obj in ipairs{...} do
if type(obj) ~= "table" or type(obj.hasType) ~= "function" then
if noErr then
return false
end
error("Function expected a " .. typ .. " object, but received a " .. type(obj) .. " instead.", 2)
elseif not (typ == "object" or obj:hasType(typ)) then
for _, wrong_type in ipairs{"family", "language", "script", "Wikimedia language", "writing system"} do
if obj:hasType(wrong_type) then
if noErr then
return false
end
error("Function expected a " .. typ .. " object, but received a " .. wrong_type .. " object instead.", 2)
end
end
if noErr then
return false
end
error("Function expected a " .. typ .. " object, but received another type of object instead.", 2)
end
end
return true
end
return export
1lmfc04dnmohg8j2k1bo1ws8ksrutj3
Modul:links/templates
828
9792
375815
375365
2026-09-24T17:46:31Z
SNN95
2113
375815
Scribunto
text/plain
-- Prevent substitution.
if mw.isSubsting() then
return require("Module:unsubst")
end
local export = {}
local links_module = "Module:links"
local process_params = require("Module:parameters").process
local remove = table.remove
local upper = require("Module:string utilities").upper
--[=[
Modules used:
[[Module:links]]
[[Module:languages]]
[[Module:scripts]]
[[Module:parameters]]
[[Module:debug]]
]=]
do
local function get_args(frame)
-- `compat` is a compatibility mode for {{term}}.
-- If given a nonempty value, the function uses lang= to specify the
-- language, and all the positional parameters shift one number lower.
local iargs = frame.args
iargs.compat = iargs.compat and iargs.compat ~= ""
iargs.langname = iargs.langname and iargs.langname ~= ""
iargs.notself = iargs.notself and iargs.notself ~= ""
local alias_of_4 = {alias_of = 4}
local boolean = {type = "boolean"}
local params = {
[1] = {required = true, type = "language", default = "und"},
[2] = true,
[3] = true,
[4] = true,
g = {list = true, type = "genders", flatten = true},
gloss = alias_of_4,
id = true,
lit = true,
ng = true,
pos = true,
sc = {type = "script"},
t = alias_of_4,
tr = true,
ts = true,
q = {type = "qualifier"},
qq = {type = "qualifier"},
l = {type = "labels"},
ll = {type = "labels"},
ref = {type = "references"},
["accel-form"] = true,
["accel-translit"] = true,
["accel-lemma"] = true,
["accel-lemma-translit"] = true,
["accel-gender"] = true,
["accel-nostore"] = boolean,
}
if iargs.compat then
params.lang = {type = "language", default = "und"}
remove(params, 1)
alias_of_4.alias_of = 3
end
if iargs.langname then
params.w = boolean
end
return process_params(frame:getParent().args, params), iargs
end
-- Used in [[Template:l]] and [[Template:m]].
function export.l_term_t(frame)
local args, iargs = get_args(frame)
local compat = iargs.compat
local lang = args[compat and "lang" or 1]
-- Tracking for und.
if not compat and lang:getCode() == "und" then
require("Module:debug").track("link/und")
end
local term = args[(compat and 1 or 2)]
local alt = args[(compat and 2 or 3)]
term = term ~= "" and term or nil
if not term and not alt and iargs.demo then
term = iargs.demo
end
local langname = iargs.langname and (
args.w and lang:makeWikipediaLink() or
lang:getCanonicalName()
) or nil
if langname and term == "-" then
return langname
end
-- Forward the information to full_link
return (langname and langname .. " " or "") .. require(links_module).full_link(
{
lang = lang,
sc = args.sc,
track_sc = true,
term = term,
alt = alt,
gloss = args[4],
id = args.id,
tr = args.tr,
ts = args.ts,
genders = args.g,
pos = args.pos,
ng = args.ng,
lit = args.lit,
q = args.q,
qq = args.qq,
l = args.l,
ll = args.ll,
refs = args.ref,
show_decorations = true,
accel = args["accel-form"] and {
form = args["accel-form"],
translit = args["accel-translit"],
lemma = args["accel-lemma"],
lemma_translit = args["accel-lemma-translit"],
gender = args["accel-gender"],
nostore = args["accel-nostore"],
} or nil
},
iargs.face,
not iargs.notself
)
end
-- Used in [[Template:link-annotations]].
function export.l_annotations_t(frame)
local args, iargs = get_args(frame)
-- Forward the information to format_link_annotations
return require(links_module).format_link_annotations(
{
lang = args[1],
tr = { args.tr },
ts = { args.ts },
genders = args.g,
pos = args.pos,
ng = args.ng,
lit = args.lit
},
iargs.face
)
end
end
-- Used in [[Template:ll]].
do
local function get_args(frame)
return process_params(frame:getParent().args, {
[1] = {required = true, type = "language", default = "und"},
[2] = {allow_empty = true},
[3] = true,
id = true,
sc = {type = "script"},
})
end
function export.ll(frame)
local args = get_args(frame)
local lang = args[1]
local sc = args.sc
local term = args[2]
term = term ~= "" and term or nil
return require(links_module).language_link{
lang = lang,
sc = sc,
term = term,
alt = args[3],
id = args.id
} or "<small>[Perkataan?]</small>" ..
require("Module:utilities").format_categories(
{"Permintaan perkataan bahasa " .. lang:getFullName()},
lang, "-", nil, nil, sc
)
end
end
function export.def_t(frame)
local args = process_params(frame:getParent().args, {
[1] = {required = true, default = ""},
})
local face = frame.args.face
local ret = require("Module:script utilities").tag_definition(require(links_module).embedded_language_links{
term = args[1],
lang = require("Module:languages").getByCode("ms"),
sc = require("Module:scripts").getByCode("Latn")
}, face)
if face == "non-gloss" then
return ret
end
return '<span class="mention-gloss-paren">(</span>' .. ret .. '<span class="mention-gloss-paren">)</span>'
end
function export.linkify_t(frame)
local args = process_params(frame:getParent().args, {
[1] = {required = true, default = ""},
})
args[1] = mw.text.trim(args[1])
if args[1] == "" or args[1]:find("[[", nil, true) then
return args[1]
end
return "[[" .. args[1] .. "]]"
end
function export.cap_t(frame)
local args = process_params(frame:getParent().args, {
[1] = {required = true},
[2] = true,
lang = {type = "language", default = "ms"},
})
local term = args[1]
return require(links_module).full_link{
lang = args.lang,
term = term,
alt = term:gsub("^.[\128-\191]*", upper) .. (args[2] or "")
}
end
function export.section_link_t(frame)
local args = process_params(frame:getParent().args, {
[1] = {},
})
return require(links_module).section_link(args[1])
end
return export
srot7z9thogy9e35a3ushwp3dywkrja
Modul:translations
828
9941
375866
375388
2026-09-25T11:58:07Z
Hakimi97
2668
Ubah sikit terjemahan dalam ralat Translingual > Rentas bahasa
375866
Scribunto
text/plain
local export = {}
local anchors_module = "Module:anchors"
local debug_track_module = "Module:debug/track"
local languages_module = "Module:languages"
local links_module = "Module:links"
local pages_module = "Module:pages"
local parameters_module = "Module:parameters"
local string_utilities_module = "Module:string utilities"
local templatestyles_module = "Module:TemplateStyles"
local utilities_module = "Module:utilities"
local wikimedia_languages_module = "Module:wikimedia languages"
local concat = table.concat
local html_create = mw.html.create
local insert = table.insert
local load_data = mw.loadData
local new_title = mw.title.new
local require = require
--[==[
Loaders for functions in other modules, which overwrite themselves with the target function when called. This ensures modules are only loaded when needed, retains the speed/convenience of locally-declared pre-loaded functions, and has no overhead after the first call, since the target functions are called directly in any subsequent calls.]==]
local function decode_uri(...)
decode_uri = require(string_utilities_module).decode_uri
return decode_uri(...)
end
local function format_categories(...)
format_categories = require(utilities_module).format_categories
return format_categories(...)
end
local function full_link(...)
full_link = require(links_module).full_link
return full_link(...)
end
local function get_link_page(...)
get_link_page = require(links_module).get_link_page
return get_link_page(...)
end
local function get_wikimedia_lang(...)
get_wikimedia_lang = require(wikimedia_languages_module).getByCode
return get_wikimedia_lang(...)
end
local function language_link(...)
language_link = require(links_module).language_link
return language_link(...)
end
local function normalize_anchor(...)
normalize_anchor = require(anchors_module).normalize_anchor
return normalize_anchor(...)
end
local function plain_link(...)
plain_link = require(links_module).plain_link
return plain_link(...)
end
local function process_params(...)
process_params = require(parameters_module).process
return process_params(...)
end
local function remove_links(...)
remove_links = require(links_module).remove_links
return remove_links(...)
end
local function split_on_slashes(...)
split_on_slashes = require(links_module).split_on_slashes
return split_on_slashes(...)
end
local function templatestyles(...)
templatestyles = require(templatestyles_module)
return templatestyles(...)
end
local function track(...)
track = require(debug_track_module)
return track(...)
end
--[==[
Loaders for objects, which load data (or some other object) into some variable, which can then be accessed as "foo or get_foo()", where the function get_foo sets the object to "foo" and then returns it. This ensures they are only loaded when needed, and avoids the need to check for the existence of the object each time, since once "foo" has been set, "get_foo" will not be called again.]==]
local en
local function get_en()
en, get_en = require(languages_module).getByCode("ms"), nil
return en
end
local headword_data
local function get_headword_data()
headword_data, get_headword_data = load_data("Module:headword/data"), nil
return headword_data
end
local parameters_data
local function get_parameters_data()
parameters_data, get_parameters_data = load_data("Module:parameters/data"), nil
return parameters_data
end
local translations_data
local function get_translations_data()
translations_data, get_translations_data = load_data("Module:translations/data"), nil
return translations_data
end
local function is_translation_subpage(pagename)
if (headword_data or get_headword_data()).page.namespace ~= "" then
return false
elseif not pagename then
pagename = (headword_data or get_headword_data()).encoded_pagename
end
return pagename:match("./translations$") and true or false
end
local function canonical_pagename()
local pagename = (headword_data or get_headword_data()).encoded_pagename
return is_translation_subpage(pagename) and pagename:sub(1, -14) or pagename
end
local function interwiki(terminfo, term, lang, langcode)
-- No interwiki link if term is empty/missing
if not term or #term < 1 then
terminfo.interwiki = false
return
end
-- Percent-decode the term.
term = decode_uri(terminfo.term, "PATH")
-- Don't show an interwiki link if it's an invalid title.
if not new_title(term) then
terminfo.interwiki = false
return
end
local interwiki_langcode = (translations_data or get_translations_data()).interwiki_langs[langcode]
local wmlangs = interwiki_langcode and {get_wikimedia_lang(interwiki_langcode)} or lang:getWikimediaLanguages()
-- Don't show the interwiki link if the language is not recognised by Wikimedia.
if #wmlangs == 0 then
terminfo.interwiki = false
return
end
local sc = terminfo.sc
local target_page = get_link_page(term, lang, sc)
local split = split_on_slashes(target_page)
if not split[1] then
terminfo.interwiki = false
return
end
target_page = split[1]
local wmlangcode = wmlangs[1]:getCode()
local interwiki_link = language_link{
lang = lang,
sc = sc,
term = wmlangcode .. ":" .. target_page,
alt = "(" .. wmlangcode .. ")",
tr = "-"
}
terminfo.interwiki = tostring(html_create("span")
:addClass("tpos")
:wikitext(" " .. interwiki_link)
)
end
function export.show_terminfo(terminfo, check)
local lang = terminfo.lang
local langcode, langname = lang:getCode(), lang:getCanonicalName()
-- Translations must be for mainspace languages.
if not lang:hasType("regular") then
error("Translations must be for attested and approved main-namespace languages.")
else
local disallowed = (translations_data or get_translations_data()).disallowed
local err_msg = disallowed[langcode]
if err_msg then
error("Translations not allowed in " .. langname .. " (" .. langcode .. "). " .. langname .. " translations should " .. err_msg)
end
local fullcode = lang:getFullCode()
if fullcode ~= langcode then
err_msg = disallowed[fullcode]
if err_msg then
langname = lang:getFullName()
error("Translations not allowed in " .. langname .. " (" .. fullcode .. "). " .. langname .. " translations should " .. err_msg)
end
end
end
if langcode == "ms" then
if terminfo.interwiki then
error("Interwiki translations not allowed for Malay; they should always link to a different Wiktionary")
end
local current_L2 = require(pages_module).get_current_L2()
if current_L2 ~= "Rentas bahasa" and mw.title.getCurrentTitle().nsText ~= "Wikikamus" then
if current_L2 then
error("Malay translations only allowed in Rentas bahasa section, not in " .. current_L2)
else
error("Malay translations only allowed in Rentas bahasa section, not outside of any L2")
end
end
end
local term = terminfo.term
-- Check if there is a term. Don't show the interwiki link if there is nothing to link to.
if not term then
-- Track entries that don't provide a term.
-- FIXME: This should be a category.
track("translations/no term")
track("translations/no term/" .. langcode)
end
if terminfo.interwiki then
interwiki(terminfo, term, lang, langcode)
end
langcode = lang:getFullCode()
if (translations_data or get_translations_data()).need_super[langcode] then
local tr = terminfo.tr
if tr ~= nil then
terminfo.tr = tr:gsub("%d[%d%*%-]*%f[^%d%*]", "<sup>%0</sup>")
end
end
terminfo.show_decorations = true
local link = full_link(terminfo, "translation")
local canonical_name = lang:getCanonicalName()
local full_name = lang:getFullName()
local categories = {"Perkataan dengan terjemahan bahasa " .. canonical_name}
if canonical_name ~= full_name then
insert(categories, "Perkataan dengan terjemahan bahasa " .. full_name)
end
if check then
link = tostring(html_create("span")
:addClass("ttbc")
:tag("sup")
:addClass("ttbc")
:wikitext("(sila [[WT:Terjemahan#Terjemahan untuk disemak|sahkan]])")
:done()
:wikitext(" " .. link)
)
insert(categories, "Permintaan pengesahan terjemahan bahasa " .. langname )
end
return link .. format_categories(categories, en or get_en(), nil, canonical_pagename())
end
-- Implements {{t}}, {{t+}}, {{t-check}} and {{t+check}}.
function export.show(frame)
local args = process_params(frame:getParent().args, (parameters_data or get_parameters_data())["translation"])
local check = frame.args.check
return export.show_terminfo({
lang = args[1],
sc = args.sc,
track_sc = true,
term = args[2],
alt = args.alt,
id = args.id,
genders = args[3],
tr = args.tr,
ts = args.ts,
lit = args.lit,
q = args.q,
qq = args.qq,
l = args.l,
ll = args.ll,
refs = args.ref,
interwiki = frame.args.interwiki,
}, check and check ~= "")
end
local function add_id(div, id)
return id and div:attr("id", normalize_anchor("Translations-" .. id)) or div
end
-- Implements {{ter-atas}} and part of {{ter-atas-juga}}.
local function top(args, title, id, navhead)
local column_width = (args["column-width"] == "wide" or args["column-width"] == "narrow") and "-" .. args["column-width"] or ""
local div = html_create("div")
:addClass("NavFrame")
:node(navhead)
:tag("div")
:addClass("NavContent")
:tag("table")
:addClass("translations")
:attr("role", "presentation")
:attr("data-gloss", title or "")
:tag("tr")
:tag("td")
:addClass("translations-cell")
:addClass("multicolumn-list" .. column_width)
:attr("colspan", "3")
:allDone()
div = add_id(div, id)
local categories = {}
if not title then
insert(categories, "Pengepala jadual terjemahan kekurangan padanan kata")
end
local pagename = canonical_pagename()
if is_translation_subpage() then
insert(categories, "Sublaman terjemahan")
end
return (tostring(div):gsub("</td></tr></table></div></div>$", "")) ..
(#categories > 0 and format_categories(categories, en or get_en(), nil, pagename) or "") ..
-- Category to trigger [[MediaWiki:Gadget-TranslationAdder.js]]; we want this even on
-- user pages and such.
format_categories("Perkataan dengan kotak terjemahan", nil, nil, nil, true) ..
templatestyles("Modul:translations/styles.css")
end
-- Entry point for {{ter-atas}}.
function export.top(frame)
local args = process_params(frame:getParent().args, (parameters_data or get_parameters_data())["ter-atas"])
local title = args[1]
local id = args.id or title
title = title and remove_links(title)
return top(args, title, id, html_create("div")
:addClass("NavHead")
:css("text-align", "left")
:wikitext(title or "Terjemahan")
)
end
-- Entry point for {{checktrans-top}}.
function export.check_top(frame)
local args = process_params(frame:getParent().args, (parameters_data or get_parameters_data())["checktrans-top"])
local text = "\n:''The translations below need to be checked and inserted above into the appropriate translation tables. See instructions at " ..
frame:expandTemplate{
title = "section link",
args = {"Wikikamus:Susun atur entri#Terjemahan"}
} ..
".''\n"
local header = html_create("div")
:addClass("checktrans")
:wikitext(text)
local subtitle = args[1]
local title = "Terjemahan untuk disemak"
if subtitle then
title = title .. "‌: \"" .. subtitle .. "\""
end
-- No ID, since these should always accompany proper translation tables, and can't be trusted anyway (i.e. there's no use-case for links).
return tostring(header) .. "\n" .. top(args, title, nil, html_create("div")
:addClass("NavHead")
:css("text-align", "left")
:wikitext(title or "Terjemahan")
)
end
-- Implements {{ter-bawah}}.
function export.bottom(frame)
-- Check nothing is being passed as a parameter.
process_params(frame:getParent().args, (parameters_data or get_parameters_data())["ter-bawah"])
return "</table></div></div>"
end
-- Implements {{ter-lihat}} and part of {{ter-atas-juga}}.
local function see(args, see_text)
local navhead = html_create("div")
:addClass("NavHead")
:css("text-align", "left")
:wikitext(args[1] .. " ")
:tag("span")
:css("font-weight", "normal")
:wikitext("— ")
:tag("i")
:wikitext(see_text)
:allDone()
local terms, id = args[2], args.id
if #terms == 0 then
terms[1] = args[1]
end
for i = 1, #terms do
local term_id = id[i] or id.default
local data = {
term = terms[i],
id = term_id and "Translations-" .. term_id or "Translations",
}
terms[i] = plain_link(data)
end
return navhead:wikitext(concat(terms, ",‎ "))
end
-- Entry point for {{ter-lihat}}.
function export.see(frame)
local args = process_params(frame:getParent().args, (parameters_data or get_parameters_data())["ter-lihat"])
local div = html_create("div")
:addClass("pseudo")
:addClass("NavFrame")
:node(see(args, "see "))
return tostring(add_id(div, args.id.default or args[1]))
end
-- Entry point for {{ter-atas-juga}}.
function export.top_also(frame)
local args = process_params(frame:getParent().args, (parameters_data or get_parameters_data())["ter-atas-juga"])
local navhead = see(args, "lihat juga ")
local title = args[1]
local id = args.id.default or title
title = remove_links(title)
return top(args, title, id, navhead)
end
-- Implements {{translation subpage}}.
function export.subpage(frame)
process_params(frame:getParent().args, (parameters_data or get_parameters_data())["translation subpage"])
if not is_translation_subpage() then
error("This template should only be used on translation subpages, which have titles that end with '/translations'.")
end
-- "Translation subpages" category is handled by {{trans-top}}.
return ("''This page contains translations for ''%s''. See the main entry for more information.''"):format(full_link{
lang = en or get_en(),
term = canonical_pagename(),
})
end
-- Implements {{t-needed}}.
function export.needed(frame)
local args = process_params(frame:getParent().args, (parameters_data or get_parameters_data())["t-needed"])
local lang, category = args[1], ""
local span = html_create("span")
:addClass("trreq")
:attr("data-lang", lang:getCode())
:tag("i")
:wikitext("sila tambah terjemahan ini jika boleh")
:done()
if not args.nocat then
local type, sort = args[2], args.sort
if type == "quote" then
category = "Permintaan terjemahan petikan bahasa " .. lang:getCanonicalName()
elseif type == "usex" then
category = "Permintaan terjemahan contoh penggunaan bahasa " .. lang:getCanonicalName()
else
category = "Permintaan terjemahan ke dalam bahasa " .. lang:getCanonicalName()
lang = en or get_en()
end
category = format_categories(category, lang, sort, not sort and canonical_pagename() or nil)
end
return tostring(span) .. category
end
-- Implements {{no equivalent translation}}.
function export.no_equivalent(frame)
local args = process_params(frame:getParent().args, (parameters_data or get_parameters_data())["no equivalent translation"])
local text = "tiada padanan kata dalam bahasa " .. args[1]:getCanonicalName()
if not args.noend then
text = text .. ", tetapi lihat"
end
return tostring(html_create("i"):wikitext(text))
end
-- Implements {{no attested translation}}.
function export.no_attested(frame)
local args = process_params(frame:getParent().args, (parameters_data or get_parameters_data())["no attested translation"])
local langname = args[1]:getCanonicalName()
local text = "tiada perkataan [[WT:ATTEST|disahkan]] dalam bahasa " .. langname
local category = ""
if not args.noend then
text = text .. ", but see"
local sort = args.sort
category = format_categories("Terjemahan bahasa " .. langname .. " yang tidak disahkan", en or get_en(), sort, not sort and canonical_pagename() or nil)
end
return tostring(html_create("i"):wikitext(text)) .. category
end
-- Implements {{not used}}.
function export.not_used(frame)
local args = process_params(frame:getParent().args, (parameters_data or get_parameters_data())["not used"])
return tostring(html_create("i"):wikitext((args[2] or "tidak digunakan") .. " dalam " .. args[1]:getCanonicalName()))
end
return export
c1k8q1fg9e3fsak57g1dpamqhv31pcf
epocale
0
10848
375827
245688
2026-09-25T04:11:51Z
Hakimi97
2668
/* Kata sifat */ Membetulkan templat
375827
wikitext
text/x-wiki
{{telah pap|2015|11}}
== Bahasa Itali ==
===Kata sifat===
{{it-adj}}
# [[zaman]] penting, sangat penting, [[menempa]] [[sejarah]]
====Istilah berkaitan====
* [[epoca]]
4anyg9ki10ghiodk3x99uzcxt11ow2p
Modul:ar-headword
828
11149
375804
375777
2026-09-24T14:05:31Z
Hakimi97
2668
Cuba betulkan (1)
375804
Scribunto
text/plain
-- Author: primarily Benwing2; some work by Fenakhay, Erutuon; early version by Rua
local export = {}
local pos_functions = {}
local force_cat = false -- for testing; if true, categories appear in non-mainspace pages
local ar_translit = require("Module:ar-translit")
local ar_verb_module = "Module:ar-verb"
local ar_utilities_module = "Module:ar-utilities"
local ar = require(ar_utilities_module)
local en_utilities_module = "Module:en-utilities"
local headword_module = "Module:headword"
local headword_utilities_module = "Module:headword utilities"
local links_module = "Module:links"
local inflection_utilities_module = "Module:inflection utilities"
local parse_utilities_module = "Module:parse utilities"
local require_when_needed = require("Module:utilities/require when needed")
local remove_links = require_when_needed(links_module, "remove_links")
local m_table = require("Module:table")
local m_str_utils = require("Module:string utilities")
local m_en_utilities = require_when_needed(en_utilities_module)
local m_headword_utilities = require_when_needed(headword_utilities_module)
local glossary_link = require_when_needed(headword_utilities_module, "glossary_link")
local boolean_param = {type = "boolean"}
local list_to_set = m_table.listToSet
local rfind = m_str_utils.find
local rmatch = m_str_utils.match
local rsubn = m_str_utils.gsub
local u = m_str_utils.char
local rsplit = m_str_utils.split
local insert = table.insert
local concat = table.concat
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local langcode = "ar"
local lang = require("Module:languages").getByCode(langcode)
local langname = lang:getCanonicalName()
local TEMPCOMMA = u(0xFFF0)
local TEMPARCOMMA = u(0xFFF1)
local misc_pos_with_gender = list_to_set {
"suffixes",
"adjective forms",
"noun forms",
"proper noun forms",
"pronoun forms",
"determiner forms",
}
-----------------------------------------------------------------------------------------
-- Utility functions --
-----------------------------------------------------------------------------------------
local dump = mw.dumpObject
-- version of mw.ustring.gsub() that discards all but the first return value
local function rsub(term, foo, bar)
local retval = rsubn(term, foo, bar)
return retval
end
local function ine(val)
if val == "" then return nil else return val end
end
-- Replace comma with a temporary char in comma + whitespace.
local function escape_comma_whitespace(run)
local escaped = false
if run:find("\\,") then
run = run:gsub("\\,", "\\" .. TEMPCOMMA)
escaped = true
end
if run:find("\\،") then
run = run:gsub("\\،", "\\" .. TEMPARCOMMA)
escaped = true
end
if run:find(",%s") then
run = run:gsub(",(%s)", TEMPCOMMA .. "%1")
escaped = true
end
if run:find("،%s") then
run = run:gsub("،(%s)", TEMPARCOMMA .. "%1")
escaped = true
end
return run, escaped
end
-- Undo replacement of comma with a temporary char in comma + whitespace.
local function unescape_comma_whitespace(run)
return (run:gsub(TEMPCOMMA, ","):gsub(TEMPARCOMMA, "،"))
end
-- Split an argument on comma or Arabic comma, but not either type of comma followed by whitespace.
local function split_on_comma(val)
if rfind(val, "[,،]%s") or val:find("\\") then
return export.split_escaping(val, "[,،]", false, escape_comma_whitespace, unescape_comma_whitespace)
else
return rsplit(val, "[,،]")
end
end
local function replace_tr_ending(tr, from, to)
if not tr then
return nil
end
local pref = tr:match("^(.*)" .. from .. "$")
if not pref then
error(("Translit '%s' does not end in -%s, as expected"):format(tr, from))
end
return pref .. to
end
-----------------------------------------------------------------------------------------
-- Tracking functions --
-----------------------------------------------------------------------------------------
local trackfn = require("Module:debug/track")
local function track(page)
trackfn(langcode .. "-headword/" .. page)
return true
end
--[==[
Examples of what you can find by looking at what links to the given
pages:
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized]]
all unvocalized pages
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/pl]]
all unvocalized pages where the plural is unvocalized,
whether specified using pl=, pl2=, etc.
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/head]]
all unvocalized pages where the head is unvocalized
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/head/nouns]]
all nouns excluding proper nouns, collective nouns,
singulative nouns where the head is unvocalized
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/head/proper]]
nouns all proper nouns where the head is unvocalized
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/head/not]]
proper nouns all words that are not proper nouns
where the head is unvocalized
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/adjectives]]
all adjectives where any parameter is unvocalized;
currently only works for heads,
so equivalent to .../unvocalized/head/adjectives
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized-empty-head]]
all pages with an empty head
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized-manual-translit]]
all unvocalized pages with manual translit
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized-manual-translit/head/nouns]]
all nouns where the head is unvocalized but has manual translit
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized-no-translit]]
all unvocalized pages without manual translit
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab]]
all pages with any parameter containing i3rab
of either -un, -u, -a or -i
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab-un]]
all pages with any parameter containing an -un i3rab ending
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab-un/pl]]
all pages where a form specified using pl=, pl2=, etc.
contains an -un i3rab ending
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab-u/head]]
all pages with a head containing an -u i3rab ending
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab/head/proper]]
nouns (all proper nouns with a head containing i3rab
of either -un, -u, -a or -i)
In general, the format is one of the following:
Wiktionary:Tracking/ar-headword/FIRSTLEVEL
Wiktionary:Tracking/ar-headword/FIRSTLEVEL/ARGNAME
Wiktionary:Tracking/ar-headword/FIRSTLEVEL/POS
Wiktionary:Tracking/ar-headword/FIRSTLEVEL/ARGNAME/POS
FIRSTLEVEL can be one of "unvocalized", "unvocalized-empty-head" or its
opposite "unvocalized-specified", "unvocalized-manual-translit" or its
opposite "unvocalized-no-translit", "i3rab", "i3rab-un", "i3rab-u",
"i3rab-a", or "i3rab-i".
ARGNAME is either "head" or an argument such as "pl", "f", "cons", etc.
This automatically includes arguments specified as head2=, pl3=, etc.
POS is a part of speech, lowercase and singular, e.g. "kata nama",
"Kata sifat", "Kata nama khas", "collective nouns", etc. or
"not proper noun", which includes all parts of speech but proper nouns.
]==]
local function track_form(argname, form, translit, pos)
form = ar.reorder_shadda(remove_links(form))
function dotrack(page)
track(page)
track(page .. "/" .. argname)
if pos then
track(page .. "/" .. pos)
track(page .. "/" .. argname .. "/" .. pos)
if pos ~= "Kata nama khas" then
track(page .. "/not proper noun")
track(page .. "/" .. argname .. "/not proper noun")
end
end
end
function track_i3rab(arabic, tr)
if rfind(form, arabic .. "$") then
dotrack("i3rab")
dotrack("i3rab-" .. tr)
end
end
track_i3rab(ar.UN, "un")
track_i3rab(ar.U, "u")
track_i3rab(ar.A, "a")
track_i3rab(ar.I, "i")
if form == "" or not (lang:transliterate(form)) then
dotrack("unvocalized")
if form == "" then
dotrack("unvocalized-empty-head")
else
dotrack("unvocalized-specified")
end
if translit then
dotrack("unvocalized-manual-translit")
else
dotrack("unvocalized-no-translit")
end
end
end
-----------------------------------------------------------------------------------------
-- Inflection-parsing functions --
-----------------------------------------------------------------------------------------
-- Construct the default construct state or informal form of a term in lemma format. Usually this is the same as the
-- lemma but is different for final-weak nouns and adjectives ending in -n in their lemma. NOTE: Input must be
-- shadda-reordered for this to work properly.
local function default_construct_state_or_informal(term, tr)
local pref = term:match("^(.*)" .. ar.HAMZA .. ar.IN .."$")
-- Hamza on the line with -in changes to hamza-on-yā with -ī.
if pref then
return pref .. ar.HAMZA_ON_YA .. ar.II, replace_tr_ending(tr, "in", "ī")
end
-- Otherwise just change -in to -ī.
pref = term:match("^(.*)" .. ar.IN .. "$")
if pref then
return pref .. ar.II, replace_tr_ending(tr, "in", "ī")
end
-- Change -an with alif maqṣūra to -ā with alif maqṣūra.
pref = term:match("^(.*)" .. ar.AN .. ar.AMAQ .. "$")
if pref then
return pref .. ar.AAMAQ, replace_tr_ending(tr, "an", "ā")
end
-- Change -an with tall alif (e.g. عَصًا) to -ā with tall alif.
pref = term:match("^(.*)" .. ar.AN .. ar.ALIF .. "$")
if pref then
return pref .. ar.AA, replace_tr_ending(tr, "an", "ā")
end
return term, tr
end
local function generate_construct_state_or_informal_default(data, args)
local heads = data.heads
local consobjs = {}
local different_cons = false
for _, headobj in ipairs(data.heads) do
local consterm, constr = default_construct_state_or_informal(headobj.term, headobj.tr)
different_cons = different_cons or consterm ~= headobj.term or constr ~= headobj.tr
local consobj = m_table.shallowCopy(headobj)
consobj.term = consterm
consobj.tr = constr
insert(consobjs, consobj)
end
if different_cons then
return consobjs
else
return {}
end
end
local noun_field_cons = {
field = "cons", label = "<<construct state>>", generate_default = generate_construct_state_or_informal_default,
default_when_not_explicit = function(args, data) return true end,
}
local noun_field_inf = {field = "inf", label = "informal"}
local noun_field_obl = {field = "obl", label = "<<oblique>>"}
local noun_field_def = {field = "def", label = "<<definite>> state"}
local noun_inflections = {
noun_field_cons,
noun_field_inf,
noun_field_obl,
noun_field_def,
}
local adj_field_inf = {
field = "inf", label = "informal", generate_default = generate_construct_state_or_informal_default,
default_when_not_explicit = function(args, data) return true end,
}
local adj_field_obl = noun_field_obl
local adj_field_def = noun_field_def
local adjective_inflections = {
adj_field_inf,
adj_field_obl,
adj_field_def,
}
local function has_construct_state(data)
return data.pos_category ~= "Kata sifat"
end
local function parse_nominal_inflection(paramname, val, parse_err)
return m_headword_utilities.parse_term_with_modifiers {
val = val,
paramname = paramname,
splitchar = ",",
include_mods = {"tr", "g"},
}
end
local function make_nominal_inflection_param_mod_spec(paramname)
return {convert = function(val, parse_err)
return parse_nominal_inflection(paramname, val, parse_err)
end}
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
local argpref = field
if type(argfield) == "table" then
argpref = argfield[2]
argfield = argfield[1]
end
local include_mods
if is_head then
include_mods = {"tr"}
else
include_mods = {"tr", "g"}
for _, spec in ipairs(has_construct_state(data) and noun_inflections or adjective_inflections) do
insert(include_mods, {spec.field, make_nominal_inflection_param_mod_spec(argpref .. "." .. spec.field)})
end
end
if is_head then
local retval
if args[argfield] then
retval = m_headword_utilities.parse_term_with_modifiers {
val = args[argfield],
paramname = field,
splitchar = ",",
is_head = is_head,
include_mods = include_mods,
}
end
return retval or {}
else
return m_headword_utilities.parse_term_list_with_modifiers {
forms = args[argfield],
paramname = field,
splitchar = ",",
is_head = is_head,
include_mods = include_mods,
}
end
end
local function insert_inflection(data, terms, label, accel, defgender, track_field, no_label, usually_no_label)
local track_pos = m_en_utilities.singularize(data.pos_category)
for _, termobj in ipairs(terms) do
-- If the user supplied a construct state or informal form for the term with a value of "+", substitute the
-- default value for the term. If the user supplied a value of "--", they want no value displayed. Otherwise,
-- if the user didn't supply any value, we check to see if the default construct state or informal form is
-- different from the lemma and display it if so; this applies particularly to terms in '-in' and '-an', where
-- the default construct state or informal form is almost always correct.
local field = has_construct_state(data) and "cons" or "inf"
if not termobj[field] then
local defcons, defconstr = default_construct_state_or_informal(termobj.term, termobj.tr)
if termobj.term ~= defcons or termobj.tr ~= defconstr then
-- We don't want to copy decorations from the term object because we're a subinflection of the term
-- object.
termobj[field] = {{term = defcons, tr = defconstr}}
end
elseif termobj[field][1].term == "--" then
if termobj[field][2] then
error("Can't specify more than one value for <" .. field .. ":...> if first value is '--', meaning \"don't insert anything\"")
end
termobj[field] = nil
else
for i, consobj in ipairs(termobj[field]) do
if consobj.term == "+" then
if consobj.tr then
error("Can't specify translit for default value '+'")
end
consobj.term, consobj.tr = default_construct_state_or_informal(termobj.term, termobj.tr)
elseif consobj.term == "~" then
if consobj.tr then
error("Can't specify translit for term-requesting value '~'")
end
consobj.term, consobj.tr = termobj.term, termobj.tr
end
end
end
if defgender and not termobj.genders then
termobj.genders = {{spec = defgender}}
end
local function insert_nested_inflection(field, label)
if termobj[field] then
m_headword_utilities.insert_inflection {
headdata = data,
inflobj = termobj,
terms = termobj[field],
label = label
}
end
end
for _, spec in ipairs(has_construct_state(data) and noun_inflections or adjective_inflections) do
insert_nested_inflection(spec.field, spec.label)
end
track_form(track_field, termobj.term, termobj.tr, track_pos)
end
m_headword_utilities.insert_inflection {
headdata = data,
terms = terms,
label = label,
accel = accel and {form = accel} or nil,
no_label = no_label,
usually_no_label = usually_no_label,
}
end
-----------------------------------------------------------------------------------------
-- Main entry point --
-----------------------------------------------------------------------------------------
function export.show(frame)
local iparams = {
[1] = true,
}
local iargs = require("Module:parameters").process(frame.args, iparams)
local parargs = frame:getParent().args
local poscat = iargs[1]
local pos_in_1 = not poscat
if pos_in_1 then
poscat = ine(parargs[1]) or
mw.title.getCurrentTitle().fullText == "Templat:" .. langcode .. "-head" and "interjection" or
error("Part of speech must be specified in 1=")
poscat = require(headword_module).canonicalize_pos(poscat)
end
local indexing_poscat = pos_in_1 and (misc_pos_with_gender[poscat] and "head_with_gender" or "head") or poscat
local params = {
["suffix"] = boolean_param,
["nosuffix"] = boolean_param,
["id"] = true,
["json"] = boolean_param,
["pagename"] = {}, -- for testing
}
if pos_in_1 then
params[1] = {required = true} -- required but ignored as already processed above
end
local head_is_head = pos_functions[indexing_poscat] and pos_functions[indexing_poscat].head_is_not_1
local headfield = head_is_head and "head" or pos_in_1 and 2 or 1
params[headfield] = head_is_head and true or {default = "+"}
params.head2 = {replaced_by = false, instead = "use multiple comma-separated values in |" .. headfield .. "="}
local tr_replaced_by = {replaced_by = false, instead = "use <tr:...> inline modifier on |" .. headfield .. "="}
params.tr = tr_replaced_by
params.tr2 = tr_replaced_by
if pos_functions[indexing_poscat] then
for key, val in pairs(pos_functions[indexing_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,
pos_category = poscat,
orig_pos_category = poscat,
categories = {},
heads = {},
genders = {},
inflections = {enable_auto_translit = true},
pagename = pagename,
id = args.id,
sort_key = args.sort,
force_cat_output = force_cat,
-- We expect a head always so the redundant head cat will be inaccurate.
no_redundant_head_cat = true,
}
data.heads = parse_inflection(data, args, headfield, "is_head")
for _, headobj in ipairs(data.heads) do
if headobj.term == "+" then
headobj.term = pagename
end
end
data.is_suffix = false
if args.suffix or (
not args.nosuffix and pagename:find("^%-") and poscat ~= "suffixes" and poscat ~= "suffix forms"
) then
data.is_suffix = true
data.pos_category = "suffixes"
local singular_poscat = m_en_utilities.singularize(poscat)
insert(data.categories, langname .. " " .. singular_poscat .. "-forming suffixes")
insert(data.inflections, {label = singular_poscat .. "-forming suffix"})
end
if pos_functions[indexing_poscat] then
pos_functions[indexing_poscat].func(data, args)
end
-- Do this after calling pos_functions[poscat].func() as it may modify data.heads (as verbs do).
local irreg_translit = false
for _, head in ipairs(data.heads) do
if ar_translit.irregular_translit(head.term, head.tr) then
irreg_translit = true
break
end
end
if irreg_translit then
insert(data.categories, langname .. " terms with irregular pronunciations")
end
if args.json then
return require("Module:JSON").toJSON(data)
end
return require(headword_module).full_headword(data)
end
-----------------------------------------------------------------------------------------
-- Gender handling --
-----------------------------------------------------------------------------------------
local valid_bare_genders = {false, "m", "f", "mf", "mfbysense", "mfequiv"}
local valid_bare_numbers = {false, "d", "p"}
local valid_bare_animacies = {false, "pr", "np"}
local valid_genders = {}
for _, gender in ipairs(valid_bare_genders) do
for _, number in ipairs(valid_bare_numbers) do
for _, animacy in ipairs(valid_bare_animacies) do
local parts = {}
local function ins_part(part)
if part then
insert(parts, part)
end
end
ins_part(gender)
ins_part(number)
ins_part(animacy)
local full_gender = concat(parts, "-")
valid_genders[full_gender == "" and "?" or full_gender] = true
end
end
end
local function is_masc_sg(g)
return g == "m" or g == "m-pr" or g == "m-np"
end
local function is_fem_sg(g)
return g == "f" or g == "f-pr" or g == "f-np"
end
local function is_masc_fem_sg(g)
g = g:gsub("%-pr", ""):gsub("%-np", "")
return g == "mf" or g == "mfequiv" or g == "mfbysense"
end
local function add_gender_params(params, default)
params[2] = {type = "genders", default = default or "?", template_default = "m"}
params["g2"] = {replaced_by = false, instead = "use comma-separated values in |g="}
end
-- Handle gender in params 2=, inserting into `data.genders`. Also, if a lemma, insert categories into `data.categories`
-- if the gender is unexpected for the form of the noun. (Note: If there are multiple genders,
-- [[Module:gender and number]] will automatically insert 'Arabic POS with multiple genders'.)
local function handle_gender(data, args, nonlemma, field)
if not args[field or 2] then
return
end
for _, gspec in ipairs(args[field or 2]) do
if not valid_genders[gspec.spec] then
error("Unrecognized gender: " .. gspec.spec)
end
end
data.genders = args[field or 2]
if nonlemma then
return
end
for _, gspec in ipairs(data.genders) do
local g = gspec.spec
if is_masc_sg(g) or is_fem_sg(g) or is_masc_fem_sg(g) then
local head = data.heads[1]
if head then
head = rsub(ar.reorder_shadda(remove_links(head.term)), ar.UNUOPT .. "$", "")
local ends_with_tam = rfind(head, "^[^ ]*" .. ar.TAM .. "$") or
rfind(head, "^[^ ]*" .. ar.TAM .. " ")
if (is_masc_sg(g) or is_masc_fem_sg(g)) and ends_with_tam then
insert(data.categories, langname .. " masculine terms with feminine ending")
elseif (is_fem_sg(g) or is_masc_fem_sg(g)) and not ends_with_tam and
not rfind(head, "[" .. ar.ALIF .. ar.AMAQ .. "]$") and
not rfind(head, ar.ALIF .. ar.HAMZA .. "$") then
insert(data.categories, langname .. " feminine terms lacking feminine ending")
end
end
end
end
end
-----------------------------------------------------------------------------------------
-- Inflection handlers --
-----------------------------------------------------------------------------------------
-- Add list parameters to `params` (a structure as passed to [[Module:parameters]]) for a parameter named `argpref`.
-- If `argpref` is "*", add the nominal inflection parameters for construct state, definite state, etc. Related
-- transliteration and gender parameters are no longer supported in favor of inline modifiers, and error messages are
-- output if these parameters are used.
local function add_infl_params(params, argpref)
params[argpref] = {list = true, disallow_holes = true}
params[argpref .. "tr"] = {replaced_by = false, instead = "use <tr:...> inline modifier on |" .. argpref .. "="}
params[argpref .. "g"] = {replaced_by = false, instead = "use <g:...> inline modifier on |" .. argpref .. "="}
end
--[=[
Fetch a list of inflections from the arguments in `args` based on argument `field` (e.g. "pl"). Label with `label`
(e.g. "plural"), which will appear in the headword. Insert into `data.inflections`, where `data` is the structure
passed to [[Module:headword]]. If `generate_default` is specified, it should be a function of two arguments
(`data`, `args`), which should generate the default value if no values are specified or if "+" is explicitly given.
If `generate_default` isn't specified and the user gave no values, no inflection will be inserted.
]=]
local function handle_infl(data, args, spec)
local newinfls = parse_inflection(data, args, spec.field, false)
if not newinfls[1] and spec.default_when_not_explicit and spec.default_when_not_explicit(data, args) then
newinfls = {{term = "+"}}
end
if spec.handle then
spec.handle(data, args, newinfls)
end
local default_specs = spec.allowed_defspecs
if not default_specs then
default_specs = spec.generate_default and {["+"] = true} or {}
end
local saw_defspec = false
for _, newinfl in ipairs(newinfls) do
if default_specs[newinfl.term] or newinfl.term == "~" then
saw_defspec = true
break
end
end
if saw_defspec then
local newnewinfls = {}
for _, newinfl in ipairs(newinfls) do
if default_specs[newinfl.term] then
if newinfl.tr then
error("Can't specify translit for default value '" .. newinfl.term .. "'")
end
local definfls = spec.generate_default(data, args, newinfl.term)
for _, definfl in ipairs(definfls) do
m_headword_utilities.combine_termobj_decorations(definfl, newinfl)
insert(newnewinfls, definfl)
end
elseif newinfl.term == "~" then
if newinfl.tr then
error("Can't specify translit for head-requesting value '~'")
end
for _, headobj in ipairs(data.heads) do
headobj = m_table.shallowCopy(headobj)
m_headword_utilities.combine_termobj_decorations(headobj, newinfl)
insert(newnewinfls, headobj)
end
else
insert(newnewinfls, newinfl)
end
end
newinfls = newnewinfls
end
if newinfls[1] then
if newinfls[1].term == "--" then
if newinfls[2] then
error("Can't specify more than one term if first term is '--', meaning \"don't insert anything\"")
end
else
insert_inflection(data, newinfls, spec.label, nil, spec.defgender, spec.field, spec.no_label,
spec.usually_no_label)
end
end
end
local function add_infl_list_params(params, infl_list)
for _, infl in ipairs(infl_list) do
add_infl_params(params, infl.field)
end
end
local function handle_infl_list_args(data, args, infl_list)
for _, infl in ipairs(infl_list) do
handle_infl(data, args, infl)
end
end
-----------------------------------------------------------------------------------------
-- Default ending generators --
-----------------------------------------------------------------------------------------
local function make_conditional_default(specs)
return function(data, args)
local heads = data.heads
if not heads[1] then
heads = {{term = data.pagename}}
end
local newobjs = {}
for _, headobj in ipairs(heads) do
local term = ar.reorder_shadda(headobj.term)
local tr = headobj.tr
local matched = false
for _, spec in ipairs(specs) do
local from, fromtr, to, totr = unpack(spec)
if from:find("^%^") then
pref = rmatch(term, from .. "$")
else
pref = rmatch(term, "^(.*)" .. from .. "$")
end
if pref then
term = pref .. to
tr = replace_tr_ending(tr, fromtr, totr)
matched = true
headobj = m_table.shallowCopy(headobj)
headobj.term = ar.undo_reorder_shadda(term)
headobj.tr = tr
insert(newobjs, headobj)
break
end
end
if not matched then
error(("Internal error: No matching spec: head=%s"):format(dump(headobj)))
end
end
return newobjs
end
end
local default_feminine = make_conditional_default {
{ar.AN .. ar.AMAQ, "an", ar.AAH, "āh"},
{ar.AN .. ar.ALIF, "an", ar.AAH, "āh"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_YA .. ar.IYAH, "iya"},
{ar.IN, "in", ar.IYAH, "iya"},
{"", "", ar.AH, "a"},
}
local default_masculine = make_conditional_default {
-- tall alif substitutes for alif maqṣūra after a yāʔ
{ar.Y .. ar.AAH, "āh", ar.AN .. ar.ALIF, "an"},
{ar.AAH, "āh", ar.AN .. ar.AMAQ, "an"},
-- handle the common case of final-weak feminine active participle with preceding hamza;
-- the hamza-on-yāʔ always converts back to hamza on the line when preceded by ā (alif) but
-- may not otherwise, so we just leave it alone in that case
{ar.ALIF .. ar.HAMZA_ON_YA .. ar.IYAH, "iya", ar.HAMZA .. ar.IN, "in"},
{ar.IYAH, "iya", ar.IN, "in"},
{ar.AH, "a", "", ""},
{"", "", "", ""},
}
local default_masculine_plural = make_conditional_default {
{ar.AN .. ar.AMAQ, "an", ar.AWN, "awn"},
{ar.AN .. ar.ALIF, "an", ar.AWN, "awn"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_WAW .. ar.UUN, "ūn"},
{ar.IN, "in", ar.UUN, "ūn"},
{"", "", ar.UUN, "ūn"},
}
local default_feminine_plural = make_conditional_default {
-- صَلَاة pl. صَلَوَات and أَدَاة pl. أَدَوَات and similar; but نَوَاة and وَفَاة with a و in them become نَوَيَات and وَفَيَات;
-- and longer terms like مُبَارَاة and كُمَّثْرَاة invariably form their plural in -يَات.
{"^([^و]" .. ar.A .. "[^و])" .. ar.AAH, "āh", ar.A .. ar.W .. ar.AAT, "awāt"},
{ar.AAH, "āh", ar.AYAAT, "ayāt"},
{ar.AN .. ar.AMAQ, "an", ar.AYAAT, "ayāt"},
{ar.AN .. ar.ALIF, "an", ar.AYAAT, "ayāt"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_YA .. ar.IYAAT, "iyāt"},
{ar.IN, "in", ar.IYAAT, "iyāt"},
{ar.AH, "a", ar.AAT, "āt"},
{"", "", ar.AAT, "āt"},
}
local default_masculine_dual = make_conditional_default {
{ar.AN .. ar.AMAQ, "an", ar.AYAAN, "ayān"},
{ar.AN .. ar.ALIF, "an", ar.AYAAN, "ayān"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_YA .. ar.IYAAN, "iyān"},
{ar.IN, "in", ar.IYAAN, "iyān"},
{"", "", ar.AAN, "ān"},
}
local default_feminine_dual = make_conditional_default {
{ar.AN .. ar.AMAQ, "an", ar.AATAAN, "ātān"},
{ar.AN .. ar.ALIF, "an", ar.AATAAN, "ātān"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_YA .. ar.IY .. ar.ATAAN, "iyatān"},
{ar.IN, "in", ar.IY .. ar.ATAAN, "iyatān"},
{"", "", ar.ATAAN, "atān"},
}
-- Return whether `term` is a nisba noun or adjective, ending in -iyy or -iyyah. `nisba_val` is the value of
-- args.nisba; if non-nil, it overrides any auto-determination based on the shape of the term.
local function term_is_nisba(term, nisba_val)
if nisba_val ~= nil then
return nisba_val
end
term = ar.reorder_shadda(term) -- necessary to avoid issues with e.g. أُورُوبِّيّ.
local pref = rmatch(term, "^(.*)" .. ar.IYY .. ar.UN .. "?$")
if not pref then
pref = rmatch(term, "^(.*)" .. ar.IYYAH .. ar.UN .. "?$")
end
-- Avoid false positives for words like قَوِيّ "strong" and صَبِيّ "boy". There may be other false positives
-- but this should catch most of them and will avoid very many false negatives.
return pref and not rfind(pref, "^[^ا]" .. ar.A .. ".$")
end
-----------------------------------------------------------------------------------------
-- Adjectives --
-----------------------------------------------------------------------------------------
local function is_defaulting_adjective(data, args)
return data.orig_pos_category == "defaulting adjectives"
end
local adj_field_elative = {field = "el", label = "<<elative>>"}
local adj_inflections = {
adj_field_inf,
adj_field_obl,
adj_field_def,
{field = "f", label = "feminin", generate_default = default_feminine,
default_when_not_explicit = is_defaulting_adjective},
{field = "d", label = "maskulin duaan", generate_default = default_masculine_dual},
{field = "fd", label = "feminin duaan", generate_default = default_feminine_dual},
{field = "cpl", label = "am jamak"},
{field = "pl", label = "maskulin jamak", generate_default = default_masculine_plural,
default_when_not_explicit = is_defaulting_adjective},
{field = "fpl", label = "feminin jamak", generate_default = default_feminine_plural,
default_when_not_explicit = is_defaulting_adjective},
}
local function get_adj_params()
local params = {}
add_infl_list_params(params, adj_inflections)
add_infl_params(params, "el")
params.nisba = boolean_param
return params
end
local function handle_adj_args(data, args)
handle_infl_list_args(data, args, adj_inflections)
handle_infl(data, args, adj_field_elative)
for _, headobj in ipairs(data.heads) do
if term_is_nisba(headobj.term, args.nisba) then
insert(data.categories, "Kata sifat relatif (nisba) bahasa " .. langname)
break
end
end
end
pos_functions["Kata sifat"] = {
params = get_adj_params,
func = handle_adj_args,
}
pos_functions["defaulting adjectives"] = {
params = get_adj_params,
func = function(data, args)
data.pos_category = "Kata sifat"
handle_adj_args(data, args)
end,
}
-----------------------------------------------------------------------------------------
-- Nouns, etc. --
-----------------------------------------------------------------------------------------
local function get_masc_or_feminine_gender(data, default_type)
local saw_m, saw_f, saw_mf
for _, gender in ipairs(data.genders) do
if is_masc_sg(gender.spec) then
saw_m = true
elseif is_fem_sg(gender.spec) then
saw_f = true
elseif is_masc_fem_sg(gender.spec) then
saw_mf = true
end
end
if saw_mf or saw_m and saw_f then
error("Can't generate default for " .. default_type .. " when gender is both masculine and feminine")
elseif saw_m then
return "m"
elseif saw_f then
return "f"
else
error("Can't generate default for " .. default_type .. " when gender is not specified as " ..
"maskulin or feminin tunggal")
end
end
local function is_defaulting_noun(data, args)
return data.orig_pos_category == "defaulting nouns"
end
local noun_field_dual = {
field = "d", label = "dual",
generate_default = function(data, args)
local gender = get_masc_or_feminine_gender(data, "noun dual")
if gender == "m" then
return default_masculine_dual(data, args)
else
return default_feminine_dual(data, args)
end
end,
}
local noun_field_plural = {
field = "pl", label = "jamak",
generate_default = function(data, args, defspec)
local gender = get_masc_or_feminine_gender(data, "noun plural")
if gender == "m" then
if defspec == "+f" then
return default_feminine_plural(data, args)
else
return default_masculine_plural(data, args)
end
elseif defspec == "+f" then
error("Can't specify '+f' with feminine gender; just use '+'")
else
return default_feminine_plural(data, args)
end
end,
-- Handle the case where pl=-, indicating an uncountable noun.
handle = function(data, args, terms)
if terms[1] and terms[1] == "-" then
insert(data.categories, langname .. " uncountable nouns")
if args.pauc and args.pauc[1] then
error("Can't specify paucals when pl=-")
end
end
end,
allowed_defspecs = {["+"] = true, ["+f"] = true},
default_when_not_explicit = is_defaulting_noun,
no_label = "<<uncountable>>",
usually_no_label = "usually <<uncountable>>",
}
local noun_field_paucal = {
field = "pauc", label = "<<paucal>>", generate_default = default_feminine_plural,
}
local noun_field_feminine = {
field = "f", label = "feminin", generate_default = default_feminine,
default_when_not_explicit = function(data, args)
if data.orig_pos_category ~= "defaulting nouns" then
return nil
end
local gender = get_masc_or_feminine_gender(data, "defaulting-if-masculine noun feminine")
return gender == "m"
end,
}
local noun_field_masculine = {
field = "m", label = "maskulin", generate_default = default_masculine,
default_when_not_explicit = function(data, args)
if data.orig_pos_category ~= "defaulting nouns" then
return nil
end
local gender = get_masc_or_feminine_gender(data, "defaulting-if-feminine noun masculine")
return gender == "f"
end,
}
local noun_basic_inflections = {
noun_field_cons,
noun_field_inf,
noun_field_obl,
noun_field_def,
}
local noun_shared_inflections = {
noun_field_dual,
noun_field_plural,
}
local noun_extra_inflections = {
noun_field_paucal,
noun_field_feminine,
noun_field_masculine,
}
local function get_noun_params()
local params = {}
add_gender_params(params)
add_infl_list_params(params, noun_basic_inflections)
add_infl_list_params(params, noun_shared_inflections)
add_infl_list_params(params, noun_extra_inflections)
params.nisba = boolean_param
return params
end
local function handle_noun_args(data, args)
handle_gender(data, args)
handle_infl_list_args(data, args, noun_basic_inflections)
handle_infl_list_args(data, args, noun_shared_inflections)
handle_infl_list_args(data, args, noun_extra_inflections)
for _, headobj in ipairs(data.heads) do
if term_is_nisba(headobj.term, args.nisba) then
insert(data.categories, langname .. " relative nouns (nisba)")
break
end
end
end
pos_functions["Kata nama"] = {
params = get_noun_params,
func = handle_noun_args,
}
pos_functions["defaulting nouns"] = {
params = get_noun_params,
func = function(data, args)
data.pos_category = "Kata nama"
handle_noun_args(data, args)
end,
}
local noun_field_singulative = {field = "sing", label = "<<singulative>>", defgender = "f", generate_default = default_feminine}
local noun_field_collective = {field = "coll", label = "<<collective>>", defgender = "m", generate_default = default_masculine}
local function handle_sing_coll_noun_infls(data, args, otherinfl, otherlabel, othergender)
-- Handle sing= (corresponding singulative noun) or coll= (corresponding collective noun) and their gender
handle_infl(data, args, otherinfl, otherlabel, nil, othergender)
handle_infl_list_args(data, args, sing_coll_noun_inflections)
end
local function get_singulative_collective_noun_params(defgender, otherinfl)
local params = {}
add_gender_params(params, defgender)
add_infl_list_params(params, noun_basic_inflections)
add_infl_params(params, otherinfl)
add_infl_list_params(params, noun_shared_inflections)
add_infl_params(params, "pauc")
return params
end
pos_functions["collective nouns"] = {
params = function() return get_singulative_collective_noun_params("m", "sing") end,
func = function(data, args)
data.pos_category = "Kata nama"
insert(data.categories, langname .. " collective nouns")
m_headword_utilities.insert_fixed_inflection {
headdata = data,
label = "<<collective>>",
}
handle_gender(data, args)
handle_infl_list_args(data, args, noun_basic_inflections)
handle_infl(data, args, noun_field_singulative)
handle_infl_list_args(data, args, noun_shared_inflections)
handle_infl(data, args, noun_field_paucal)
end
}
pos_functions["singulative nouns"] = {
params = function() return get_singulative_collective_noun_params("f", "coll") end,
func = function(data, args)
data.pos_category = "Kata nama"
insert(data.categories, langname .. " singulative nouns")
m_headword_utilities.insert_fixed_inflection {
headdata = data,
label = "<<singulative>>",
}
handle_gender(data, args)
handle_infl_list_args(data, args, noun_basic_inflections)
handle_infl(data, args, noun_field_collective)
handle_infl_list_args(data, args, noun_shared_inflections)
handle_infl(data, args, noun_field_paucal)
end
}
-- FIXME: Do numerals really behave almost as nouns? They vary by masc/fem.
pos_functions["numerals"] = {
params = get_noun_params,
func = function(data, args)
insert(data.categories, langname .. " cardinal numbers")
handle_noun_args(data, args)
end
}
pos_functions["Kata nama khas"] = {
params = get_noun_params,
func = handle_noun_args,
}
local function get_pronoun_params()
local params = {}
add_gender_params(params, defgender)
add_infl_list_params(params, noun_basic_inflections)
add_infl_list_params(params, noun_shared_inflections)
add_infl_params(params, "f")
return params
end
pos_functions["pronouns"] = {
params = get_pronoun_params,
func = function(data, args)
handle_gender(data, args)
handle_infl_list_args(data, args, noun_basic_inflections)
handle_infl_list_args(data, args, noun_shared_inflections)
handle_infl(data, args, noun_field_feminine)
end
}
-----------------------------------------------------------------------------------------
-- Non-lemma forms --
-----------------------------------------------------------------------------------------
local valid_forms = list_to_set(
{ "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII",
"XIII", "XIV", "XV", "Iq", "IIq", "IIIq", "IVq" })
-- FIXME: Partly duplicated in [[Module:ar-inflections]].
local function handle_conj_form(data, args)
local form = args[2]
if form then
if not valid_forms[form] then
error("Invalid verb conjugation form " .. form)
end
insert(data.inflections, { label = "[[Appendix:Arabic verbs#Form " .. form .. "|form " .. form .. "]]" })
end
end
pos_functions["verb forms"] = {
params = function()
return {
[2] = {},
}
end,
func = function(data, args)
handle_conj_form(data, args)
end
}
local function get_participle_params()
local params = get_adj_params()
params[2] = {}
return params
end
pos_functions["active participles"] = {
params = get_participle_params,
func = function(data, args)
data.pos_category = "participles"
insert(data.categories, langname .. " active participles")
handle_conj_form(data, args)
handle_infl_list_args(data, args, adj_inflections)
end
}
pos_functions["passive participles"] = {
params = get_participle_params,
func = function(data, args)
data.pos_category = "participles"
insert(data.categories, langname .. " passive participles")
handle_conj_form(data, args)
handle_infl_list_args(data, args, adj_inflections)
end
}
-----------------------------------------------------------------------------------------
-- Verbs --
-----------------------------------------------------------------------------------------
pos_functions["verbs"] = {
head_is_not_1 = true,
params = function() return {
[1] = {},
-- Comma-separated lists with possible inline modifiers
["past"] = {},
["past1s"] = {},
["nonpast"] = {},
["vn"] = {},
["noautolinktext"] = {type = "boolean"},
["noautolinkverb"] = {type = "boolean"},
} end,
func = function(data, args)
local ar_verb = require(ar_verb_module)
local alternant_multiword_spec =
args[1] ~= "-" and ar_verb.do_generate_forms(args, "ar-verb", data.pagename) or nil
local function do_slot(slots_to_check, override, label, slot_is_headword)
-- Do this even with an override so we can return the correct filled slot.
local slot, slotval
if alternant_multiword_spec then
for _, potential_slot in ipairs(slots_to_check) do
slotval = alternant_multiword_spec.forms[potential_slot]
if slotval then
slot = potential_slot
break
end
end
end
local function get_slot_values()
local terms = {}
for _, form in ipairs(slotval) do
local term = {
term = form.form,
id = form.id,
genders = form.genders,
pos = form.pos,
lit = form.lit,
}
term.tr = form.translit
if form.footnotes then
local quals, refs = require(inflection_utilities_module).
convert_footnotes_to_qualifiers_and_references(form.footnotes)
term.q = quals
term.refs = refs
end
insert(terms, term)
end
return terms
end
if override then
local override_param_mods = {
alt = {},
t = {
-- [[Module:headword]] expects the gloss in "gloss".
item_dest = "gloss",
},
gloss = {},
g = {
-- [[Module:headword]] expects the genders in "genders".
item_dest = "genders",
type = "genders",
},
pos = {},
lit = {},
id = {},
-- Qualifiers and labels
q = {
type = "qualifier",
},
qq = {
type = "qualifier",
},
l = {
type = "labels",
},
ll = {
type = "labels",
},
ref = {
-- [[Module:headword]] expects the references in "refs".
item_dest = "refs",
type = "references",
},
}
local function generate_obj(formval, parse_err)
if formval == "+" then
return {term = "+", underlying_terms = get_slot_values()}
end
local val, uncertain = formval:match("^(.*)(%?)$")
val = val or formval
uncertain = not not uncertain
local ar, translit = val:match("^(.*)//(.*)$")
if not ar then
ar = formval
end
local retval = {term = ar, uncertain = uncertain}
retval.tr = translit
end
local terms
if override:find("<") then
terms = require(parse_utilities_module).parse_inline_modifiers(override, {
paramname = paramname,
param_mods = override_param_mods,
generate_obj = generate_obj,
splitchar = "[,،]",
escape_fun = escape_comma_whitespace,
unescape_fun = unescape_comma_whitespace,
})
else
terms = split_on_comma(override)
for i, split in ipairs(terms) do
terms[i] = generate_obj(split)
end
end
-- See if + was supplied and we have to potentially flatten multiple default terms and harmonize
-- default properties with override properties.
local saw_underlying_terms = false
for _, term in ipairs(terms) do
if term.underlying_terms then
saw_underlying_terms = true
break
end
end
if saw_underlying_terms then
-- Flatten any default terms, copying the corresponding override properties over the default
-- properties. Non-default terms get inserted directly.
local flattened = {}
for _, term in ipairs(terms) do
if term.underlying_terms then
for _, underlying in ipairs(term.underlying_terms) do
for k, v in pairs(term) do
if k ~= "term" and k ~= "underlying_terms" then
if k == "uncertain" then
underlying.uncertain = underlying.uncertain or v
elseif type(v) ~= "table" or v[1] then
-- Don't copy empty lists (which are the default) over possibly non-empty
-- lists.
underlying[k] = v
end
end
end
insert(flattened, underlying)
end
else
insert(flattened, term)
end
end
terms = flattened
end
if not slot_is_headword then
terms.label = label
end
return terms, slot
elseif not alternant_multiword_spec then
return nil, slot
else
if not slotval then
if slot_is_headword then
-- FIXME, put "uncertain" as qualifier? Does this ever happen?
return nil, slot
elseif alternant_multiword_spec.slot_uncertain[slot] then
return {label = label .. " uncertain"}, slot
elseif alternant_multiword_spec.slot_explicitly_missing[slot] then
return {label = "no " .. label}, slot
else
-- just say nothing about this slot
return nil, slot
end
end
local terms = get_slot_values()
if not slot_is_headword then
terms.label = label
end
return terms, slot
end
end
local gloss_parts = {}
for _, vform in ipairs(alternant_multiword_spec.verb_forms) do
insert(gloss_parts, "[[Appendix:Arabic verbs#Form " .. vform .. "|" .. vform .. "]]")
end
if gloss_parts[1] then
data.gloss = concat(gloss_parts, ", ")
end
if data.heads[1] and args.past then
error("Can't specify both head= and past= to {{ar-verb}}; prefer past=")
end
if not alternant_multiword_spec.has_active then
insert(data.inflections, {label = "passive-only"})
end
-- Do this always so `past_slot` is correctly filled.
local past, past_slot = do_slot(ar_verb.potential_lemma_slots, args.past, "-", "slot is headword")
if data.heads[1] then
-- user specified head=; don't override with past= or slot 'past_3sm' etc.
else
if past then
data.heads = past
end
end
local should_do_past1s = not not args.past1s
if not should_do_past1s then
local is_form_I = false
for _, vform in ipairs(alternant_multiword_spec.verb_forms) do
if vform == "I" then
is_form_I = true
break
end
end
if is_form_I then
require(inflection_utilities_module).map_word_specs(alternant_multiword_spec, function(base)
if base.verb_form == "I" then
for _, vowel_spec in ipairs(base.conj_vowels) do
-- For form-I geminate verbs, the final vowel of the past is elided in the citation form.
-- We want to display it for all cases other than active a~u and a~i (the most common
-- cases).
if vowel_spec.weakness == "geminate" then
if ar_verb.is_passive_only(base.passive) then
should_do_past1s = true
break
end
local past_vowel = ar_verb.rget(vowel_spec.past)
local nonpast_vowel = ar_verb.rget(vowel_spec.nonpast)
if not (past_vowel == ar.A and (nonpast_vowel == ar.U or nonpast_vowel == ar.I)) then
should_do_past1s = true
break
end
end
end
-- FIXME, provide way of breaking early from map_word_specs().
end
end)
end
end
local past1s
if should_do_past1s then
past1s, _ = do_slot({"past_1s", "past_pass_1s"}, args.past1s, "first-person singular past")
if past1s then
insert(data.inflections, past1s)
end
end
local nonpast_slots
if not past_slot or past_slot:find("^past_") then
nonpast_slots = {"ind_3ms", "ind_pass_3ms", "imp_2ms"}
else
nonpast_slots = {}
end
local nonpast, _ = do_slot(nonpast_slots, args.nonpast, "non-past")
if nonpast then
insert(data.inflections, nonpast)
end
local vn, _ = do_slot({"vn"}, args.vn, "verbal noun")
if vn then
insert(data.inflections, vn)
end
-- FIXME: Should we insert categories? Conjugation also does it and is more likely to be accurate.
--for _, cat in ipairs(alternant_multiword_spec.categories) do
-- insert(data.categories, cat)
--end
--[=[
-- FIXME: Review this to see if we need to port it.
-- If the user didn't explicitly specify head=, or specified exactly one head (not 2+) and we were able to
-- incorporate any links in that head into the 1= specification, use the infinitive generated by
-- [[Module:pt-verb]] in place of the user-specified or auto-generated head. This was copied from
-- [[Module:it-headword]], where doing this gets accents marked on the verb(s). We don't have accents marked on
-- the verb but by doing this we do get any footnotes on the infinitive propagated here. Don't do this if the
-- user gave multiple heads or gave a head with a multiword-linked verbal expression such as Italian
-- '[[dare esca]] [[al]] [[fuoco]]' (FIXME: give Portuguese equivalent).
if not data.user_specified_heads[1] or (
not data.user_specified_heads[2] and alternant_multiword_spec.incorporated_headword_head_into_lemma
) then
data.heads = {}
for _, lemma_obj in ipairs(alternant_multiword_spec.forms.infinitive_linked) do
local quals, refs = require(inflection_utilities_module).
convert_footnotes_to_qualifiers_and_references(lemma_obj.footnotes)
insert(data.heads, {term = lemma_obj.form, q = quals, refs = refs})
end
end
]=]
end
}
-----------------------------------------------------------------------------------------
-- Generic parts of speech --
-----------------------------------------------------------------------------------------
pos_functions.head_with_gender = {
params = function()
return {
[3] = {type = "genders"},
}
end,
func = function(data, args)
handle_gender(data, args, "nonlemma", 3)
end,
}
return export
28pshcjtcmds9oldxhr5yf970j24nt1
375807
375804
2026-09-24T14:39:49Z
Hakimi97
2668
Menyetempatkan modul
375807
Scribunto
text/plain
-- Author: primarily Benwing2; some work by Fenakhay, Erutuon; early version by Rua
local export = {}
local pos_functions = {}
local force_cat = false -- for testing; if true, categories appear in non-mainspace pages
local ar_translit = require("Module:ar-translit")
local ar_verb_module = "Module:ar-verb"
local ar_utilities_module = "Module:ar-utilities"
local ar = require(ar_utilities_module)
local en_utilities_module = "Module:en-utilities"
local headword_module = "Module:headword"
local headword_utilities_module = "Module:headword utilities"
local links_module = "Module:links"
local inflection_utilities_module = "Module:inflection utilities"
local parse_utilities_module = "Module:parse utilities"
local require_when_needed = require("Module:utilities/require when needed")
local remove_links = require_when_needed(links_module, "remove_links")
local m_table = require("Module:table")
local m_str_utils = require("Module:string utilities")
local m_en_utilities = require_when_needed(en_utilities_module)
local m_headword_utilities = require_when_needed(headword_utilities_module)
local glossary_link = require_when_needed(headword_utilities_module, "glossary_link")
local boolean_param = {type = "boolean"}
local list_to_set = m_table.listToSet
local rfind = m_str_utils.find
local rmatch = m_str_utils.match
local rsubn = m_str_utils.gsub
local u = m_str_utils.char
local rsplit = m_str_utils.split
local insert = table.insert
local concat = table.concat
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local langcode = "ar"
local lang = require("Module:languages").getByCode(langcode)
local langname = lang:getCanonicalName()
local TEMPCOMMA = u(0xFFF0)
local TEMPARCOMMA = u(0xFFF1)
local misc_pos_with_gender = list_to_set {
"suffixes",
"adjective forms",
"noun forms",
"proper noun forms",
"pronoun forms",
"determiner forms",
}
-----------------------------------------------------------------------------------------
-- Utility functions --
-----------------------------------------------------------------------------------------
local dump = mw.dumpObject
-- version of mw.ustring.gsub() that discards all but the first return value
local function rsub(term, foo, bar)
local retval = rsubn(term, foo, bar)
return retval
end
local function ine(val)
if val == "" then return nil else return val end
end
-- Replace comma with a temporary char in comma + whitespace.
local function escape_comma_whitespace(run)
local escaped = false
if run:find("\\,") then
run = run:gsub("\\,", "\\" .. TEMPCOMMA)
escaped = true
end
if run:find("\\،") then
run = run:gsub("\\،", "\\" .. TEMPARCOMMA)
escaped = true
end
if run:find(",%s") then
run = run:gsub(",(%s)", TEMPCOMMA .. "%1")
escaped = true
end
if run:find("،%s") then
run = run:gsub("،(%s)", TEMPARCOMMA .. "%1")
escaped = true
end
return run, escaped
end
-- Undo replacement of comma with a temporary char in comma + whitespace.
local function unescape_comma_whitespace(run)
return (run:gsub(TEMPCOMMA, ","):gsub(TEMPARCOMMA, "،"))
end
-- Split an argument on comma or Arabic comma, but not either type of comma followed by whitespace.
local function split_on_comma(val)
if rfind(val, "[,،]%s") or val:find("\\") then
return export.split_escaping(val, "[,،]", false, escape_comma_whitespace, unescape_comma_whitespace)
else
return rsplit(val, "[,،]")
end
end
local function replace_tr_ending(tr, from, to)
if not tr then
return nil
end
local pref = tr:match("^(.*)" .. from .. "$")
if not pref then
error(("Translit '%s' does not end in -%s, as expected"):format(tr, from))
end
return pref .. to
end
-----------------------------------------------------------------------------------------
-- Tracking functions --
-----------------------------------------------------------------------------------------
local trackfn = require("Module:debug/track")
local function track(page)
trackfn(langcode .. "-headword/" .. page)
return true
end
--[==[
Examples of what you can find by looking at what links to the given
pages:
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized]]
all unvocalized pages
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/pl]]
all unvocalized pages where the plural is unvocalized,
whether specified using pl=, pl2=, etc.
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/head]]
all unvocalized pages where the head is unvocalized
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/head/nouns]]
all nouns excluding proper nouns, collective nouns,
singulative nouns where the head is unvocalized
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/head/proper]]
nouns all proper nouns where the head is unvocalized
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/head/not]]
proper nouns all words that are not proper nouns
where the head is unvocalized
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/adjectives]]
all adjectives where any parameter is unvocalized;
currently only works for heads,
so equivalent to .../unvocalized/head/adjectives
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized-empty-head]]
all pages with an empty head
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized-manual-translit]]
all unvocalized pages with manual translit
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized-manual-translit/head/nouns]]
all nouns where the head is unvocalized but has manual translit
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized-no-translit]]
all unvocalized pages without manual translit
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab]]
all pages with any parameter containing i3rab
of either -un, -u, -a or -i
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab-un]]
all pages with any parameter containing an -un i3rab ending
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab-un/pl]]
all pages where a form specified using pl=, pl2=, etc.
contains an -un i3rab ending
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab-u/head]]
all pages with a head containing an -u i3rab ending
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab/head/proper]]
nouns (all proper nouns with a head containing i3rab
of either -un, -u, -a or -i)
In general, the format is one of the following:
Wiktionary:Tracking/ar-headword/FIRSTLEVEL
Wiktionary:Tracking/ar-headword/FIRSTLEVEL/ARGNAME
Wiktionary:Tracking/ar-headword/FIRSTLEVEL/POS
Wiktionary:Tracking/ar-headword/FIRSTLEVEL/ARGNAME/POS
FIRSTLEVEL can be one of "unvocalized", "unvocalized-empty-head" or its
opposite "unvocalized-specified", "unvocalized-manual-translit" or its
opposite "unvocalized-no-translit", "i3rab", "i3rab-un", "i3rab-u",
"i3rab-a", or "i3rab-i".
ARGNAME is either "head" or an argument such as "pl", "f", "cons", etc.
This automatically includes arguments specified as head2=, pl3=, etc.
POS is a part of speech, lowercase and singular, e.g. "kata nama",
"Kata sifat", "Kata nama khas", "collective nouns", etc. or
"not proper noun", which includes all parts of speech but proper nouns.
]==]
local function track_form(argname, form, translit, pos)
form = ar.reorder_shadda(remove_links(form))
function dotrack(page)
track(page)
track(page .. "/" .. argname)
if pos then
track(page .. "/" .. pos)
track(page .. "/" .. argname .. "/" .. pos)
if pos ~= "Kata nama khas" then
track(page .. "/not proper noun")
track(page .. "/" .. argname .. "/not proper noun")
end
end
end
function track_i3rab(arabic, tr)
if rfind(form, arabic .. "$") then
dotrack("i3rab")
dotrack("i3rab-" .. tr)
end
end
track_i3rab(ar.UN, "un")
track_i3rab(ar.U, "u")
track_i3rab(ar.A, "a")
track_i3rab(ar.I, "i")
if form == "" or not (lang:transliterate(form)) then
dotrack("unvocalized")
if form == "" then
dotrack("unvocalized-empty-head")
else
dotrack("unvocalized-specified")
end
if translit then
dotrack("unvocalized-manual-translit")
else
dotrack("unvocalized-no-translit")
end
end
end
-----------------------------------------------------------------------------------------
-- Inflection-parsing functions --
-----------------------------------------------------------------------------------------
-- Construct the default construct state or informal form of a term in lemma format. Usually this is the same as the
-- lemma but is different for final-weak nouns and adjectives ending in -n in their lemma. NOTE: Input must be
-- shadda-reordered for this to work properly.
local function default_construct_state_or_informal(term, tr)
local pref = term:match("^(.*)" .. ar.HAMZA .. ar.IN .."$")
-- Hamza on the line with -in changes to hamza-on-yā with -ī.
if pref then
return pref .. ar.HAMZA_ON_YA .. ar.II, replace_tr_ending(tr, "in", "ī")
end
-- Otherwise just change -in to -ī.
pref = term:match("^(.*)" .. ar.IN .. "$")
if pref then
return pref .. ar.II, replace_tr_ending(tr, "in", "ī")
end
-- Change -an with alif maqṣūra to -ā with alif maqṣūra.
pref = term:match("^(.*)" .. ar.AN .. ar.AMAQ .. "$")
if pref then
return pref .. ar.AAMAQ, replace_tr_ending(tr, "an", "ā")
end
-- Change -an with tall alif (e.g. عَصًا) to -ā with tall alif.
pref = term:match("^(.*)" .. ar.AN .. ar.ALIF .. "$")
if pref then
return pref .. ar.AA, replace_tr_ending(tr, "an", "ā")
end
return term, tr
end
local function generate_construct_state_or_informal_default(data, args)
local heads = data.heads
local consobjs = {}
local different_cons = false
for _, headobj in ipairs(data.heads) do
local consterm, constr = default_construct_state_or_informal(headobj.term, headobj.tr)
different_cons = different_cons or consterm ~= headobj.term or constr ~= headobj.tr
local consobj = m_table.shallowCopy(headobj)
consobj.term = consterm
consobj.tr = constr
insert(consobjs, consobj)
end
if different_cons then
return consobjs
else
return {}
end
end
local noun_field_cons = {
field = "cons", label = "<<construct state>>", generate_default = generate_construct_state_or_informal_default,
default_when_not_explicit = function(args, data) return true end,
}
local noun_field_inf = {field = "inf", label = "informal"}
local noun_field_obl = {field = "obl", label = "<<oblique>>"}
local noun_field_def = {field = "def", label = "<<definite>> state"}
local noun_inflections = {
noun_field_cons,
noun_field_inf,
noun_field_obl,
noun_field_def,
}
local adj_field_inf = {
field = "inf", label = "informal", generate_default = generate_construct_state_or_informal_default,
default_when_not_explicit = function(args, data) return true end,
}
local adj_field_obl = noun_field_obl
local adj_field_def = noun_field_def
local adjective_inflections = {
adj_field_inf,
adj_field_obl,
adj_field_def,
}
local function has_construct_state(data)
return data.pos_category ~= "Kata sifat"
end
local function parse_nominal_inflection(paramname, val, parse_err)
return m_headword_utilities.parse_term_with_modifiers {
val = val,
paramname = paramname,
splitchar = ",",
include_mods = {"tr", "g"},
}
end
local function make_nominal_inflection_param_mod_spec(paramname)
return {convert = function(val, parse_err)
return parse_nominal_inflection(paramname, val, parse_err)
end}
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
local argpref = field
if type(argfield) == "table" then
argpref = argfield[2]
argfield = argfield[1]
end
local include_mods
if is_head then
include_mods = {"tr"}
else
include_mods = {"tr", "g"}
for _, spec in ipairs(has_construct_state(data) and noun_inflections or adjective_inflections) do
insert(include_mods, {spec.field, make_nominal_inflection_param_mod_spec(argpref .. "." .. spec.field)})
end
end
if is_head then
local retval
if args[argfield] then
retval = m_headword_utilities.parse_term_with_modifiers {
val = args[argfield],
paramname = field,
splitchar = ",",
is_head = is_head,
include_mods = include_mods,
}
end
return retval or {}
else
return m_headword_utilities.parse_term_list_with_modifiers {
forms = args[argfield],
paramname = field,
splitchar = ",",
is_head = is_head,
include_mods = include_mods,
}
end
end
local function insert_inflection(data, terms, label, accel, defgender, track_field, no_label, usually_no_label)
local track_pos = m_en_utilities.singularize(data.pos_category)
for _, termobj in ipairs(terms) do
-- If the user supplied a construct state or informal form for the term with a value of "+", substitute the
-- default value for the term. If the user supplied a value of "--", they want no value displayed. Otherwise,
-- if the user didn't supply any value, we check to see if the default construct state or informal form is
-- different from the lemma and display it if so; this applies particularly to terms in '-in' and '-an', where
-- the default construct state or informal form is almost always correct.
local field = has_construct_state(data) and "cons" or "inf"
if not termobj[field] then
local defcons, defconstr = default_construct_state_or_informal(termobj.term, termobj.tr)
if termobj.term ~= defcons or termobj.tr ~= defconstr then
-- We don't want to copy decorations from the term object because we're a subinflection of the term
-- object.
termobj[field] = {{term = defcons, tr = defconstr}}
end
elseif termobj[field][1].term == "--" then
if termobj[field][2] then
error("Can't specify more than one value for <" .. field .. ":...> if first value is '--', meaning \"don't insert anything\"")
end
termobj[field] = nil
else
for i, consobj in ipairs(termobj[field]) do
if consobj.term == "+" then
if consobj.tr then
error("Can't specify translit for default value '+'")
end
consobj.term, consobj.tr = default_construct_state_or_informal(termobj.term, termobj.tr)
elseif consobj.term == "~" then
if consobj.tr then
error("Can't specify translit for term-requesting value '~'")
end
consobj.term, consobj.tr = termobj.term, termobj.tr
end
end
end
if defgender and not termobj.genders then
termobj.genders = {{spec = defgender}}
end
local function insert_nested_inflection(field, label)
if termobj[field] then
m_headword_utilities.insert_inflection {
headdata = data,
inflobj = termobj,
terms = termobj[field],
label = label
}
end
end
for _, spec in ipairs(has_construct_state(data) and noun_inflections or adjective_inflections) do
insert_nested_inflection(spec.field, spec.label)
end
track_form(track_field, termobj.term, termobj.tr, track_pos)
end
m_headword_utilities.insert_inflection {
headdata = data,
terms = terms,
label = label,
accel = accel and {form = accel} or nil,
no_label = no_label,
usually_no_label = usually_no_label,
}
end
-----------------------------------------------------------------------------------------
-- Main entry point --
-----------------------------------------------------------------------------------------
function export.show(frame)
local iparams = {
[1] = true,
}
local iargs = require("Module:parameters").process(frame.args, iparams)
local parargs = frame:getParent().args
local poscat = iargs[1]
local pos_in_1 = not poscat
if pos_in_1 then
poscat = ine(parargs[1]) or
mw.title.getCurrentTitle().fullText == "Templat:" .. langcode .. "-head" and "interjection" or
error("Part of speech must be specified in 1=")
poscat = require(headword_module).canonicalize_pos(poscat)
end
local indexing_poscat = pos_in_1 and (misc_pos_with_gender[poscat] and "head_with_gender" or "head") or poscat
local params = {
["suffix"] = boolean_param,
["nosuffix"] = boolean_param,
["id"] = true,
["json"] = boolean_param,
["pagename"] = {}, -- for testing
}
if pos_in_1 then
params[1] = {required = true} -- required but ignored as already processed above
end
local head_is_head = pos_functions[indexing_poscat] and pos_functions[indexing_poscat].head_is_not_1
local headfield = head_is_head and "head" or pos_in_1 and 2 or 1
params[headfield] = head_is_head and true or {default = "+"}
params.head2 = {replaced_by = false, instead = "use multiple comma-separated values in |" .. headfield .. "="}
local tr_replaced_by = {replaced_by = false, instead = "use <tr:...> inline modifier on |" .. headfield .. "="}
params.tr = tr_replaced_by
params.tr2 = tr_replaced_by
if pos_functions[indexing_poscat] then
for key, val in pairs(pos_functions[indexing_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,
pos_category = poscat,
orig_pos_category = poscat,
categories = {},
heads = {},
genders = {},
inflections = {enable_auto_translit = true},
pagename = pagename,
id = args.id,
sort_key = args.sort,
force_cat_output = force_cat,
-- We expect a head always so the redundant head cat will be inaccurate.
no_redundant_head_cat = true,
}
data.heads = parse_inflection(data, args, headfield, "is_head")
for _, headobj in ipairs(data.heads) do
if headobj.term == "+" then
headobj.term = pagename
end
end
data.is_suffix = false
if args.suffix or (
not args.nosuffix and pagename:find("^%-") and poscat ~= "suffixes" and poscat ~= "suffix forms"
) then
data.is_suffix = true
data.pos_category = "suffixes"
local singular_poscat = m_en_utilities.singularize(poscat)
insert(data.categories, langname .. " " .. singular_poscat .. "-forming suffixes")
insert(data.inflections, {label = singular_poscat .. "-forming suffix"})
end
if pos_functions[indexing_poscat] then
pos_functions[indexing_poscat].func(data, args)
end
-- Do this after calling pos_functions[poscat].func() as it may modify data.heads (as verbs do).
local irreg_translit = false
for _, head in ipairs(data.heads) do
if ar_translit.irregular_translit(head.term, head.tr) then
irreg_translit = true
break
end
end
if irreg_translit then
insert(data.categories, langname .. " terms with irregular pronunciations")
end
if args.json then
return require("Module:JSON").toJSON(data)
end
return require(headword_module).full_headword(data)
end
-----------------------------------------------------------------------------------------
-- Gender handling --
-----------------------------------------------------------------------------------------
local valid_bare_genders = {false, "m", "f", "mf", "mfbysense", "mfequiv"}
local valid_bare_numbers = {false, "d", "p"}
local valid_bare_animacies = {false, "pr", "np"}
local valid_genders = {}
for _, gender in ipairs(valid_bare_genders) do
for _, number in ipairs(valid_bare_numbers) do
for _, animacy in ipairs(valid_bare_animacies) do
local parts = {}
local function ins_part(part)
if part then
insert(parts, part)
end
end
ins_part(gender)
ins_part(number)
ins_part(animacy)
local full_gender = concat(parts, "-")
valid_genders[full_gender == "" and "?" or full_gender] = true
end
end
end
local function is_masc_sg(g)
return g == "m" or g == "m-pr" or g == "m-np"
end
local function is_fem_sg(g)
return g == "f" or g == "f-pr" or g == "f-np"
end
local function is_masc_fem_sg(g)
g = g:gsub("%-pr", ""):gsub("%-np", "")
return g == "mf" or g == "mfequiv" or g == "mfbysense"
end
local function add_gender_params(params, default)
params[2] = {type = "genders", default = default or "?", template_default = "m"}
params["g2"] = {replaced_by = false, instead = "use comma-separated values in |g="}
end
-- Handle gender in params 2=, inserting into `data.genders`. Also, if a lemma, insert categories into `data.categories`
-- if the gender is unexpected for the form of the noun. (Note: If there are multiple genders,
-- [[Module:gender and number]] will automatically insert 'Arabic POS with multiple genders'.)
local function handle_gender(data, args, nonlemma, field)
if not args[field or 2] then
return
end
for _, gspec in ipairs(args[field or 2]) do
if not valid_genders[gspec.spec] then
error("Unrecognized gender: " .. gspec.spec)
end
end
data.genders = args[field or 2]
if nonlemma then
return
end
for _, gspec in ipairs(data.genders) do
local g = gspec.spec
if is_masc_sg(g) or is_fem_sg(g) or is_masc_fem_sg(g) then
local head = data.heads[1]
if head then
head = rsub(ar.reorder_shadda(remove_links(head.term)), ar.UNUOPT .. "$", "")
local ends_with_tam = rfind(head, "^[^ ]*" .. ar.TAM .. "$") or
rfind(head, "^[^ ]*" .. ar.TAM .. " ")
if (is_masc_sg(g) or is_masc_fem_sg(g)) and ends_with_tam then
insert(data.categories, langname .. " masculine terms with feminine ending")
elseif (is_fem_sg(g) or is_masc_fem_sg(g)) and not ends_with_tam and
not rfind(head, "[" .. ar.ALIF .. ar.AMAQ .. "]$") and
not rfind(head, ar.ALIF .. ar.HAMZA .. "$") then
insert(data.categories, langname .. " feminine terms lacking feminine ending")
end
end
end
end
end
-----------------------------------------------------------------------------------------
-- Inflection handlers --
-----------------------------------------------------------------------------------------
-- Add list parameters to `params` (a structure as passed to [[Module:parameters]]) for a parameter named `argpref`.
-- If `argpref` is "*", add the nominal inflection parameters for construct state, definite state, etc. Related
-- transliteration and gender parameters are no longer supported in favor of inline modifiers, and error messages are
-- output if these parameters are used.
local function add_infl_params(params, argpref)
params[argpref] = {list = true, disallow_holes = true}
params[argpref .. "tr"] = {replaced_by = false, instead = "use <tr:...> inline modifier on |" .. argpref .. "="}
params[argpref .. "g"] = {replaced_by = false, instead = "use <g:...> inline modifier on |" .. argpref .. "="}
end
--[=[
Fetch a list of inflections from the arguments in `args` based on argument `field` (e.g. "pl"). Label with `label`
(e.g. "plural"), which will appear in the headword. Insert into `data.inflections`, where `data` is the structure
passed to [[Module:headword]]. If `generate_default` is specified, it should be a function of two arguments
(`data`, `args`), which should generate the default value if no values are specified or if "+" is explicitly given.
If `generate_default` isn't specified and the user gave no values, no inflection will be inserted.
]=]
local function handle_infl(data, args, spec)
local newinfls = parse_inflection(data, args, spec.field, false)
if not newinfls[1] and spec.default_when_not_explicit and spec.default_when_not_explicit(data, args) then
newinfls = {{term = "+"}}
end
if spec.handle then
spec.handle(data, args, newinfls)
end
local default_specs = spec.allowed_defspecs
if not default_specs then
default_specs = spec.generate_default and {["+"] = true} or {}
end
local saw_defspec = false
for _, newinfl in ipairs(newinfls) do
if default_specs[newinfl.term] or newinfl.term == "~" then
saw_defspec = true
break
end
end
if saw_defspec then
local newnewinfls = {}
for _, newinfl in ipairs(newinfls) do
if default_specs[newinfl.term] then
if newinfl.tr then
error("Can't specify translit for default value '" .. newinfl.term .. "'")
end
local definfls = spec.generate_default(data, args, newinfl.term)
for _, definfl in ipairs(definfls) do
m_headword_utilities.combine_termobj_decorations(definfl, newinfl)
insert(newnewinfls, definfl)
end
elseif newinfl.term == "~" then
if newinfl.tr then
error("Can't specify translit for head-requesting value '~'")
end
for _, headobj in ipairs(data.heads) do
headobj = m_table.shallowCopy(headobj)
m_headword_utilities.combine_termobj_decorations(headobj, newinfl)
insert(newnewinfls, headobj)
end
else
insert(newnewinfls, newinfl)
end
end
newinfls = newnewinfls
end
if newinfls[1] then
if newinfls[1].term == "--" then
if newinfls[2] then
error("Can't specify more than one term if first term is '--', meaning \"don't insert anything\"")
end
else
insert_inflection(data, newinfls, spec.label, nil, spec.defgender, spec.field, spec.no_label,
spec.usually_no_label)
end
end
end
local function add_infl_list_params(params, infl_list)
for _, infl in ipairs(infl_list) do
add_infl_params(params, infl.field)
end
end
local function handle_infl_list_args(data, args, infl_list)
for _, infl in ipairs(infl_list) do
handle_infl(data, args, infl)
end
end
-----------------------------------------------------------------------------------------
-- Default ending generators --
-----------------------------------------------------------------------------------------
local function make_conditional_default(specs)
return function(data, args)
local heads = data.heads
if not heads[1] then
heads = {{term = data.pagename}}
end
local newobjs = {}
for _, headobj in ipairs(heads) do
local term = ar.reorder_shadda(headobj.term)
local tr = headobj.tr
local matched = false
for _, spec in ipairs(specs) do
local from, fromtr, to, totr = unpack(spec)
if from:find("^%^") then
pref = rmatch(term, from .. "$")
else
pref = rmatch(term, "^(.*)" .. from .. "$")
end
if pref then
term = pref .. to
tr = replace_tr_ending(tr, fromtr, totr)
matched = true
headobj = m_table.shallowCopy(headobj)
headobj.term = ar.undo_reorder_shadda(term)
headobj.tr = tr
insert(newobjs, headobj)
break
end
end
if not matched then
error(("Internal error: No matching spec: head=%s"):format(dump(headobj)))
end
end
return newobjs
end
end
local default_feminine = make_conditional_default {
{ar.AN .. ar.AMAQ, "an", ar.AAH, "āh"},
{ar.AN .. ar.ALIF, "an", ar.AAH, "āh"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_YA .. ar.IYAH, "iya"},
{ar.IN, "in", ar.IYAH, "iya"},
{"", "", ar.AH, "a"},
}
local default_masculine = make_conditional_default {
-- tall alif substitutes for alif maqṣūra after a yāʔ
{ar.Y .. ar.AAH, "āh", ar.AN .. ar.ALIF, "an"},
{ar.AAH, "āh", ar.AN .. ar.AMAQ, "an"},
-- handle the common case of final-weak feminine active participle with preceding hamza;
-- the hamza-on-yāʔ always converts back to hamza on the line when preceded by ā (alif) but
-- may not otherwise, so we just leave it alone in that case
{ar.ALIF .. ar.HAMZA_ON_YA .. ar.IYAH, "iya", ar.HAMZA .. ar.IN, "in"},
{ar.IYAH, "iya", ar.IN, "in"},
{ar.AH, "a", "", ""},
{"", "", "", ""},
}
local default_masculine_plural = make_conditional_default {
{ar.AN .. ar.AMAQ, "an", ar.AWN, "awn"},
{ar.AN .. ar.ALIF, "an", ar.AWN, "awn"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_WAW .. ar.UUN, "ūn"},
{ar.IN, "in", ar.UUN, "ūn"},
{"", "", ar.UUN, "ūn"},
}
local default_feminine_plural = make_conditional_default {
-- صَلَاة pl. صَلَوَات and أَدَاة pl. أَدَوَات and similar; but نَوَاة and وَفَاة with a و in them become نَوَيَات and وَفَيَات;
-- and longer terms like مُبَارَاة and كُمَّثْرَاة invariably form their plural in -يَات.
{"^([^و]" .. ar.A .. "[^و])" .. ar.AAH, "āh", ar.A .. ar.W .. ar.AAT, "awāt"},
{ar.AAH, "āh", ar.AYAAT, "ayāt"},
{ar.AN .. ar.AMAQ, "an", ar.AYAAT, "ayāt"},
{ar.AN .. ar.ALIF, "an", ar.AYAAT, "ayāt"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_YA .. ar.IYAAT, "iyāt"},
{ar.IN, "in", ar.IYAAT, "iyāt"},
{ar.AH, "a", ar.AAT, "āt"},
{"", "", ar.AAT, "āt"},
}
local default_masculine_dual = make_conditional_default {
{ar.AN .. ar.AMAQ, "an", ar.AYAAN, "ayān"},
{ar.AN .. ar.ALIF, "an", ar.AYAAN, "ayān"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_YA .. ar.IYAAN, "iyān"},
{ar.IN, "in", ar.IYAAN, "iyān"},
{"", "", ar.AAN, "ān"},
}
local default_feminine_dual = make_conditional_default {
{ar.AN .. ar.AMAQ, "an", ar.AATAAN, "ātān"},
{ar.AN .. ar.ALIF, "an", ar.AATAAN, "ātān"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_YA .. ar.IY .. ar.ATAAN, "iyatān"},
{ar.IN, "in", ar.IY .. ar.ATAAN, "iyatān"},
{"", "", ar.ATAAN, "atān"},
}
-- Return whether `term` is a nisba noun or adjective, ending in -iyy or -iyyah. `nisba_val` is the value of
-- args.nisba; if non-nil, it overrides any auto-determination based on the shape of the term.
local function term_is_nisba(term, nisba_val)
if nisba_val ~= nil then
return nisba_val
end
term = ar.reorder_shadda(term) -- necessary to avoid issues with e.g. أُورُوبِّيّ.
local pref = rmatch(term, "^(.*)" .. ar.IYY .. ar.UN .. "?$")
if not pref then
pref = rmatch(term, "^(.*)" .. ar.IYYAH .. ar.UN .. "?$")
end
-- Avoid false positives for words like قَوِيّ "strong" and صَبِيّ "boy". There may be other false positives
-- but this should catch most of them and will avoid very many false negatives.
return pref and not rfind(pref, "^[^ا]" .. ar.A .. ".$")
end
-----------------------------------------------------------------------------------------
-- Adjectives --
-----------------------------------------------------------------------------------------
local function is_defaulting_adjective(data, args)
return data.orig_pos_category == "defaulting adjectives"
end
local adj_field_elative = {field = "el", label = "<<elative>>"}
local adj_inflections = {
adj_field_inf,
adj_field_obl,
adj_field_def,
{field = "f", label = "feminin", generate_default = default_feminine,
default_when_not_explicit = is_defaulting_adjective},
{field = "d", label = "maskulin duaan", generate_default = default_masculine_dual},
{field = "fd", label = "feminin duaan", generate_default = default_feminine_dual},
{field = "cpl", label = "am jamak"},
{field = "pl", label = "maskulin jamak", generate_default = default_masculine_plural,
default_when_not_explicit = is_defaulting_adjective},
{field = "fpl", label = "feminin jamak", generate_default = default_feminine_plural,
default_when_not_explicit = is_defaulting_adjective},
}
local function get_adj_params()
local params = {}
add_infl_list_params(params, adj_inflections)
add_infl_params(params, "el")
params.nisba = boolean_param
return params
end
local function handle_adj_args(data, args)
handle_infl_list_args(data, args, adj_inflections)
handle_infl(data, args, adj_field_elative)
for _, headobj in ipairs(data.heads) do
if term_is_nisba(headobj.term, args.nisba) then
insert(data.categories, "Kata sifat relatif (nisba) bahasa " .. langname)
break
end
end
end
pos_functions["Kata sifat"] = {
params = get_adj_params,
func = handle_adj_args,
}
pos_functions["defaulting adjectives"] = {
params = get_adj_params,
func = function(data, args)
data.pos_category = "Kata sifat"
handle_adj_args(data, args)
end,
}
-----------------------------------------------------------------------------------------
-- Nouns, etc. --
-----------------------------------------------------------------------------------------
local function get_masc_or_feminine_gender(data, default_type)
local saw_m, saw_f, saw_mf
for _, gender in ipairs(data.genders) do
if is_masc_sg(gender.spec) then
saw_m = true
elseif is_fem_sg(gender.spec) then
saw_f = true
elseif is_masc_fem_sg(gender.spec) then
saw_mf = true
end
end
if saw_mf or saw_m and saw_f then
error("Can't generate default for " .. default_type .. " when gender is both masculine and feminine")
elseif saw_m then
return "m"
elseif saw_f then
return "f"
else
error("Can't generate default for " .. default_type .. " when gender is not specified as " ..
"maskulin or feminin tunggal")
end
end
local function is_defaulting_noun(data, args)
return data.orig_pos_category == "defaulting nouns"
end
local noun_field_dual = {
field = "d", label = "dual",
generate_default = function(data, args)
local gender = get_masc_or_feminine_gender(data, "noun dual")
if gender == "m" then
return default_masculine_dual(data, args)
else
return default_feminine_dual(data, args)
end
end,
}
local noun_field_plural = {
field = "pl", label = "jamak",
generate_default = function(data, args, defspec)
local gender = get_masc_or_feminine_gender(data, "noun plural")
if gender == "m" then
if defspec == "+f" then
return default_feminine_plural(data, args)
else
return default_masculine_plural(data, args)
end
elseif defspec == "+f" then
error("Can't specify '+f' with feminine gender; just use '+'")
else
return default_feminine_plural(data, args)
end
end,
-- Handle the case where pl=-, indicating an uncountable noun.
handle = function(data, args, terms)
if terms[1] and terms[1] == "-" then
insert(data.categories, "Kata nama berbilang bahasa " .. langname)
if args.pauc and args.pauc[1] then
error("Can't specify paucals when pl=-")
end
end
end,
allowed_defspecs = {["+"] = true, ["+f"] = true},
default_when_not_explicit = is_defaulting_noun,
no_label = "<<uncountable>>",
usually_no_label = "usually <<uncountable>>",
}
local noun_field_paucal = {
field = "pauc", label = "<<paucal>>", generate_default = default_feminine_plural,
}
local noun_field_feminine = {
field = "f", label = "feminin", generate_default = default_feminine,
default_when_not_explicit = function(data, args)
if data.orig_pos_category ~= "defaulting nouns" then
return nil
end
local gender = get_masc_or_feminine_gender(data, "defaulting-if-masculine noun feminine")
return gender == "m"
end,
}
local noun_field_masculine = {
field = "m", label = "maskulin", generate_default = default_masculine,
default_when_not_explicit = function(data, args)
if data.orig_pos_category ~= "defaulting nouns" then
return nil
end
local gender = get_masc_or_feminine_gender(data, "defaulting-if-feminine noun masculine")
return gender == "f"
end,
}
local noun_basic_inflections = {
noun_field_cons,
noun_field_inf,
noun_field_obl,
noun_field_def,
}
local noun_shared_inflections = {
noun_field_dual,
noun_field_plural,
}
local noun_extra_inflections = {
noun_field_paucal,
noun_field_feminine,
noun_field_masculine,
}
local function get_noun_params()
local params = {}
add_gender_params(params)
add_infl_list_params(params, noun_basic_inflections)
add_infl_list_params(params, noun_shared_inflections)
add_infl_list_params(params, noun_extra_inflections)
params.nisba = boolean_param
return params
end
local function handle_noun_args(data, args)
handle_gender(data, args)
handle_infl_list_args(data, args, noun_basic_inflections)
handle_infl_list_args(data, args, noun_shared_inflections)
handle_infl_list_args(data, args, noun_extra_inflections)
for _, headobj in ipairs(data.heads) do
if term_is_nisba(headobj.term, args.nisba) then
insert(data.categories, langname .. " relative nouns (nisba)")
break
end
end
end
pos_functions["Kata nama"] = {
params = get_noun_params,
func = handle_noun_args,
}
pos_functions["defaulting nouns"] = {
params = get_noun_params,
func = function(data, args)
data.pos_category = "Kata nama"
handle_noun_args(data, args)
end,
}
local noun_field_singulative = {field = "sing", label = "<<singulative>>", defgender = "f", generate_default = default_feminine}
local noun_field_collective = {field = "coll", label = "<<collective>>", defgender = "m", generate_default = default_masculine}
local function handle_sing_coll_noun_infls(data, args, otherinfl, otherlabel, othergender)
-- Handle sing= (corresponding singulative noun) or coll= (corresponding collective noun) and their gender
handle_infl(data, args, otherinfl, otherlabel, nil, othergender)
handle_infl_list_args(data, args, sing_coll_noun_inflections)
end
local function get_singulative_collective_noun_params(defgender, otherinfl)
local params = {}
add_gender_params(params, defgender)
add_infl_list_params(params, noun_basic_inflections)
add_infl_params(params, otherinfl)
add_infl_list_params(params, noun_shared_inflections)
add_infl_params(params, "pauc")
return params
end
pos_functions["collective nouns"] = {
params = function() return get_singulative_collective_noun_params("m", "sing") end,
func = function(data, args)
data.pos_category = "Kata nama"
insert(data.categories, langname .. " collective nouns")
m_headword_utilities.insert_fixed_inflection {
headdata = data,
label = "<<collective>>",
}
handle_gender(data, args)
handle_infl_list_args(data, args, noun_basic_inflections)
handle_infl(data, args, noun_field_singulative)
handle_infl_list_args(data, args, noun_shared_inflections)
handle_infl(data, args, noun_field_paucal)
end
}
pos_functions["singulative nouns"] = {
params = function() return get_singulative_collective_noun_params("f", "coll") end,
func = function(data, args)
data.pos_category = "Kata nama"
insert(data.categories, langname .. " singulative nouns")
m_headword_utilities.insert_fixed_inflection {
headdata = data,
label = "<<singulative>>",
}
handle_gender(data, args)
handle_infl_list_args(data, args, noun_basic_inflections)
handle_infl(data, args, noun_field_collective)
handle_infl_list_args(data, args, noun_shared_inflections)
handle_infl(data, args, noun_field_paucal)
end
}
-- FIXME: Do numerals really behave almost as nouns? They vary by masc/fem.
pos_functions["numerals"] = {
params = get_noun_params,
func = function(data, args)
insert(data.categories, langname .. " cardinal numbers")
handle_noun_args(data, args)
end
}
pos_functions["Kata nama khas"] = {
params = get_noun_params,
func = handle_noun_args,
}
local function get_pronoun_params()
local params = {}
add_gender_params(params, defgender)
add_infl_list_params(params, noun_basic_inflections)
add_infl_list_params(params, noun_shared_inflections)
add_infl_params(params, "f")
return params
end
pos_functions["Kata ganti nama"] = {
params = get_pronoun_params,
func = function(data, args)
handle_gender(data, args)
handle_infl_list_args(data, args, noun_basic_inflections)
handle_infl_list_args(data, args, noun_shared_inflections)
handle_infl(data, args, noun_field_feminine)
end
}
-----------------------------------------------------------------------------------------
-- Non-lemma forms --
-----------------------------------------------------------------------------------------
local valid_forms = list_to_set(
{ "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII",
"XIII", "XIV", "XV", "Iq", "IIq", "IIIq", "IVq" })
-- FIXME: Partly duplicated in [[Module:ar-inflections]].
local function handle_conj_form(data, args)
local form = args[2]
if form then
if not valid_forms[form] then
error("Invalid verb conjugation form " .. form)
end
insert(data.inflections, { label = "[[Lampiran:Kata kerja bahasa Arab#Form " .. form .. "|form " .. form .. "]]" })
end
end
pos_functions["Bentuk kerjaan"] = {
params = function()
return {
[2] = {},
}
end,
func = function(data, args)
handle_conj_form(data, args)
end
}
local function get_participle_params()
local params = get_adj_params()
params[2] = {}
return params
end
pos_functions["active participles"] = {
params = get_participle_params,
func = function(data, args)
data.pos_category = "participles"
insert(data.categories, langname .. " active participles")
handle_conj_form(data, args)
handle_infl_list_args(data, args, adj_inflections)
end
}
pos_functions["passive participles"] = {
params = get_participle_params,
func = function(data, args)
data.pos_category = "participles"
insert(data.categories, langname .. " passive participles")
handle_conj_form(data, args)
handle_infl_list_args(data, args, adj_inflections)
end
}
-----------------------------------------------------------------------------------------
-- Verbs --
-----------------------------------------------------------------------------------------
pos_functions["Kata kerja"] = {
head_is_not_1 = true,
params = function() return {
[1] = {},
-- Comma-separated lists with possible inline modifiers
["past"] = {},
["past1s"] = {},
["nonpast"] = {},
["vn"] = {},
["noautolinktext"] = {type = "boolean"},
["noautolinkverb"] = {type = "boolean"},
} end,
func = function(data, args)
local ar_verb = require(ar_verb_module)
local alternant_multiword_spec =
args[1] ~= "-" and ar_verb.do_generate_forms(args, "ar-verb", data.pagename) or nil
local function do_slot(slots_to_check, override, label, slot_is_headword)
-- Do this even with an override so we can return the correct filled slot.
local slot, slotval
if alternant_multiword_spec then
for _, potential_slot in ipairs(slots_to_check) do
slotval = alternant_multiword_spec.forms[potential_slot]
if slotval then
slot = potential_slot
break
end
end
end
local function get_slot_values()
local terms = {}
for _, form in ipairs(slotval) do
local term = {
term = form.form,
id = form.id,
genders = form.genders,
pos = form.pos,
lit = form.lit,
}
term.tr = form.translit
if form.footnotes then
local quals, refs = require(inflection_utilities_module).
convert_footnotes_to_qualifiers_and_references(form.footnotes)
term.q = quals
term.refs = refs
end
insert(terms, term)
end
return terms
end
if override then
local override_param_mods = {
alt = {},
t = {
-- [[Module:headword]] expects the gloss in "gloss".
item_dest = "gloss",
},
gloss = {},
g = {
-- [[Module:headword]] expects the genders in "genders".
item_dest = "genders",
type = "genders",
},
pos = {},
lit = {},
id = {},
-- Qualifiers and labels
q = {
type = "qualifier",
},
qq = {
type = "qualifier",
},
l = {
type = "labels",
},
ll = {
type = "labels",
},
ref = {
-- [[Module:headword]] expects the references in "refs".
item_dest = "refs",
type = "references",
},
}
local function generate_obj(formval, parse_err)
if formval == "+" then
return {term = "+", underlying_terms = get_slot_values()}
end
local val, uncertain = formval:match("^(.*)(%?)$")
val = val or formval
uncertain = not not uncertain
local ar, translit = val:match("^(.*)//(.*)$")
if not ar then
ar = formval
end
local retval = {term = ar, uncertain = uncertain}
retval.tr = translit
end
local terms
if override:find("<") then
terms = require(parse_utilities_module).parse_inline_modifiers(override, {
paramname = paramname,
param_mods = override_param_mods,
generate_obj = generate_obj,
splitchar = "[,،]",
escape_fun = escape_comma_whitespace,
unescape_fun = unescape_comma_whitespace,
})
else
terms = split_on_comma(override)
for i, split in ipairs(terms) do
terms[i] = generate_obj(split)
end
end
-- See if + was supplied and we have to potentially flatten multiple default terms and harmonize
-- default properties with override properties.
local saw_underlying_terms = false
for _, term in ipairs(terms) do
if term.underlying_terms then
saw_underlying_terms = true
break
end
end
if saw_underlying_terms then
-- Flatten any default terms, copying the corresponding override properties over the default
-- properties. Non-default terms get inserted directly.
local flattened = {}
for _, term in ipairs(terms) do
if term.underlying_terms then
for _, underlying in ipairs(term.underlying_terms) do
for k, v in pairs(term) do
if k ~= "term" and k ~= "underlying_terms" then
if k == "uncertain" then
underlying.uncertain = underlying.uncertain or v
elseif type(v) ~= "table" or v[1] then
-- Don't copy empty lists (which are the default) over possibly non-empty
-- lists.
underlying[k] = v
end
end
end
insert(flattened, underlying)
end
else
insert(flattened, term)
end
end
terms = flattened
end
if not slot_is_headword then
terms.label = label
end
return terms, slot
elseif not alternant_multiword_spec then
return nil, slot
else
if not slotval then
if slot_is_headword then
-- FIXME, put "uncertain" as qualifier? Does this ever happen?
return nil, slot
elseif alternant_multiword_spec.slot_uncertain[slot] then
return {label = label .. " uncertain"}, slot
elseif alternant_multiword_spec.slot_explicitly_missing[slot] then
return {label = "no " .. label}, slot
else
-- just say nothing about this slot
return nil, slot
end
end
local terms = get_slot_values()
if not slot_is_headword then
terms.label = label
end
return terms, slot
end
end
local gloss_parts = {}
for _, vform in ipairs(alternant_multiword_spec.verb_forms) do
insert(gloss_parts, "[[Lampiran:Kata kerja bahasa Arab#Form " .. vform .. "|" .. vform .. "]]")
end
if gloss_parts[1] then
data.gloss = concat(gloss_parts, ", ")
end
if data.heads[1] and args.past then
error("Can't specify both head= and past= to {{ar-verb}}; prefer past=")
end
if not alternant_multiword_spec.has_active then
insert(data.inflections, {label = "passive-only"})
end
-- Do this always so `past_slot` is correctly filled.
local past, past_slot = do_slot(ar_verb.potential_lemma_slots, args.past, "-", "slot is headword")
if data.heads[1] then
-- user specified head=; don't override with past= or slot 'past_3sm' etc.
else
if past then
data.heads = past
end
end
local should_do_past1s = not not args.past1s
if not should_do_past1s then
local is_form_I = false
for _, vform in ipairs(alternant_multiword_spec.verb_forms) do
if vform == "I" then
is_form_I = true
break
end
end
if is_form_I then
require(inflection_utilities_module).map_word_specs(alternant_multiword_spec, function(base)
if base.verb_form == "I" then
for _, vowel_spec in ipairs(base.conj_vowels) do
-- For form-I geminate verbs, the final vowel of the past is elided in the citation form.
-- We want to display it for all cases other than active a~u and a~i (the most common
-- cases).
if vowel_spec.weakness == "geminate" then
if ar_verb.is_passive_only(base.passive) then
should_do_past1s = true
break
end
local past_vowel = ar_verb.rget(vowel_spec.past)
local nonpast_vowel = ar_verb.rget(vowel_spec.nonpast)
if not (past_vowel == ar.A and (nonpast_vowel == ar.U or nonpast_vowel == ar.I)) then
should_do_past1s = true
break
end
end
end
-- FIXME, provide way of breaking early from map_word_specs().
end
end)
end
end
local past1s
if should_do_past1s then
past1s, _ = do_slot({"past_1s", "past_pass_1s"}, args.past1s, "first-person singular past")
if past1s then
insert(data.inflections, past1s)
end
end
local nonpast_slots
if not past_slot or past_slot:find("^past_") then
nonpast_slots = {"ind_3ms", "ind_pass_3ms", "imp_2ms"}
else
nonpast_slots = {}
end
local nonpast, _ = do_slot(nonpast_slots, args.nonpast, "bukan lampau")
if nonpast then
insert(data.inflections, nonpast)
end
local vn, _ = do_slot({"vn"}, args.vn, "kata nama kerjaan")
if vn then
insert(data.inflections, vn)
end
-- FIXME: Should we insert categories? Conjugation also does it and is more likely to be accurate.
--for _, cat in ipairs(alternant_multiword_spec.categories) do
-- insert(data.categories, cat)
--end
--[=[
-- FIXME: Review this to see if we need to port it.
-- If the user didn't explicitly specify head=, or specified exactly one head (not 2+) and we were able to
-- incorporate any links in that head into the 1= specification, use the infinitive generated by
-- [[Module:pt-verb]] in place of the user-specified or auto-generated head. This was copied from
-- [[Module:it-headword]], where doing this gets accents marked on the verb(s). We don't have accents marked on
-- the verb but by doing this we do get any footnotes on the infinitive propagated here. Don't do this if the
-- user gave multiple heads or gave a head with a multiword-linked verbal expression such as Italian
-- '[[dare esca]] [[al]] [[fuoco]]' (FIXME: give Portuguese equivalent).
if not data.user_specified_heads[1] or (
not data.user_specified_heads[2] and alternant_multiword_spec.incorporated_headword_head_into_lemma
) then
data.heads = {}
for _, lemma_obj in ipairs(alternant_multiword_spec.forms.infinitive_linked) do
local quals, refs = require(inflection_utilities_module).
convert_footnotes_to_qualifiers_and_references(lemma_obj.footnotes)
insert(data.heads, {term = lemma_obj.form, q = quals, refs = refs})
end
end
]=]
end
}
-----------------------------------------------------------------------------------------
-- Generic parts of speech --
-----------------------------------------------------------------------------------------
pos_functions.head_with_gender = {
params = function()
return {
[3] = {type = "genders"},
}
end,
func = function(data, args)
handle_gender(data, args, "nonlemma", 3)
end,
}
return export
61uwi5qx5l7tuk0h7yoprzk92qhfef7
375810
375807
2026-09-24T14:48:07Z
Hakimi97
2668
Add pos category Kata kerja instead
375810
Scribunto
text/plain
-- Author: primarily Benwing2; some work by Fenakhay, Erutuon; early version by Rua
local export = {}
local pos_functions = {}
local force_cat = false -- for testing; if true, categories appear in non-mainspace pages
local ar_translit = require("Module:ar-translit")
local ar_verb_module = "Module:ar-verb"
local ar_utilities_module = "Module:ar-utilities"
local ar = require(ar_utilities_module)
local en_utilities_module = "Module:en-utilities"
local headword_module = "Module:headword"
local headword_utilities_module = "Module:headword utilities"
local links_module = "Module:links"
local inflection_utilities_module = "Module:inflection utilities"
local parse_utilities_module = "Module:parse utilities"
local require_when_needed = require("Module:utilities/require when needed")
local remove_links = require_when_needed(links_module, "remove_links")
local m_table = require("Module:table")
local m_str_utils = require("Module:string utilities")
local m_en_utilities = require_when_needed(en_utilities_module)
local m_headword_utilities = require_when_needed(headword_utilities_module)
local glossary_link = require_when_needed(headword_utilities_module, "glossary_link")
local boolean_param = {type = "boolean"}
local list_to_set = m_table.listToSet
local rfind = m_str_utils.find
local rmatch = m_str_utils.match
local rsubn = m_str_utils.gsub
local u = m_str_utils.char
local rsplit = m_str_utils.split
local insert = table.insert
local concat = table.concat
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local langcode = "ar"
local lang = require("Module:languages").getByCode(langcode)
local langname = lang:getCanonicalName()
local TEMPCOMMA = u(0xFFF0)
local TEMPARCOMMA = u(0xFFF1)
local misc_pos_with_gender = list_to_set {
"suffixes",
"adjective forms",
"noun forms",
"proper noun forms",
"pronoun forms",
"determiner forms",
}
-----------------------------------------------------------------------------------------
-- Utility functions --
-----------------------------------------------------------------------------------------
local dump = mw.dumpObject
-- version of mw.ustring.gsub() that discards all but the first return value
local function rsub(term, foo, bar)
local retval = rsubn(term, foo, bar)
return retval
end
local function ine(val)
if val == "" then return nil else return val end
end
-- Replace comma with a temporary char in comma + whitespace.
local function escape_comma_whitespace(run)
local escaped = false
if run:find("\\,") then
run = run:gsub("\\,", "\\" .. TEMPCOMMA)
escaped = true
end
if run:find("\\،") then
run = run:gsub("\\،", "\\" .. TEMPARCOMMA)
escaped = true
end
if run:find(",%s") then
run = run:gsub(",(%s)", TEMPCOMMA .. "%1")
escaped = true
end
if run:find("،%s") then
run = run:gsub("،(%s)", TEMPARCOMMA .. "%1")
escaped = true
end
return run, escaped
end
-- Undo replacement of comma with a temporary char in comma + whitespace.
local function unescape_comma_whitespace(run)
return (run:gsub(TEMPCOMMA, ","):gsub(TEMPARCOMMA, "،"))
end
-- Split an argument on comma or Arabic comma, but not either type of comma followed by whitespace.
local function split_on_comma(val)
if rfind(val, "[,،]%s") or val:find("\\") then
return export.split_escaping(val, "[,،]", false, escape_comma_whitespace, unescape_comma_whitespace)
else
return rsplit(val, "[,،]")
end
end
local function replace_tr_ending(tr, from, to)
if not tr then
return nil
end
local pref = tr:match("^(.*)" .. from .. "$")
if not pref then
error(("Translit '%s' does not end in -%s, as expected"):format(tr, from))
end
return pref .. to
end
-----------------------------------------------------------------------------------------
-- Tracking functions --
-----------------------------------------------------------------------------------------
local trackfn = require("Module:debug/track")
local function track(page)
trackfn(langcode .. "-headword/" .. page)
return true
end
--[==[
Examples of what you can find by looking at what links to the given
pages:
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized]]
all unvocalized pages
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/pl]]
all unvocalized pages where the plural is unvocalized,
whether specified using pl=, pl2=, etc.
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/head]]
all unvocalized pages where the head is unvocalized
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/head/nouns]]
all nouns excluding proper nouns, collective nouns,
singulative nouns where the head is unvocalized
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/head/proper]]
nouns all proper nouns where the head is unvocalized
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/head/not]]
proper nouns all words that are not proper nouns
where the head is unvocalized
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized/adjectives]]
all adjectives where any parameter is unvocalized;
currently only works for heads,
so equivalent to .../unvocalized/head/adjectives
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized-empty-head]]
all pages with an empty head
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized-manual-translit]]
all unvocalized pages with manual translit
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized-manual-translit/head/nouns]]
all nouns where the head is unvocalized but has manual translit
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/unvocalized-no-translit]]
all unvocalized pages without manual translit
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab]]
all pages with any parameter containing i3rab
of either -un, -u, -a or -i
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab-un]]
all pages with any parameter containing an -un i3rab ending
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab-un/pl]]
all pages where a form specified using pl=, pl2=, etc.
contains an -un i3rab ending
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab-u/head]]
all pages with a head containing an -u i3rab ending
[[Special:WhatLinksHere/Wiktionary:Tracking/ar-headword/i3rab/head/proper]]
nouns (all proper nouns with a head containing i3rab
of either -un, -u, -a or -i)
In general, the format is one of the following:
Wiktionary:Tracking/ar-headword/FIRSTLEVEL
Wiktionary:Tracking/ar-headword/FIRSTLEVEL/ARGNAME
Wiktionary:Tracking/ar-headword/FIRSTLEVEL/POS
Wiktionary:Tracking/ar-headword/FIRSTLEVEL/ARGNAME/POS
FIRSTLEVEL can be one of "unvocalized", "unvocalized-empty-head" or its
opposite "unvocalized-specified", "unvocalized-manual-translit" or its
opposite "unvocalized-no-translit", "i3rab", "i3rab-un", "i3rab-u",
"i3rab-a", or "i3rab-i".
ARGNAME is either "head" or an argument such as "pl", "f", "cons", etc.
This automatically includes arguments specified as head2=, pl3=, etc.
POS is a part of speech, lowercase and singular, e.g. "kata nama",
"Kata sifat", "Kata nama khas", "collective nouns", etc. or
"not proper noun", which includes all parts of speech but proper nouns.
]==]
local function track_form(argname, form, translit, pos)
form = ar.reorder_shadda(remove_links(form))
function dotrack(page)
track(page)
track(page .. "/" .. argname)
if pos then
track(page .. "/" .. pos)
track(page .. "/" .. argname .. "/" .. pos)
if pos ~= "Kata nama khas" then
track(page .. "/not proper noun")
track(page .. "/" .. argname .. "/not proper noun")
end
end
end
function track_i3rab(arabic, tr)
if rfind(form, arabic .. "$") then
dotrack("i3rab")
dotrack("i3rab-" .. tr)
end
end
track_i3rab(ar.UN, "un")
track_i3rab(ar.U, "u")
track_i3rab(ar.A, "a")
track_i3rab(ar.I, "i")
if form == "" or not (lang:transliterate(form)) then
dotrack("unvocalized")
if form == "" then
dotrack("unvocalized-empty-head")
else
dotrack("unvocalized-specified")
end
if translit then
dotrack("unvocalized-manual-translit")
else
dotrack("unvocalized-no-translit")
end
end
end
-----------------------------------------------------------------------------------------
-- Inflection-parsing functions --
-----------------------------------------------------------------------------------------
-- Construct the default construct state or informal form of a term in lemma format. Usually this is the same as the
-- lemma but is different for final-weak nouns and adjectives ending in -n in their lemma. NOTE: Input must be
-- shadda-reordered for this to work properly.
local function default_construct_state_or_informal(term, tr)
local pref = term:match("^(.*)" .. ar.HAMZA .. ar.IN .."$")
-- Hamza on the line with -in changes to hamza-on-yā with -ī.
if pref then
return pref .. ar.HAMZA_ON_YA .. ar.II, replace_tr_ending(tr, "in", "ī")
end
-- Otherwise just change -in to -ī.
pref = term:match("^(.*)" .. ar.IN .. "$")
if pref then
return pref .. ar.II, replace_tr_ending(tr, "in", "ī")
end
-- Change -an with alif maqṣūra to -ā with alif maqṣūra.
pref = term:match("^(.*)" .. ar.AN .. ar.AMAQ .. "$")
if pref then
return pref .. ar.AAMAQ, replace_tr_ending(tr, "an", "ā")
end
-- Change -an with tall alif (e.g. عَصًا) to -ā with tall alif.
pref = term:match("^(.*)" .. ar.AN .. ar.ALIF .. "$")
if pref then
return pref .. ar.AA, replace_tr_ending(tr, "an", "ā")
end
return term, tr
end
local function generate_construct_state_or_informal_default(data, args)
local heads = data.heads
local consobjs = {}
local different_cons = false
for _, headobj in ipairs(data.heads) do
local consterm, constr = default_construct_state_or_informal(headobj.term, headobj.tr)
different_cons = different_cons or consterm ~= headobj.term or constr ~= headobj.tr
local consobj = m_table.shallowCopy(headobj)
consobj.term = consterm
consobj.tr = constr
insert(consobjs, consobj)
end
if different_cons then
return consobjs
else
return {}
end
end
local noun_field_cons = {
field = "cons", label = "<<construct state>>", generate_default = generate_construct_state_or_informal_default,
default_when_not_explicit = function(args, data) return true end,
}
local noun_field_inf = {field = "inf", label = "informal"}
local noun_field_obl = {field = "obl", label = "<<oblique>>"}
local noun_field_def = {field = "def", label = "<<definite>> state"}
local noun_inflections = {
noun_field_cons,
noun_field_inf,
noun_field_obl,
noun_field_def,
}
local adj_field_inf = {
field = "inf", label = "informal", generate_default = generate_construct_state_or_informal_default,
default_when_not_explicit = function(args, data) return true end,
}
local adj_field_obl = noun_field_obl
local adj_field_def = noun_field_def
local adjective_inflections = {
adj_field_inf,
adj_field_obl,
adj_field_def,
}
local function has_construct_state(data)
return data.pos_category ~= "Kata sifat"
end
local function parse_nominal_inflection(paramname, val, parse_err)
return m_headword_utilities.parse_term_with_modifiers {
val = val,
paramname = paramname,
splitchar = ",",
include_mods = {"tr", "g"},
}
end
local function make_nominal_inflection_param_mod_spec(paramname)
return {convert = function(val, parse_err)
return parse_nominal_inflection(paramname, val, parse_err)
end}
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
local argpref = field
if type(argfield) == "table" then
argpref = argfield[2]
argfield = argfield[1]
end
local include_mods
if is_head then
include_mods = {"tr"}
else
include_mods = {"tr", "g"}
for _, spec in ipairs(has_construct_state(data) and noun_inflections or adjective_inflections) do
insert(include_mods, {spec.field, make_nominal_inflection_param_mod_spec(argpref .. "." .. spec.field)})
end
end
if is_head then
local retval
if args[argfield] then
retval = m_headword_utilities.parse_term_with_modifiers {
val = args[argfield],
paramname = field,
splitchar = ",",
is_head = is_head,
include_mods = include_mods,
}
end
return retval or {}
else
return m_headword_utilities.parse_term_list_with_modifiers {
forms = args[argfield],
paramname = field,
splitchar = ",",
is_head = is_head,
include_mods = include_mods,
}
end
end
local function insert_inflection(data, terms, label, accel, defgender, track_field, no_label, usually_no_label)
local track_pos = m_en_utilities.singularize(data.pos_category)
for _, termobj in ipairs(terms) do
-- If the user supplied a construct state or informal form for the term with a value of "+", substitute the
-- default value for the term. If the user supplied a value of "--", they want no value displayed. Otherwise,
-- if the user didn't supply any value, we check to see if the default construct state or informal form is
-- different from the lemma and display it if so; this applies particularly to terms in '-in' and '-an', where
-- the default construct state or informal form is almost always correct.
local field = has_construct_state(data) and "cons" or "inf"
if not termobj[field] then
local defcons, defconstr = default_construct_state_or_informal(termobj.term, termobj.tr)
if termobj.term ~= defcons or termobj.tr ~= defconstr then
-- We don't want to copy decorations from the term object because we're a subinflection of the term
-- object.
termobj[field] = {{term = defcons, tr = defconstr}}
end
elseif termobj[field][1].term == "--" then
if termobj[field][2] then
error("Can't specify more than one value for <" .. field .. ":...> if first value is '--', meaning \"don't insert anything\"")
end
termobj[field] = nil
else
for i, consobj in ipairs(termobj[field]) do
if consobj.term == "+" then
if consobj.tr then
error("Can't specify translit for default value '+'")
end
consobj.term, consobj.tr = default_construct_state_or_informal(termobj.term, termobj.tr)
elseif consobj.term == "~" then
if consobj.tr then
error("Can't specify translit for term-requesting value '~'")
end
consobj.term, consobj.tr = termobj.term, termobj.tr
end
end
end
if defgender and not termobj.genders then
termobj.genders = {{spec = defgender}}
end
local function insert_nested_inflection(field, label)
if termobj[field] then
m_headword_utilities.insert_inflection {
headdata = data,
inflobj = termobj,
terms = termobj[field],
label = label
}
end
end
for _, spec in ipairs(has_construct_state(data) and noun_inflections or adjective_inflections) do
insert_nested_inflection(spec.field, spec.label)
end
track_form(track_field, termobj.term, termobj.tr, track_pos)
end
m_headword_utilities.insert_inflection {
headdata = data,
terms = terms,
label = label,
accel = accel and {form = accel} or nil,
no_label = no_label,
usually_no_label = usually_no_label,
}
end
-----------------------------------------------------------------------------------------
-- Main entry point --
-----------------------------------------------------------------------------------------
function export.show(frame)
local iparams = {
[1] = true,
}
local iargs = require("Module:parameters").process(frame.args, iparams)
local parargs = frame:getParent().args
local poscat = iargs[1]
local pos_in_1 = not poscat
if pos_in_1 then
poscat = ine(parargs[1]) or
mw.title.getCurrentTitle().fullText == "Templat:" .. langcode .. "-head" and "interjection" or
error("Part of speech must be specified in 1=")
poscat = require(headword_module).canonicalize_pos(poscat)
end
local indexing_poscat = pos_in_1 and (misc_pos_with_gender[poscat] and "head_with_gender" or "head") or poscat
local params = {
["suffix"] = boolean_param,
["nosuffix"] = boolean_param,
["id"] = true,
["json"] = boolean_param,
["pagename"] = {}, -- for testing
}
if pos_in_1 then
params[1] = {required = true} -- required but ignored as already processed above
end
local head_is_head = pos_functions[indexing_poscat] and pos_functions[indexing_poscat].head_is_not_1
local headfield = head_is_head and "head" or pos_in_1 and 2 or 1
params[headfield] = head_is_head and true or {default = "+"}
params.head2 = {replaced_by = false, instead = "use multiple comma-separated values in |" .. headfield .. "="}
local tr_replaced_by = {replaced_by = false, instead = "use <tr:...> inline modifier on |" .. headfield .. "="}
params.tr = tr_replaced_by
params.tr2 = tr_replaced_by
if pos_functions[indexing_poscat] then
for key, val in pairs(pos_functions[indexing_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,
pos_category = poscat,
orig_pos_category = poscat,
categories = {},
heads = {},
genders = {},
inflections = {enable_auto_translit = true},
pagename = pagename,
id = args.id,
sort_key = args.sort,
force_cat_output = force_cat,
-- We expect a head always so the redundant head cat will be inaccurate.
no_redundant_head_cat = true,
}
data.heads = parse_inflection(data, args, headfield, "is_head")
for _, headobj in ipairs(data.heads) do
if headobj.term == "+" then
headobj.term = pagename
end
end
data.is_suffix = false
if args.suffix or (
not args.nosuffix and pagename:find("^%-") and poscat ~= "suffixes" and poscat ~= "suffix forms"
) then
data.is_suffix = true
data.pos_category = "suffixes"
local singular_poscat = m_en_utilities.singularize(poscat)
insert(data.categories, langname .. " " .. singular_poscat .. "-forming suffixes")
insert(data.inflections, {label = singular_poscat .. "-forming suffix"})
end
if pos_functions[indexing_poscat] then
pos_functions[indexing_poscat].func(data, args)
end
-- Do this after calling pos_functions[poscat].func() as it may modify data.heads (as verbs do).
local irreg_translit = false
for _, head in ipairs(data.heads) do
if ar_translit.irregular_translit(head.term, head.tr) then
irreg_translit = true
break
end
end
if irreg_translit then
insert(data.categories, langname .. " terms with irregular pronunciations")
end
if args.json then
return require("Module:JSON").toJSON(data)
end
return require(headword_module).full_headword(data)
end
-----------------------------------------------------------------------------------------
-- Gender handling --
-----------------------------------------------------------------------------------------
local valid_bare_genders = {false, "m", "f", "mf", "mfbysense", "mfequiv"}
local valid_bare_numbers = {false, "d", "p"}
local valid_bare_animacies = {false, "pr", "np"}
local valid_genders = {}
for _, gender in ipairs(valid_bare_genders) do
for _, number in ipairs(valid_bare_numbers) do
for _, animacy in ipairs(valid_bare_animacies) do
local parts = {}
local function ins_part(part)
if part then
insert(parts, part)
end
end
ins_part(gender)
ins_part(number)
ins_part(animacy)
local full_gender = concat(parts, "-")
valid_genders[full_gender == "" and "?" or full_gender] = true
end
end
end
local function is_masc_sg(g)
return g == "m" or g == "m-pr" or g == "m-np"
end
local function is_fem_sg(g)
return g == "f" or g == "f-pr" or g == "f-np"
end
local function is_masc_fem_sg(g)
g = g:gsub("%-pr", ""):gsub("%-np", "")
return g == "mf" or g == "mfequiv" or g == "mfbysense"
end
local function add_gender_params(params, default)
params[2] = {type = "genders", default = default or "?", template_default = "m"}
params["g2"] = {replaced_by = false, instead = "use comma-separated values in |g="}
end
-- Handle gender in params 2=, inserting into `data.genders`. Also, if a lemma, insert categories into `data.categories`
-- if the gender is unexpected for the form of the noun. (Note: If there are multiple genders,
-- [[Module:gender and number]] will automatically insert 'Arabic POS with multiple genders'.)
local function handle_gender(data, args, nonlemma, field)
if not args[field or 2] then
return
end
for _, gspec in ipairs(args[field or 2]) do
if not valid_genders[gspec.spec] then
error("Unrecognized gender: " .. gspec.spec)
end
end
data.genders = args[field or 2]
if nonlemma then
return
end
for _, gspec in ipairs(data.genders) do
local g = gspec.spec
if is_masc_sg(g) or is_fem_sg(g) or is_masc_fem_sg(g) then
local head = data.heads[1]
if head then
head = rsub(ar.reorder_shadda(remove_links(head.term)), ar.UNUOPT .. "$", "")
local ends_with_tam = rfind(head, "^[^ ]*" .. ar.TAM .. "$") or
rfind(head, "^[^ ]*" .. ar.TAM .. " ")
if (is_masc_sg(g) or is_masc_fem_sg(g)) and ends_with_tam then
insert(data.categories, langname .. " masculine terms with feminine ending")
elseif (is_fem_sg(g) or is_masc_fem_sg(g)) and not ends_with_tam and
not rfind(head, "[" .. ar.ALIF .. ar.AMAQ .. "]$") and
not rfind(head, ar.ALIF .. ar.HAMZA .. "$") then
insert(data.categories, langname .. " feminine terms lacking feminine ending")
end
end
end
end
end
-----------------------------------------------------------------------------------------
-- Inflection handlers --
-----------------------------------------------------------------------------------------
-- Add list parameters to `params` (a structure as passed to [[Module:parameters]]) for a parameter named `argpref`.
-- If `argpref` is "*", add the nominal inflection parameters for construct state, definite state, etc. Related
-- transliteration and gender parameters are no longer supported in favor of inline modifiers, and error messages are
-- output if these parameters are used.
local function add_infl_params(params, argpref)
params[argpref] = {list = true, disallow_holes = true}
params[argpref .. "tr"] = {replaced_by = false, instead = "use <tr:...> inline modifier on |" .. argpref .. "="}
params[argpref .. "g"] = {replaced_by = false, instead = "use <g:...> inline modifier on |" .. argpref .. "="}
end
--[=[
Fetch a list of inflections from the arguments in `args` based on argument `field` (e.g. "pl"). Label with `label`
(e.g. "plural"), which will appear in the headword. Insert into `data.inflections`, where `data` is the structure
passed to [[Module:headword]]. If `generate_default` is specified, it should be a function of two arguments
(`data`, `args`), which should generate the default value if no values are specified or if "+" is explicitly given.
If `generate_default` isn't specified and the user gave no values, no inflection will be inserted.
]=]
local function handle_infl(data, args, spec)
local newinfls = parse_inflection(data, args, spec.field, false)
if not newinfls[1] and spec.default_when_not_explicit and spec.default_when_not_explicit(data, args) then
newinfls = {{term = "+"}}
end
if spec.handle then
spec.handle(data, args, newinfls)
end
local default_specs = spec.allowed_defspecs
if not default_specs then
default_specs = spec.generate_default and {["+"] = true} or {}
end
local saw_defspec = false
for _, newinfl in ipairs(newinfls) do
if default_specs[newinfl.term] or newinfl.term == "~" then
saw_defspec = true
break
end
end
if saw_defspec then
local newnewinfls = {}
for _, newinfl in ipairs(newinfls) do
if default_specs[newinfl.term] then
if newinfl.tr then
error("Can't specify translit for default value '" .. newinfl.term .. "'")
end
local definfls = spec.generate_default(data, args, newinfl.term)
for _, definfl in ipairs(definfls) do
m_headword_utilities.combine_termobj_decorations(definfl, newinfl)
insert(newnewinfls, definfl)
end
elseif newinfl.term == "~" then
if newinfl.tr then
error("Can't specify translit for head-requesting value '~'")
end
for _, headobj in ipairs(data.heads) do
headobj = m_table.shallowCopy(headobj)
m_headword_utilities.combine_termobj_decorations(headobj, newinfl)
insert(newnewinfls, headobj)
end
else
insert(newnewinfls, newinfl)
end
end
newinfls = newnewinfls
end
if newinfls[1] then
if newinfls[1].term == "--" then
if newinfls[2] then
error("Can't specify more than one term if first term is '--', meaning \"don't insert anything\"")
end
else
insert_inflection(data, newinfls, spec.label, nil, spec.defgender, spec.field, spec.no_label,
spec.usually_no_label)
end
end
end
local function add_infl_list_params(params, infl_list)
for _, infl in ipairs(infl_list) do
add_infl_params(params, infl.field)
end
end
local function handle_infl_list_args(data, args, infl_list)
for _, infl in ipairs(infl_list) do
handle_infl(data, args, infl)
end
end
-----------------------------------------------------------------------------------------
-- Default ending generators --
-----------------------------------------------------------------------------------------
local function make_conditional_default(specs)
return function(data, args)
local heads = data.heads
if not heads[1] then
heads = {{term = data.pagename}}
end
local newobjs = {}
for _, headobj in ipairs(heads) do
local term = ar.reorder_shadda(headobj.term)
local tr = headobj.tr
local matched = false
for _, spec in ipairs(specs) do
local from, fromtr, to, totr = unpack(spec)
if from:find("^%^") then
pref = rmatch(term, from .. "$")
else
pref = rmatch(term, "^(.*)" .. from .. "$")
end
if pref then
term = pref .. to
tr = replace_tr_ending(tr, fromtr, totr)
matched = true
headobj = m_table.shallowCopy(headobj)
headobj.term = ar.undo_reorder_shadda(term)
headobj.tr = tr
insert(newobjs, headobj)
break
end
end
if not matched then
error(("Internal error: No matching spec: head=%s"):format(dump(headobj)))
end
end
return newobjs
end
end
local default_feminine = make_conditional_default {
{ar.AN .. ar.AMAQ, "an", ar.AAH, "āh"},
{ar.AN .. ar.ALIF, "an", ar.AAH, "āh"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_YA .. ar.IYAH, "iya"},
{ar.IN, "in", ar.IYAH, "iya"},
{"", "", ar.AH, "a"},
}
local default_masculine = make_conditional_default {
-- tall alif substitutes for alif maqṣūra after a yāʔ
{ar.Y .. ar.AAH, "āh", ar.AN .. ar.ALIF, "an"},
{ar.AAH, "āh", ar.AN .. ar.AMAQ, "an"},
-- handle the common case of final-weak feminine active participle with preceding hamza;
-- the hamza-on-yāʔ always converts back to hamza on the line when preceded by ā (alif) but
-- may not otherwise, so we just leave it alone in that case
{ar.ALIF .. ar.HAMZA_ON_YA .. ar.IYAH, "iya", ar.HAMZA .. ar.IN, "in"},
{ar.IYAH, "iya", ar.IN, "in"},
{ar.AH, "a", "", ""},
{"", "", "", ""},
}
local default_masculine_plural = make_conditional_default {
{ar.AN .. ar.AMAQ, "an", ar.AWN, "awn"},
{ar.AN .. ar.ALIF, "an", ar.AWN, "awn"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_WAW .. ar.UUN, "ūn"},
{ar.IN, "in", ar.UUN, "ūn"},
{"", "", ar.UUN, "ūn"},
}
local default_feminine_plural = make_conditional_default {
-- صَلَاة pl. صَلَوَات and أَدَاة pl. أَدَوَات and similar; but نَوَاة and وَفَاة with a و in them become نَوَيَات and وَفَيَات;
-- and longer terms like مُبَارَاة and كُمَّثْرَاة invariably form their plural in -يَات.
{"^([^و]" .. ar.A .. "[^و])" .. ar.AAH, "āh", ar.A .. ar.W .. ar.AAT, "awāt"},
{ar.AAH, "āh", ar.AYAAT, "ayāt"},
{ar.AN .. ar.AMAQ, "an", ar.AYAAT, "ayāt"},
{ar.AN .. ar.ALIF, "an", ar.AYAAT, "ayāt"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_YA .. ar.IYAAT, "iyāt"},
{ar.IN, "in", ar.IYAAT, "iyāt"},
{ar.AH, "a", ar.AAT, "āt"},
{"", "", ar.AAT, "āt"},
}
local default_masculine_dual = make_conditional_default {
{ar.AN .. ar.AMAQ, "an", ar.AYAAN, "ayān"},
{ar.AN .. ar.ALIF, "an", ar.AYAAN, "ayān"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_YA .. ar.IYAAN, "iyān"},
{ar.IN, "in", ar.IYAAN, "iyān"},
{"", "", ar.AAN, "ān"},
}
local default_feminine_dual = make_conditional_default {
{ar.AN .. ar.AMAQ, "an", ar.AATAAN, "ātān"},
{ar.AN .. ar.ALIF, "an", ar.AATAAN, "ātān"}, -- e.g. مُحْيًا
{ar.HAMZA .. ar.IN, "in", ar.HAMZA_ON_YA .. ar.IY .. ar.ATAAN, "iyatān"},
{ar.IN, "in", ar.IY .. ar.ATAAN, "iyatān"},
{"", "", ar.ATAAN, "atān"},
}
-- Return whether `term` is a nisba noun or adjective, ending in -iyy or -iyyah. `nisba_val` is the value of
-- args.nisba; if non-nil, it overrides any auto-determination based on the shape of the term.
local function term_is_nisba(term, nisba_val)
if nisba_val ~= nil then
return nisba_val
end
term = ar.reorder_shadda(term) -- necessary to avoid issues with e.g. أُورُوبِّيّ.
local pref = rmatch(term, "^(.*)" .. ar.IYY .. ar.UN .. "?$")
if not pref then
pref = rmatch(term, "^(.*)" .. ar.IYYAH .. ar.UN .. "?$")
end
-- Avoid false positives for words like قَوِيّ "strong" and صَبِيّ "boy". There may be other false positives
-- but this should catch most of them and will avoid very many false negatives.
return pref and not rfind(pref, "^[^ا]" .. ar.A .. ".$")
end
-----------------------------------------------------------------------------------------
-- Adjectives --
-----------------------------------------------------------------------------------------
local function is_defaulting_adjective(data, args)
return data.orig_pos_category == "defaulting adjectives"
end
local adj_field_elative = {field = "el", label = "<<elative>>"}
local adj_inflections = {
adj_field_inf,
adj_field_obl,
adj_field_def,
{field = "f", label = "feminin", generate_default = default_feminine,
default_when_not_explicit = is_defaulting_adjective},
{field = "d", label = "maskulin duaan", generate_default = default_masculine_dual},
{field = "fd", label = "feminin duaan", generate_default = default_feminine_dual},
{field = "cpl", label = "am jamak"},
{field = "pl", label = "maskulin jamak", generate_default = default_masculine_plural,
default_when_not_explicit = is_defaulting_adjective},
{field = "fpl", label = "feminin jamak", generate_default = default_feminine_plural,
default_when_not_explicit = is_defaulting_adjective},
}
local function get_adj_params()
local params = {}
add_infl_list_params(params, adj_inflections)
add_infl_params(params, "el")
params.nisba = boolean_param
return params
end
local function handle_adj_args(data, args)
handle_infl_list_args(data, args, adj_inflections)
handle_infl(data, args, adj_field_elative)
for _, headobj in ipairs(data.heads) do
if term_is_nisba(headobj.term, args.nisba) then
insert(data.categories, "Kata sifat relatif (nisba) bahasa " .. langname)
break
end
end
end
pos_functions["Kata sifat"] = {
params = get_adj_params,
func = handle_adj_args,
}
pos_functions["defaulting adjectives"] = {
params = get_adj_params,
func = function(data, args)
data.pos_category = "Kata sifat"
handle_adj_args(data, args)
end,
}
-----------------------------------------------------------------------------------------
-- Nouns, etc. --
-----------------------------------------------------------------------------------------
local function get_masc_or_feminine_gender(data, default_type)
local saw_m, saw_f, saw_mf
for _, gender in ipairs(data.genders) do
if is_masc_sg(gender.spec) then
saw_m = true
elseif is_fem_sg(gender.spec) then
saw_f = true
elseif is_masc_fem_sg(gender.spec) then
saw_mf = true
end
end
if saw_mf or saw_m and saw_f then
error("Can't generate default for " .. default_type .. " when gender is both masculine and feminine")
elseif saw_m then
return "m"
elseif saw_f then
return "f"
else
error("Can't generate default for " .. default_type .. " when gender is not specified as " ..
"maskulin or feminin tunggal")
end
end
local function is_defaulting_noun(data, args)
return data.orig_pos_category == "defaulting nouns"
end
local noun_field_dual = {
field = "d", label = "dual",
generate_default = function(data, args)
local gender = get_masc_or_feminine_gender(data, "noun dual")
if gender == "m" then
return default_masculine_dual(data, args)
else
return default_feminine_dual(data, args)
end
end,
}
local noun_field_plural = {
field = "pl", label = "jamak",
generate_default = function(data, args, defspec)
local gender = get_masc_or_feminine_gender(data, "noun plural")
if gender == "m" then
if defspec == "+f" then
return default_feminine_plural(data, args)
else
return default_masculine_plural(data, args)
end
elseif defspec == "+f" then
error("Can't specify '+f' with feminine gender; just use '+'")
else
return default_feminine_plural(data, args)
end
end,
-- Handle the case where pl=-, indicating an uncountable noun.
handle = function(data, args, terms)
if terms[1] and terms[1] == "-" then
insert(data.categories, "Kata nama berbilang bahasa " .. langname)
if args.pauc and args.pauc[1] then
error("Can't specify paucals when pl=-")
end
end
end,
allowed_defspecs = {["+"] = true, ["+f"] = true},
default_when_not_explicit = is_defaulting_noun,
no_label = "<<uncountable>>",
usually_no_label = "usually <<uncountable>>",
}
local noun_field_paucal = {
field = "pauc", label = "<<paucal>>", generate_default = default_feminine_plural,
}
local noun_field_feminine = {
field = "f", label = "feminin", generate_default = default_feminine,
default_when_not_explicit = function(data, args)
if data.orig_pos_category ~= "defaulting nouns" then
return nil
end
local gender = get_masc_or_feminine_gender(data, "defaulting-if-masculine noun feminine")
return gender == "m"
end,
}
local noun_field_masculine = {
field = "m", label = "maskulin", generate_default = default_masculine,
default_when_not_explicit = function(data, args)
if data.orig_pos_category ~= "defaulting nouns" then
return nil
end
local gender = get_masc_or_feminine_gender(data, "defaulting-if-feminine noun masculine")
return gender == "f"
end,
}
local noun_basic_inflections = {
noun_field_cons,
noun_field_inf,
noun_field_obl,
noun_field_def,
}
local noun_shared_inflections = {
noun_field_dual,
noun_field_plural,
}
local noun_extra_inflections = {
noun_field_paucal,
noun_field_feminine,
noun_field_masculine,
}
local function get_noun_params()
local params = {}
add_gender_params(params)
add_infl_list_params(params, noun_basic_inflections)
add_infl_list_params(params, noun_shared_inflections)
add_infl_list_params(params, noun_extra_inflections)
params.nisba = boolean_param
return params
end
local function handle_noun_args(data, args)
handle_gender(data, args)
handle_infl_list_args(data, args, noun_basic_inflections)
handle_infl_list_args(data, args, noun_shared_inflections)
handle_infl_list_args(data, args, noun_extra_inflections)
for _, headobj in ipairs(data.heads) do
if term_is_nisba(headobj.term, args.nisba) then
insert(data.categories, langname .. " relative nouns (nisba)")
break
end
end
end
pos_functions["Kata nama"] = {
params = get_noun_params,
func = handle_noun_args,
}
pos_functions["defaulting nouns"] = {
params = get_noun_params,
func = function(data, args)
data.pos_category = "Kata nama"
handle_noun_args(data, args)
end,
}
local noun_field_singulative = {field = "sing", label = "<<singulative>>", defgender = "f", generate_default = default_feminine}
local noun_field_collective = {field = "coll", label = "<<collective>>", defgender = "m", generate_default = default_masculine}
local function handle_sing_coll_noun_infls(data, args, otherinfl, otherlabel, othergender)
-- Handle sing= (corresponding singulative noun) or coll= (corresponding collective noun) and their gender
handle_infl(data, args, otherinfl, otherlabel, nil, othergender)
handle_infl_list_args(data, args, sing_coll_noun_inflections)
end
local function get_singulative_collective_noun_params(defgender, otherinfl)
local params = {}
add_gender_params(params, defgender)
add_infl_list_params(params, noun_basic_inflections)
add_infl_params(params, otherinfl)
add_infl_list_params(params, noun_shared_inflections)
add_infl_params(params, "pauc")
return params
end
pos_functions["collective nouns"] = {
params = function() return get_singulative_collective_noun_params("m", "sing") end,
func = function(data, args)
data.pos_category = "Kata nama"
insert(data.categories, langname .. " collective nouns")
m_headword_utilities.insert_fixed_inflection {
headdata = data,
label = "<<collective>>",
}
handle_gender(data, args)
handle_infl_list_args(data, args, noun_basic_inflections)
handle_infl(data, args, noun_field_singulative)
handle_infl_list_args(data, args, noun_shared_inflections)
handle_infl(data, args, noun_field_paucal)
end
}
pos_functions["singulative nouns"] = {
params = function() return get_singulative_collective_noun_params("f", "coll") end,
func = function(data, args)
data.pos_category = "Kata nama"
insert(data.categories, langname .. " singulative nouns")
m_headword_utilities.insert_fixed_inflection {
headdata = data,
label = "<<singulative>>",
}
handle_gender(data, args)
handle_infl_list_args(data, args, noun_basic_inflections)
handle_infl(data, args, noun_field_collective)
handle_infl_list_args(data, args, noun_shared_inflections)
handle_infl(data, args, noun_field_paucal)
end
}
-- FIXME: Do numerals really behave almost as nouns? They vary by masc/fem.
pos_functions["numerals"] = {
params = get_noun_params,
func = function(data, args)
insert(data.categories, langname .. " cardinal numbers")
handle_noun_args(data, args)
end
}
pos_functions["Kata nama khas"] = {
params = get_noun_params,
func = handle_noun_args,
}
local function get_pronoun_params()
local params = {}
add_gender_params(params, defgender)
add_infl_list_params(params, noun_basic_inflections)
add_infl_list_params(params, noun_shared_inflections)
add_infl_params(params, "f")
return params
end
pos_functions["Kata ganti nama"] = {
params = get_pronoun_params,
func = function(data, args)
handle_gender(data, args)
handle_infl_list_args(data, args, noun_basic_inflections)
handle_infl_list_args(data, args, noun_shared_inflections)
handle_infl(data, args, noun_field_feminine)
end
}
-----------------------------------------------------------------------------------------
-- Non-lemma forms --
-----------------------------------------------------------------------------------------
local valid_forms = list_to_set(
{ "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII",
"XIII", "XIV", "XV", "Iq", "IIq", "IIIq", "IVq" })
-- FIXME: Partly duplicated in [[Module:ar-inflections]].
local function handle_conj_form(data, args)
local form = args[2]
if form then
if not valid_forms[form] then
error("Invalid verb conjugation form " .. form)
end
insert(data.inflections, { label = "[[Lampiran:Kata kerja bahasa Arab#Form " .. form .. "|form " .. form .. "]]" })
end
end
pos_functions["Bentuk kerjaan"] = {
params = function()
return {
[2] = {},
}
end,
func = function(data, args)
handle_conj_form(data, args)
end
}
local function get_participle_params()
local params = get_adj_params()
params[2] = {}
return params
end
pos_functions["active participles"] = {
params = get_participle_params,
func = function(data, args)
data.pos_category = "participles"
insert(data.categories, langname .. " active participles")
handle_conj_form(data, args)
handle_infl_list_args(data, args, adj_inflections)
end
}
pos_functions["passive participles"] = {
params = get_participle_params,
func = function(data, args)
data.pos_category = "participles"
insert(data.categories, langname .. " passive participles")
handle_conj_form(data, args)
handle_infl_list_args(data, args, adj_inflections)
end
}
-----------------------------------------------------------------------------------------
-- Verbs --
-----------------------------------------------------------------------------------------
pos_functions["verbs"] = {
head_is_not_1 = true,
params = function() return {
[1] = {},
-- Comma-separated lists with possible inline modifiers
["past"] = {},
["past1s"] = {},
["nonpast"] = {},
["vn"] = {},
["noautolinktext"] = {type = "boolean"},
["noautolinkverb"] = {type = "boolean"},
} end,
func = function(data, args)
data.pos_category = "Kata kerja"
local ar_verb = require(ar_verb_module)
local alternant_multiword_spec =
args[1] ~= "-" and ar_verb.do_generate_forms(args, "ar-verb", data.pagename) or nil
local function do_slot(slots_to_check, override, label, slot_is_headword)
-- Do this even with an override so we can return the correct filled slot.
local slot, slotval
if alternant_multiword_spec then
for _, potential_slot in ipairs(slots_to_check) do
slotval = alternant_multiword_spec.forms[potential_slot]
if slotval then
slot = potential_slot
break
end
end
end
local function get_slot_values()
local terms = {}
for _, form in ipairs(slotval) do
local term = {
term = form.form,
id = form.id,
genders = form.genders,
pos = form.pos,
lit = form.lit,
}
term.tr = form.translit
if form.footnotes then
local quals, refs = require(inflection_utilities_module).
convert_footnotes_to_qualifiers_and_references(form.footnotes)
term.q = quals
term.refs = refs
end
insert(terms, term)
end
return terms
end
if override then
local override_param_mods = {
alt = {},
t = {
-- [[Module:headword]] expects the gloss in "gloss".
item_dest = "gloss",
},
gloss = {},
g = {
-- [[Module:headword]] expects the genders in "genders".
item_dest = "genders",
type = "genders",
},
pos = {},
lit = {},
id = {},
-- Qualifiers and labels
q = {
type = "qualifier",
},
qq = {
type = "qualifier",
},
l = {
type = "labels",
},
ll = {
type = "labels",
},
ref = {
-- [[Module:headword]] expects the references in "refs".
item_dest = "refs",
type = "references",
},
}
local function generate_obj(formval, parse_err)
if formval == "+" then
return {term = "+", underlying_terms = get_slot_values()}
end
local val, uncertain = formval:match("^(.*)(%?)$")
val = val or formval
uncertain = not not uncertain
local ar, translit = val:match("^(.*)//(.*)$")
if not ar then
ar = formval
end
local retval = {term = ar, uncertain = uncertain}
retval.tr = translit
end
local terms
if override:find("<") then
terms = require(parse_utilities_module).parse_inline_modifiers(override, {
paramname = paramname,
param_mods = override_param_mods,
generate_obj = generate_obj,
splitchar = "[,،]",
escape_fun = escape_comma_whitespace,
unescape_fun = unescape_comma_whitespace,
})
else
terms = split_on_comma(override)
for i, split in ipairs(terms) do
terms[i] = generate_obj(split)
end
end
-- See if + was supplied and we have to potentially flatten multiple default terms and harmonize
-- default properties with override properties.
local saw_underlying_terms = false
for _, term in ipairs(terms) do
if term.underlying_terms then
saw_underlying_terms = true
break
end
end
if saw_underlying_terms then
-- Flatten any default terms, copying the corresponding override properties over the default
-- properties. Non-default terms get inserted directly.
local flattened = {}
for _, term in ipairs(terms) do
if term.underlying_terms then
for _, underlying in ipairs(term.underlying_terms) do
for k, v in pairs(term) do
if k ~= "term" and k ~= "underlying_terms" then
if k == "uncertain" then
underlying.uncertain = underlying.uncertain or v
elseif type(v) ~= "table" or v[1] then
-- Don't copy empty lists (which are the default) over possibly non-empty
-- lists.
underlying[k] = v
end
end
end
insert(flattened, underlying)
end
else
insert(flattened, term)
end
end
terms = flattened
end
if not slot_is_headword then
terms.label = label
end
return terms, slot
elseif not alternant_multiword_spec then
return nil, slot
else
if not slotval then
if slot_is_headword then
-- FIXME, put "uncertain" as qualifier? Does this ever happen?
return nil, slot
elseif alternant_multiword_spec.slot_uncertain[slot] then
return {label = label .. " uncertain"}, slot
elseif alternant_multiword_spec.slot_explicitly_missing[slot] then
return {label = "no " .. label}, slot
else
-- just say nothing about this slot
return nil, slot
end
end
local terms = get_slot_values()
if not slot_is_headword then
terms.label = label
end
return terms, slot
end
end
local gloss_parts = {}
for _, vform in ipairs(alternant_multiword_spec.verb_forms) do
insert(gloss_parts, "[[Lampiran:Kata kerja bahasa Arab#Form " .. vform .. "|" .. vform .. "]]")
end
if gloss_parts[1] then
data.gloss = concat(gloss_parts, ", ")
end
if data.heads[1] and args.past then
error("Can't specify both head= and past= to {{ar-verb}}; prefer past=")
end
if not alternant_multiword_spec.has_active then
insert(data.inflections, {label = "passive-only"})
end
-- Do this always so `past_slot` is correctly filled.
local past, past_slot = do_slot(ar_verb.potential_lemma_slots, args.past, "-", "slot is headword")
if data.heads[1] then
-- user specified head=; don't override with past= or slot 'past_3sm' etc.
else
if past then
data.heads = past
end
end
local should_do_past1s = not not args.past1s
if not should_do_past1s then
local is_form_I = false
for _, vform in ipairs(alternant_multiword_spec.verb_forms) do
if vform == "I" then
is_form_I = true
break
end
end
if is_form_I then
require(inflection_utilities_module).map_word_specs(alternant_multiword_spec, function(base)
if base.verb_form == "I" then
for _, vowel_spec in ipairs(base.conj_vowels) do
-- For form-I geminate verbs, the final vowel of the past is elided in the citation form.
-- We want to display it for all cases other than active a~u and a~i (the most common
-- cases).
if vowel_spec.weakness == "geminate" then
if ar_verb.is_passive_only(base.passive) then
should_do_past1s = true
break
end
local past_vowel = ar_verb.rget(vowel_spec.past)
local nonpast_vowel = ar_verb.rget(vowel_spec.nonpast)
if not (past_vowel == ar.A and (nonpast_vowel == ar.U or nonpast_vowel == ar.I)) then
should_do_past1s = true
break
end
end
end
-- FIXME, provide way of breaking early from map_word_specs().
end
end)
end
end
local past1s
if should_do_past1s then
past1s, _ = do_slot({"past_1s", "past_pass_1s"}, args.past1s, "first-person singular past")
if past1s then
insert(data.inflections, past1s)
end
end
local nonpast_slots
if not past_slot or past_slot:find("^past_") then
nonpast_slots = {"ind_3ms", "ind_pass_3ms", "imp_2ms"}
else
nonpast_slots = {}
end
local nonpast, _ = do_slot(nonpast_slots, args.nonpast, "bukan lampau")
if nonpast then
insert(data.inflections, nonpast)
end
local vn, _ = do_slot({"vn"}, args.vn, "kata nama kerjaan")
if vn then
insert(data.inflections, vn)
end
-- FIXME: Should we insert categories? Conjugation also does it and is more likely to be accurate.
--for _, cat in ipairs(alternant_multiword_spec.categories) do
-- insert(data.categories, cat)
--end
--[=[
-- FIXME: Review this to see if we need to port it.
-- If the user didn't explicitly specify head=, or specified exactly one head (not 2+) and we were able to
-- incorporate any links in that head into the 1= specification, use the infinitive generated by
-- [[Module:pt-verb]] in place of the user-specified or auto-generated head. This was copied from
-- [[Module:it-headword]], where doing this gets accents marked on the verb(s). We don't have accents marked on
-- the verb but by doing this we do get any footnotes on the infinitive propagated here. Don't do this if the
-- user gave multiple heads or gave a head with a multiword-linked verbal expression such as Italian
-- '[[dare esca]] [[al]] [[fuoco]]' (FIXME: give Portuguese equivalent).
if not data.user_specified_heads[1] or (
not data.user_specified_heads[2] and alternant_multiword_spec.incorporated_headword_head_into_lemma
) then
data.heads = {}
for _, lemma_obj in ipairs(alternant_multiword_spec.forms.infinitive_linked) do
local quals, refs = require(inflection_utilities_module).
convert_footnotes_to_qualifiers_and_references(lemma_obj.footnotes)
insert(data.heads, {term = lemma_obj.form, q = quals, refs = refs})
end
end
]=]
end
}
-----------------------------------------------------------------------------------------
-- Generic parts of speech --
-----------------------------------------------------------------------------------------
pos_functions.head_with_gender = {
params = function()
return {
[3] = {type = "genders"},
}
end,
func = function(data, args)
handle_gender(data, args, "nonlemma", 3)
end,
}
return export
5ynmuvhm40b2bdckislzdswt0ryedx0
Modul:ar-utilities
828
11152
375806
255485
2026-09-24T14:35:44Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/92705881|92705881]])
375806
Scribunto
text/plain
local export = {}
local m_utilities = require("Module:utilities")
local lang = require("Module:languages").getByCode("ar")
local sc = require("Module:scripts").getByCode("Arab")
local rfind = mw.ustring.find
local rsubn = mw.ustring.gsub
local rmatch = mw.ustring.match
local u = require("Module:string/char")
local consonants = "[بتثجحخدذرزسشصضطظعغقفلكمنهويء]"
-- version of rsubn() that discards all but the first return value
function export.rsub(term, foo, bar)
local retval = rsubn(term, foo, bar)
return retval
end
local rsub = export.rsub
-- synthesize a frame so that exported functions meant to be called from
-- templates can be called from the debug console.
function export.debug_frame(parargs, args)
return { args = args, getParent = function() return { args = parargs } end }
end
function export.catfix()
return m_utilities.catfix(lang, sc)
end
--------------------------- diacritics, letters and combinations ------------------------------
-- hamza variants
local HAMZA = u(0x0621) -- hamza on the line (stand-alone hamza) = ء
local HAMZA_ON_ALIF = u(0x0623)
local HAMZA_ON_WAW = u(0x0624)
local HAMZA_UNDER_ALIF = u(0x0625)
local HAMZA_ON_YA = u(0x0626)
local HAMZA_PH = u(0xFFF0) -- hamza placeholder
export.HAMZA = HAMZA
export.HAMZA_ON_ALIF = HAMZA_ON_ALIF
export.HAMZA_ON_WAW = HAMZA_ON_WAW
export.HAMZA_UNDER_ALIF = HAMZA_UNDER_ALIF
export.HAMZA_ON_YA = HAMZA_ON_YA
export.HAMZA_PH = HAMZA_PH
-- 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)
-- Pattern matching any diacritics that may be on a consonant other than shadda
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 any diacritics that may be on a consonant
local DIACRITIC = SH .. "?" .. DIACRITIC_ANY_BUT_SH
export.A = A
export.AN = AN
export.U = U
export.UN = UN
export.I = I
export.IN = IN
export.SK = SK
export.SH = SH
export.DAGGER_ALIF = DAGGER_ALIF
-- Pattern matching any diacritics that may be on a consonant other than shadda
export.DIACRITIC_ANY_BUT_SH = DIACRITIC_ANY_BUT_SH
-- Pattern matching short vowels
export.AIU = AIU
-- Pattern matching any diacritics that may be on a consonant
export.DIACRITIC = DIACRITIC
-- various letters and signs
local ALIF = u(0x0627) -- ʾalif = ا
local ALIF_WASLA = u(0x0671) -- ʾalif waṣla = hamzatu l-waṣl = ٱ
local AMAQ = u(0x0649) -- ʾalif maqṣūra = ى
local AMAD = u(0x0622) -- ʾalif madda = آ
local TAM = u(0x0629) -- tāʾ marbūṭa = ة
local WAW = u(0x0648) -- wāw = و
local W = WAW
local YA = u(0x064A) -- yā = ي
local Y = YA
local T = u(0x062A) -- tāʾ = ت
local HYPHEN = u(0x0640)
local N = u(0x0646) -- nūn = ن
local LRM = u(0x200E) -- left-to-right mark
export.ALIF = ALIF
export.ALIF_WASLA = ALIF_WASLA
export.AMAQ = AMAQ
export.AMAD = AMAD
export.TAM = TAM
export.WAW = WAW
export.W = W
export.YA = YA
export.Y = Y
export.T = T
export.HYPHEN = HYPHEN
export.N = N
export.LRM = LRM
-- common combinations
local AW = A .. W -- diphthong, construct state of some final-weak nouns, 3sm past of some final-weak verbs, etc.
local AY = A .. Y -- diphthong, construct state of most final-weak nouns, 3sm past of most final-weak verbs, etc.
local IY = I .. Y -- equivalent to long ī
local UW = U .. W -- equivalent to long ū
local AA = A .. ALIF -- long ā
local AAMAQ = A .. AMAQ -- vocalized ʾalif maqṣūra
local II = IY -- long ī
local IIN = IY .. N -- short strong masculine oblique plural ending
local IINA = IIN .. A -- full strong msaculine oblique plural ending
local UU = UW -- long ū
local UUN = UU .. N -- short strong masculine nominative plural ending
local UUNA = UUN .. A -- full strong masculine nominative plural ending
local AWN = AW .. SK .. N -- short verbal ending of some final-weak verbs
local AWNA = AWN .. A -- full verbal ending of some final-weak verbs
local AYN = AY .. SK .. N -- short oblique dual ending, verbal ending of some final-weak verbs
local AYNI = AYN .. I -- full oblique dual ending
local AYNA = AYN .. A -- full verbal ending of some final-weak verbs
local AAN = AA .. N -- short nominative dual ending
local AANI = AAN .. I -- full nominative dual ending
local UNU = "[" .. UN .. U .. "]" -- matches nominative singular of strong masculine triptotes and diptotes
local UNUOPT = UNU .. "?" -- optional equivalent of UNU, for short forms
local AH = A .. TAM -- feminine ending
local AAH = AA .. TAM -- final-weak feminine ending
local AAT = AA .. T -- short strong feminine plural ending
local AATUN = AAT .. UN -- full strong nominative feminine plural ending
local IYAH = I .. Y .. AH -- ending of some final-weak feminines
local AYAAT = AY .. AAT -- final-weak plural ending
local AYAAN = AY .. AAN -- final-weak dual ending
local IYAAT = IY .. AAT -- final-weak plural ending
local IYAAN = IY .. AAN -- final-weak dual ending
local IYY = IY .. SH -- masculine nisba ending
local IYYAH = IY .. SH .. AH -- feminine nisba ending
local ATAAN = A .. T .. AAN -- feminine dual ending
local AATAAN = AAT .. AAN -- final-weak feminine dual ending
-- other possibilities (currently found in verb module):
-- AT, AYSK, AWSK, N, NA, NI, M, MA, MU, TA, TU, _I = ALIF .. I, _U = ALIF .. U
export.AW = AW
export.AY = AY
export.IY = IY
export.UW = UW
export.AA = AA
export.AAMAQ = AAMAQ
export.II = II
export.IIN = IIN
export.IINA = IINA
export.UU = UU
export.UUN = UUN
export.UUNA = UUNA
export.AWN = AWN
export.AWNA = AWNA
export.AYN = AYN
export.AYNI = AYNI
export.AYNA = AYNA
export.AAN = AAN
export.AANI = AANI
export.UNU = UNU
export.UNUOPT = UNUOPT
export.AH = AH
export.AAH = AAH
export.AAT = AAT
export.AATUN = AATUN
export.IYAH = IYAH
export.AYAAT = AYAAT
export.AYAAN = AYAAN
export.IYAAT = IYAAT
export.IYAAN = IYAAN
export.IYY = IYY
export.IYYAH = IYYAH
export.ATAAN = ATAAN
export.AATAAN = AATAAN
function export.reorder_shadda(text)
-- shadda+short-vowel (including tanwīn vowels, i.e. -an -in -un) gets
-- replaced with short-vowel+shadda during NFC normalisation, which
-- MediaWiki does for all Unicode strings; however, it makes the
-- detection process inconvenient, so undo it. (For example, the code in
-- remove_in would fail to detect the -in in مُتَرَبٍّ because the shadda
-- would come after the -in.)
text = rsub(text, "(" .. DIACRITIC_ANY_BUT_SH .. ")" .. SH, SH .. "%1")
return text
end
function export.undo_reorder_shadda(text)
return mw.ustring.toNFC(text)
end
--------------------------- hamza processing ------------------------------
local hamza_subs = {
--------------------------- handle initial hamza --------------------------
-- put initial hamza on a seat according to following vowel.
{ "^" .. HAMZA_PH .. "([" .. I .. YA .. "])", HAMZA_UNDER_ALIF .. "%1" },
{ " " .. HAMZA_PH .. "([" .. I .. YA .. "])", " " .. HAMZA_UNDER_ALIF .. "%1" },
{ "^" .. HAMZA_PH, HAMZA_ON_ALIF }, -- if no vowel, assume a
{ " " .. HAMZA_PH, " " .. HAMZA_ON_ALIF }, -- if no vowel, assume a
----------------------------- handle final hamza --------------------------
-- "final" hamza may be followed by a short vowel or tanwīn sequence
-- use a previous short vowel to get the seat
{ "(" .. AIU .. ")(" .. HAMZA_PH .. ")(" .. DIACRITIC .. "?)$",
function(v, ham, diacrit)
ham = v == I and HAMZA_ON_YA or v == U and HAMZA_ON_WAW or HAMZA_ON_ALIF
return v .. ham .. diacrit
end
},
{ "(" .. AIU .. ")(" .. HAMZA_PH .. ")(" .. DIACRITIC .. "? )",
function(v, ham, diacrit)
ham = v == I and HAMZA_ON_YA or v == U and HAMZA_ON_WAW or HAMZA_ON_ALIF
return v .. ham .. diacrit
end
},
-- else hamza is on the line
{ HAMZA_PH .. "(" .. DIACRITIC .. "?)$", HAMZA .. "%1" },
---------------------------- handle medial hamza --------------------------
-- if long vowel or diphthong precedes, we need to ignore it.
{ "([" .. AMAD .. ALIF .. WAW .. YA .. "]" .. SK .. "?)(" .. HAMZA_PH .. ")(" .. SH .. "?)([^ ])",
function(prec, ham, shad, v2)
ham = (v2 == I or v2 == YA) and HAMZA_ON_YA or
(v2 == U or v2 == WAW) and HAMZA_ON_WAW or
rfind(prec, YA) and HAMZA_ON_YA or
HAMZA
return prec .. ham .. shad .. v2
end
},
-- otherwise, seat of medial hamza relates to vowels on one or both sides.
{ "([^ ])(" .. HAMZA_PH .. ")(" .. SH .. "?)(" .. AN .. "?[^ ])",
function(v1, ham, shad, v2)
ham = (v1 == I or v2 == I or v2 == YA) and HAMZA_ON_YA or
(v1 == U or v2 == U or v2 == WAW) and HAMZA_ON_WAW or
-- special exception for the accusative ending, in words like
-- جُزْءًا (juzʾan). By the rules of Thackston pp. 281-282 a
-- hamza-on-alif should appear, but that would result in
-- two alifs in a row, which is generally forbidden.
-- According to Haywood/Nahmad pp. 114-115, after sukūn before
-- the accusative ending (including when a pronominal suffix
-- follows) hamza is written on yāʾ if the previous letter
-- is connecting, else on the line. The only examples they
-- give involve preceding non-connecting z (جُزْءًا juzʾan and
-- (جُزْءَهُ juzʾahu) and preceding diphthongs, with the only
-- connecting letter being yāʾ, where we have hamza-on-yāʾ
-- anyway by the preceding regexp. Haywood/Nahmad's rule seems
-- too complicated, and since it conflicts with Thackston,
-- we only implement the case where otherwise two alifs would
-- appear with the indefinite accusative ending.
v2 == AN .. ALIF and HAMZA or
HAMZA_ON_ALIF
return v1 .. ham .. shad .. v2
end
},
--------------------------- handle alif madda -----------------------------
{ HAMZA_ON_ALIF .. A .. "?" .. ALIF, AMAD },
----------------------- catch any remaining hamzas ------------------------
{ HAMZA_PH, HAMZA }
}
-- Seat each HAMZA_PH in `term` by the surrounding vowels. Return a list of forms, preferred first.
local function seat_standard(term)
for _, sub in ipairs(hamza_subs) do
term = rsub(term, sub[1], sub[2])
end
-- sequence of hamza-on-wāw + wāw is problematic and leads to a preferred
-- alternative with some other type of hamza, as well as the original
-- sequence; sequence of wāw + hamza-on-wāw + wāw is especially problematic
-- and leads to two different alternatives with the original sequence not
-- one of them
if rfind(term, WAW .. "ؤُو") then
return { rsub(term, WAW .. "ؤُو", WAW .. "ئُو"), rsub(term, WAW .. "ؤُو", WAW .. "ءُو") }
elseif rfind(term, YA .. "ؤُو") then
return { rsub(term, YA .. "ؤُو", YA .. "ئُو"), term }
elseif rfind(term, ALIF .. "ؤُو") then
-- Here John Mace "Arabic Verbs" is inconsistent. In past-tense parts,
-- the preferred alternative has hamza on the line, whereas in
-- non-past parts the preferred alternative has hamza-on-yāʾ even
-- though the sequence of vowels is identical. It's too complicated to
-- propagate information about tense through to here so pick one.
return { rsub(term, ALIF .. "ؤُو", ALIF .. "ئُو"), term }
-- no alternative spelling in sequence of U/A + hamza-on-wāw + U + wāw;
-- sequence of I + hamza-on-wāw + U + wāw does not occur (has
-- hamza-on-yāʾ instead)
else
return { term }
end
end
-- Seat a word-final HAMZA_PH as if it were medial: put the dropped yāʾ back (-in becomes -iy), seat, remove the yāʾ.
-- Returns {} unless `term` ends in a HAMZA_PH with a vowel.
local function seat_medial(term)
local final_vowel = rmatch(term, HAMZA_PH .. "(" .. SH .. "?[" .. A .. I .. U .. IN .. "])$")
if not final_vowel then
return {}
end
local forms = {}
for _, form in ipairs(seat_standard(rsub(term, IN .. "$", I) .. YA)) do
form = rsub(form, YA .. "$", "")
if final_vowel == IN then
form = rsub(form, I .. "$", IN)
end
table.insert(forms, form)
end
return forms
end
-- Substitute `pattern` at the end of each word (end of string or before a space, as in hamza_subs).
local function rsub_word_final(form, pattern, repl)
form = rsub(form, pattern .. "$", repl)
return rsub(form, pattern .. " ", repl .. " ")
end
-- A word-final hamza with kasra after fatḥa (not after an alif) may be written under the alif.
local UNDER_ALIF_PATTERN = "([^" .. ALIF .. HAMZA_ON_ALIF .. "]" .. A .. ")" .. HAMZA_ON_ALIF ..
"(" .. SH .. "?[" .. I .. IN .. "])"
-- Move a word-final hamza-on-alif with kasra under the alif. Returns nil if `form` has none.
local function to_under_alif(form)
local under_alif = rsub_word_final(form, UNDER_ALIF_PATTERN, "%1" .. HAMZA_UNDER_ALIF .. "%2")
if under_alif ~= form then
return under_alif
end
end
-- Two alifs can't be adjacent, so word-final أَأ becomes أَء.
local function fix_double_alif(form)
return rsub_word_final(form, HAMZA_ON_ALIF .. A .. HAMZA_ON_ALIF .. "(" .. DIACRITIC .. "?)",
HAMZA_ON_ALIF .. A .. HAMZA .. "%1")
end
local seat_hamza_opts = { incidental_final = true }
local function check_opts(opts)
for key in pairs(opts) do
if not seat_hamza_opts[key] then
error(("Unknown option to seat_hamza(): %s"):format(tostring(key)))
end
end
end
--[==[
Seat every placeholder hamza (`HAMZA_PH`) in `term`. Returns the accepted spellings, preferred first, as a list of
`{form = FORM, rule = RULE}`. RULE is one of:
* `"standard"`: seated by the surrounding vowels ([[w:Hamza]]; Thackston pp. 281-282).
* `"medial"`: a word-final hamza seated as if medial; only with `incidental_final`
([https://shamela.ws/book/7818/57 عبد العليم إبراهيم, الإملاء والترقيم]).
* `"under_alif"`: a word-final hamza with kasra after fatḥa, written under the alif (مَبْدَإٍ); see
[https://www.arabicacademy.gov.eg/ar/items/رسم_همزة_كلمة_خطأ_وما_ماثلها،_في_حالة_الجرّ Cairo Arabic Language Academy].
`opts` is an optional table with one key:
* `incidental_final`: the word-final hamza is final only because a following letter was dropped (jussive لم ينأ,
participle منئٍ). Other keys are an error.
The result always starts with a `"standard"` item and lists standard, medial and under_alif items in that order.
No form appears twice (the earlier rule keeps it), and no form ends in two adjacent alifs (أَأ is written أَء).
]==]
function export.seat_hamza(term, opts)
opts = opts or {}
check_opts(opts)
local spellings = {}
local seen = {}
local function add(form, rule)
form = fix_double_alif(form)
if not seen[form] then
seen[form] = true
table.insert(spellings, { form = form, rule = rule })
end
end
local standard = seat_standard(term)
for _, form in ipairs(standard) do
add(form, "standard")
end
if opts.incidental_final then
for _, form in ipairs(seat_medial(term)) do
add(form, "medial")
end
end
for _, form in ipairs(standard) do
local under_alif = to_under_alif(form)
if under_alif then
add(under_alif, "under_alif")
end
end
return spellings
end
----------------------------------- misc junk ---------------------------------
-- Used in {{ar-adj-in}} so that we can specify a full lemma rather than
-- requiring the user to truncate the -in ending. FIXME: Move ar-adj-in
-- into Lua.
function export.remove_in(frame)
local lemma = frame.args[1] or error("Lemma required.")
return rsub(export.reorder_shadda(lemma), IN .. "$", "")
end
-- Used in {{ar-adj-an}} so that we can specify a full lemma rather than
-- requiring the user to truncate the -an ending. FIXME: Move ar-adj-an
-- into Lua.
function export.remove_an(frame)
local lemma = frame.args[1] or error("Lemma required.")
return rsub(export.reorder_shadda(lemma), AN .. AMAQ .. "$", "")
end
-- Compare two words and find the alternation pattern (vowel changes, prefixes, suffixes etc.)
-- Still a WIP, doesn't work correctly yet.
function export.find_pattern(word1, word2)
return nil
end
function export.etymology(frame)
local text, categories = {}, {}
local linkText
local frame_params = {
[1] = { required = true },
}
local frame_args = require("Module:parameters").process(frame.args, frame_params)
local anchor = frame_args[1]
local data = {
["color adjective"] = {
anchor = "Color or defect adjectives",
text = "color adjective",
categories = { "color/defect adjectives" },
},
["defect adjective"] = {
anchor = "Color or defect adjectives",
text = "defect adjective",
categories = { "color/defect adjectives" },
},
}
local params = {
[1] = {},
["nocat"] = { type = "boolean", default = false },
["lc"] = { type = "boolean", default = false },
["nocap"] = { alias_of = "lc" },
["notext"] = { type = "boolean", default = false },
}
local args = require("Module:parameters").process(frame:getParent().args, params)
if anchor and data[anchor] then
local data = data[anchor]
anchor = data.anchor or error('The data table does not include an anchor for "' .. anchor .. '".')
linkText = data.text or error('The data table does not include link text for "' .. anchor .. '".')
if not args.lc then
linkText = rsubn(linkText, "^%a", function(a) return mw.ustring.upper(a) end)
end
if not args.notext then
table.insert(text, "[[Lampiran:Namaan bahasa Arab#" .. anchor .. "|" .. linkText .. "]]")
end
if not args.nocat then
table.insert(categories, m_utilities.format_categories(data.categories, lang))
end
else
error('The anchor "' .. tostring(anchor) .. '" is not found in the list of anchors.')
end
return table.concat(text) .. table.concat(categories)
end
return export
72iehusccvm870j50ryngj8eomd0oyp
Modul:etymology/templates
828
11467
375833
280481
2026-09-25T07:01:55Z
SNN95
2113
kemaskini
375833
Scribunto
text/plain
local export = {}
local require_when_needed = require("Module:require when needed")
local get_current_L2 = require_when_needed("Module:pages", "get_current_L2")
local get_lang_by_name = require_when_needed("Module:languages", "getByCanonicalName")
local is_content_page = require_when_needed("Module:pages", "is_content_page")
local process_params = require_when_needed("Module:parameters", "process")
local trim = mw.text.trim
local lower = mw.ustring.lower
local etymology_module = "Module:etymology"
local headword_data_module = "Module:headword/data"
local etymology_specialized_module = "Module:etymology/specialized"
local parameter_utilities_module = "Module:parameter utilities"
-- For testing
local force_cat = false
local allowed_conjs = {"and", "or", ",", "/", "~", ";"}
-- Sinitic lects (Mandarin, Cantonese, Hokkien, etc.) are full languages, but Chinese entries sit
-- under a single ==Chinese== L2, while romanization entries (pinyin, jyutping, pe̍h-ōe-jī) have lect
-- L2s, and content is often shared between them. Contact languages (Chinese-based creoles and mixed
-- languages) have their own L2s and are excluded.
local function is_sinitic(lang)
return lang:inFamily("zhx") and not lang:inFamily("qfa-cnt")
end
local content_page
local function is_content_page_cached()
if content_page == nil then
content_page = is_content_page(mw.title.getCurrentTitle())
end
return content_page
end
-- Throw an error if `lang` (the language of the entry) doesn't match
-- the L2 header that the template is invoked under.
local function check_lang_matches_L2(lang, nocat)
if nocat or not lang or lang:getCode() == "und" or (lang.hasType and lang:hasType("family")) then
return
end
local headword_data = mw.loadData(headword_data_module)
if headword_data.large_pages[headword_data.pagename] then
return
end
if not is_content_page_cached() then
return
end
local current_L2 = get_current_L2()
if not current_L2 then
return
end
local full_name = lang:getFullName()
if full_name == current_L2 then
return
end
-- Accept any Sinitic language under any Sinitic L2.
if is_sinitic(lang) then
local L2_lang = get_lang_by_name(current_L2)
if L2_lang and is_sinitic(L2_lang) then
return
end
end
local lang_desc = lang:getCode() .. " (" .. lang:getCanonicalName() .. ")"
if lang:getFullCode() ~= lang:getCode() then
lang_desc = lang_desc .. ", an etymology-only language whose full language is " ..
lang:getFullCode() .. " (" .. full_name .. ")"
end
error("Language '" .. lang_desc .. "' does not match the L2 header (" .. current_L2 .. ").")
end
local function parse_etym_args(parent_args, base_params, has_dest_lang)
local m_param_utils = require(parameter_utilities_module)
local param_mods = m_param_utils.construct_param_mods {
{group = {"link", "q", "l", "ref"}},
}
local sourcearg, termarg
if has_dest_lang then
sourcearg, termarg = 2, 3
else
sourcearg, termarg = 1, 2
end
local terms, args = m_param_utils.parse_term_with_inline_modifiers_and_separate_params {
params = base_params,
param_mods = param_mods,
raw_args = parent_args,
termarg = termarg,
track_module = "etymology",
lang = function(args)
return args[sourcearg][#args[sourcearg]]
end,
sc = "sc",
-- Don't do this, doesn't seem to make sense.
-- parse_lang_prefix = true,
make_separate_g_into_list = true,
splitchar = ",",
subitem_param_handling = "last",
}
-- If term param 3= is empty, there will be no terms in terms.terms. To facilitate further code and for
-- compatibility,, insert one. It will display as <small>[Term?]</small>.
if not terms.terms[1] then
terms.terms[1] = {
lang = args[sourcearg][#args[sourcearg]],
sc = args.sc,
}
end
return terms.terms, args
end
function export.parse_2_lang_args(parent_args, has_text, no_family)
local boolean = {type = "boolean"}
local params = {
[1] = {
required = true,
type = "language",
default = "und"
},
[2] = {
required = true,
sublist = true,
type = "language",
family = not no_family,
default = "und"
},
[3] = true,
[4] = {alias_of = "alt"},
[5] = {alias_of = "t"},
["senseid"] = true,
["nocat"] = boolean,
["sort"] = true,
["sourceconj"] = true,
["conj"] = {set = allowed_conjs, default = ","},
}
if has_text then
params["notext"] = boolean
params["nocap"] = boolean
end
local terms, args = parse_etym_args(parent_args, params, "has dest lang")
check_lang_matches_L2(args[1], args.nocat)
return terms, args
end
-- Implementation of deprecated {{etyl}}. Provided to make histories more legible.
function export.etyl(frame)
local params = {
[1] = {required = true, type = "language", default = "und"},
[2] = {type = "language", default = "en"},
["sort"] = {},
}
-- Empty language means English, but "-" means no language. Yes, confusing...
local args = frame:getParent().args
if args[2] and trim(args[2]) == "-" then
params[2] = nil
args = process_params({
[1] = args[1],
["sort"] = args.sort
}, params)
else
args = process_params(args, params)
end
check_lang_matches_L2(args[2])
return require(etymology_module).format_source {
lang = args[2],
source = args[1],
sort_key = args.sort,
force_cat = force_cat,
}
end
-- Implementation of {{derived}}/{{der}}.
function export.derived(frame)
local parent_args = frame:getParent().args
local terms, args = export.parse_2_lang_args(parent_args)
return require(etymology_module).format_derived {
lang = args[1],
sources = args[2],
terms = terms,
sort_key = args.sort,
nocat = args.nocat,
sourceconj = args.sourceconj,
conj = args.conj,
template_name = "derived",
force_cat = force_cat,
}
end
-- Implementation of {{borrowed}}/{{bor}}.
function export.borrowed(frame)
local parent_args = frame:getParent().args
local terms, args = export.parse_2_lang_args(parent_args)
return require(etymology_module).format_borrowed {
lang = args[1],
sources = args[2],
terms = terms,
sort_key = args.sort,
nocat = args.nocat,
sourceconj = args.sourceconj,
conj = args.conj,
force_cat = force_cat,
}
end
function export.inherited(frame)
local parent_args = frame:getParent().args
local terms, args = export.parse_2_lang_args(parent_args)
local sources = args[2]
if sources[2] then
-- Because this doesn't really make sense.
error("[[Template:inherited]] doesn't support multiple comma-separated sources")
end
return require(etymology_module).format_inherited {
lang = args[1],
terms = terms,
sort_key = args.sort,
nocat = args.nocat,
conj = args.conj,
force_cat = force_cat,
}
end
function export.cognate(frame)
local params = {
[1] = {
required = true,
sublist = true,
type = "language",
family = true,
default = "und"
},
[2] = true,
[3] = {alias_of = "alt"},
[4] = {alias_of = "t"},
sourceconj = true,
["conj"] = {set = allowed_conjs, default = ","},
sort = true,
}
local parent_args = frame:getParent().args
local terms, args = parse_etym_args(parent_args, params, false)
return require(etymology_module).format_cognate {
sources = args[1],
terms = terms,
sort_key = args.sort,
sourceconj = args.sourceconj,
conj = args.conj,
force_cat = force_cat,
}
end
function export.noncognate(frame)
return export.cognate(frame)
end
-- Supports various specialized types of borrowings, according to `frame.args.bortype`:
-- "learned" = {{lbor}}/{{learned borrowing}}
-- "semi-learned" = {{slbor}}/{{semi-learned borrowing}}
-- "orthographic" = {{obor}}/{{orthographic borrowing}}
-- "unadapted" = {{ubor}}/{{unadapted borrowing}}
-- "calque" = {{cal}}/{{calque}}
-- "partial-calque" = {{pcal}}/{{partial calque}}
-- "semantic-loan" = {{sl}}/{{semantic loan}}
-- "transliteration" = {{translit}}/{{transliteration}}
-- "phono-semantic-matching" = {{psm}}/{{phono-semantic matching}}
function export.specialized_borrowing(frame)
local parent_args = frame:getParent().args
local terms, args = export.parse_2_lang_args(parent_args, "has text")
local m_etymology_specialized = require(etymology_specialized_module)
return m_etymology_specialized.specialized_borrowing {
bortype = frame.args.bortype,
lang = args[1],
sources = args[2],
terms = terms,
sort_key = args.sort,
nocap = args.nocap,
notext = args.notext,
nocat = args.nocat,
sourceconj = args.sourceconj,
conj = args.conj,
senseid = args.senseid,
force_cat = force_cat,
}
end
-- Implementation of miscellaneous templates such as {{abbrev}}, {{back-formation}}, {{clipping}}, {{ellipsis}},
-- {{rebracketing}} and {{reduplication}} that have a single associated term.
function export.misc_variant(frame)
local iparams = {
["ignore-params"] = true,
text = {required = true},
oftext = true,
cat = {list = true}, -- allow and compress holes
conj = true,
}
local iargs = process_params(frame.args, iparams)
local boolean = {type = "boolean"}
local params = {
[1] = {required = true, type = "language", default = "und"},
[2] = true,
[3] = {alias_of = "alt"},
[4] = {alias_of = "t"},
nocap = boolean, -- should be processed in the template itself
notext = boolean,
nocat = boolean,
conj = {set = allowed_conjs},
sort = true,
}
-- |ignore-params= parameter to module invocation specifies
-- additional parameter names to allow in template invocation, separated by
-- commas. They must consist of ASCII letters or numbers or hyphens.
local ignore_params = iargs["ignore-params"]
if ignore_params then
ignore_params = trim(ignore_params)
if not ignore_params:match("^[%w%-,]+$") then
error("Invalid characters in |ignore-params=: " .. ignore_params:gsub("[%w%-,]+", ""))
end
for param in ignore_params:gmatch("[%w%-]+") do
if params[param] then
error("Duplicate param |" .. param
.. " in |ignore-params=: already specified in params")
end
params[param] = true
end
end
local m_param_utils = require(parameter_utilities_module)
local param_mods = m_param_utils.construct_param_mods {
{group = {"link", "q", "l", "ref"}},
}
local parent_args = frame:getParent().args
local terms, args = m_param_utils.parse_term_with_inline_modifiers_and_separate_params {
params = params,
param_mods = param_mods,
raw_args = parent_args,
termarg = 2,
track_module = "etymology",
-- Don't set lang here as we want to know whether there was a lang prefix or not.
sc = "sc",
parse_lang_prefix = true,
allow_multiple_lang_prefixes = true,
make_separate_g_into_list = true,
splitchar = ",",
subitem_param_handling = "last",
}
check_lang_matches_L2(args[1], args.nocat)
return require(etymology_module).format_misc_variant {
lang = args[1],
notext = args.notext,
text = iargs.text,
oftext = iargs.oftext,
terms = terms.terms,
sort_key = args.sort,
conj = args.conj or iargs.conj or "and",
nocat = args.nocat,
cats = iargs.cat,
force_cat = force_cat,
}
end
-- Implementation of miscellaneous templates such as {{doublet}} that can take multiple terms. Doesn't handle {{blend}}
-- or {{univerbation}}, which display + signs between elements and use compound_like in [[Module:affix/templates]].
function export.misc_variant_multiple_terms(frame)
local iparams = {
text = {required = true},
oftext = true,
cat = {list = true}, -- allow and compress holes
conj = true,
}
local iargs = process_params(frame.args, iparams)
local boolean = {type = "boolean"}
local params = {
[1] = {required = true, type = "language", template_default = "und"},
[2] = {list = true, allow_holes = true},
nocap = boolean, -- should be processed in the template itself
notext = boolean,
nocat = boolean,
conj = {set = allowed_conjs},
sort = true,
}
local m_param_utils = require(parameter_utilities_module)
local param_mods = m_param_utils.construct_param_mods {
-- We want to require an index for all params.
{default = true, require_index = true},
{group = {"link", "q", "l", "ref"}},
}
local parent_args = frame:getParent().args
local terms, args = m_param_utils.parse_list_with_inline_modifiers_and_separate_params {
params = params,
param_mods = param_mods,
raw_args = parent_args,
termarg = 2,
parse_lang_prefix = true,
allow_multiple_lang_prefixes = true,
track_module = "etymology-templates-doublet",
disallow_custom_separators = true,
-- For compatibility, we need to not skip completely unspecified items. It is common, for example, to do
-- {{suffix|lang||foo}} to generate "+ -foo".
dont_skip_items = true,
-- Don't set lang here as we want to know whether there was a lang prefix or not.
sc = "sc.default",
}
check_lang_matches_L2(args[1], args.nocat)
return require(etymology_module).format_misc_variant {
lang = args[1],
notext = args.notext,
text = iargs.text,
oftext = iargs.oftext,
terms = terms,
sort_key = args.sort,
conj = args.conj or iargs.conj or "and",
nocat = args.nocat,
cats = iargs.cat,
force_cat = force_cat,
}
end
-- Implementation of miscellaneous templates such as {{unknown}} that have no associated terms.
do
local function get_args(frame)
local boolean = {type = "boolean"}
local params = {
[1] = {required = true, type = "language", default = "und"},
["title"] = true,
["nocap"] = boolean, -- should be processed in the template itself
["notext"] = boolean,
["nocat"] = boolean,
["sort"] = true,
}
if frame.args.title2_alias then
params[2] = {alias_of = "title"}
end
local args = process_params(frame:getParent().args, params)
check_lang_matches_L2(args[1], args.nocat)
return args
end
function export.misc_variant_no_term(frame)
local args = get_args(frame)
return require(etymology_module).format_misc_variant_no_term {
lang = args[1],
notext = args.notext,
title = args.title or frame.args.text,
nocat = args.nocat,
cat = frame.args.cat,
sort_key = args.sort,
force_cat = force_cat,
}
end
-- This function works similarly to misc_variant_no_term(), but with some automatic linking to the glossary in
-- `title`.
function export.onomatopoeia(frame)
local args = get_args(frame)
local title = args.title
if title and (lower(title) == "imitatif" or lower(title) == "imitasi" or lower(title) == "tiruan" or lower(title) == "peniruan") then
title = "[[Lampiran:Glosari#imitatif|" .. title .. "]]"
end
return require(etymology_module).format_misc_variant_no_term {
lang = args[1],
notext = args.notext,
title = title or frame.args.text,
nocat = args.nocat,
cat = frame.args.cat,
sort_key = args.sort,
force_cat = force_cat,
}
end
end
return export
hgmwm2w4zt4mbfuxp8fkvs8zbfuvhru
375842
375833
2026-09-25T09:00:24Z
Hakimi97
2668
Betulkan kod, "en" ke "ms"
375842
Scribunto
text/plain
local export = {}
local require_when_needed = require("Module:require when needed")
local get_current_L2 = require_when_needed("Module:pages", "get_current_L2")
local get_lang_by_name = require_when_needed("Module:languages", "getByCanonicalName")
local is_content_page = require_when_needed("Module:pages", "is_content_page")
local process_params = require_when_needed("Module:parameters", "process")
local trim = mw.text.trim
local lower = mw.ustring.lower
local etymology_module = "Module:etymology"
local headword_data_module = "Module:headword/data"
local etymology_specialized_module = "Module:etymology/specialized"
local parameter_utilities_module = "Module:parameter utilities"
-- For testing
local force_cat = false
local allowed_conjs = {"and", "or", ",", "/", "~", ";"}
-- Sinitic lects (Mandarin, Cantonese, Hokkien, etc.) are full languages, but Chinese entries sit
-- under a single ==Chinese== L2, while romanization entries (pinyin, jyutping, pe̍h-ōe-jī) have lect
-- L2s, and content is often shared between them. Contact languages (Chinese-based creoles and mixed
-- languages) have their own L2s and are excluded.
local function is_sinitic(lang)
return lang:inFamily("zhx") and not lang:inFamily("qfa-cnt")
end
local content_page
local function is_content_page_cached()
if content_page == nil then
content_page = is_content_page(mw.title.getCurrentTitle())
end
return content_page
end
-- Throw an error if `lang` (the language of the entry) doesn't match
-- the L2 header that the template is invoked under.
local function check_lang_matches_L2(lang, nocat)
if nocat or not lang or lang:getCode() == "und" or (lang.hasType and lang:hasType("family")) then
return
end
local headword_data = mw.loadData(headword_data_module)
if headword_data.large_pages[headword_data.pagename] then
return
end
if not is_content_page_cached() then
return
end
local current_L2 = get_current_L2()
if not current_L2 then
return
end
local full_name = lang:getFullName()
if full_name == current_L2 then
return
end
-- Accept any Sinitic language under any Sinitic L2.
if is_sinitic(lang) then
local L2_lang = get_lang_by_name(current_L2)
if L2_lang and is_sinitic(L2_lang) then
return
end
end
local lang_desc = lang:getCode() .. " (" .. lang:getCanonicalName() .. ")"
if lang:getFullCode() ~= lang:getCode() then
lang_desc = lang_desc .. ", an etymology-only language whose full language is " ..
lang:getFullCode() .. " (" .. full_name .. ")"
end
error("Language '" .. lang_desc .. "' does not match the L2 header (" .. current_L2 .. ").")
end
local function parse_etym_args(parent_args, base_params, has_dest_lang)
local m_param_utils = require(parameter_utilities_module)
local param_mods = m_param_utils.construct_param_mods {
{group = {"link", "q", "l", "ref"}},
}
local sourcearg, termarg
if has_dest_lang then
sourcearg, termarg = 2, 3
else
sourcearg, termarg = 1, 2
end
local terms, args = m_param_utils.parse_term_with_inline_modifiers_and_separate_params {
params = base_params,
param_mods = param_mods,
raw_args = parent_args,
termarg = termarg,
track_module = "etymology",
lang = function(args)
return args[sourcearg][#args[sourcearg]]
end,
sc = "sc",
-- Don't do this, doesn't seem to make sense.
-- parse_lang_prefix = true,
make_separate_g_into_list = true,
splitchar = ",",
subitem_param_handling = "last",
}
-- If term param 3= is empty, there will be no terms in terms.terms. To facilitate further code and for
-- compatibility,, insert one. It will display as <small>[Term?]</small>.
if not terms.terms[1] then
terms.terms[1] = {
lang = args[sourcearg][#args[sourcearg]],
sc = args.sc,
}
end
return terms.terms, args
end
function export.parse_2_lang_args(parent_args, has_text, no_family)
local boolean = {type = "boolean"}
local params = {
[1] = {
required = true,
type = "language",
default = "und"
},
[2] = {
required = true,
sublist = true,
type = "language",
family = not no_family,
default = "und"
},
[3] = true,
[4] = {alias_of = "alt"},
[5] = {alias_of = "t"},
["senseid"] = true,
["nocat"] = boolean,
["sort"] = true,
["sourceconj"] = true,
["conj"] = {set = allowed_conjs, default = ","},
}
if has_text then
params["notext"] = boolean
params["nocap"] = boolean
end
local terms, args = parse_etym_args(parent_args, params, "has dest lang")
check_lang_matches_L2(args[1], args.nocat)
return terms, args
end
-- Implementation of deprecated {{etyl}}. Provided to make histories more legible.
function export.etyl(frame)
local params = {
[1] = {required = true, type = "language", default = "und"},
[2] = {type = "language", default = "ms"},
["sort"] = {},
}
-- Empty language means Malay, but "-" means no language. Yes, confusing...
local args = frame:getParent().args
if args[2] and trim(args[2]) == "-" then
params[2] = nil
args = process_params({
[1] = args[1],
["sort"] = args.sort
}, params)
else
args = process_params(args, params)
end
check_lang_matches_L2(args[2])
return require(etymology_module).format_source {
lang = args[2],
source = args[1],
sort_key = args.sort,
force_cat = force_cat,
}
end
-- Implementation of {{derived}}/{{der}}.
function export.derived(frame)
local parent_args = frame:getParent().args
local terms, args = export.parse_2_lang_args(parent_args)
return require(etymology_module).format_derived {
lang = args[1],
sources = args[2],
terms = terms,
sort_key = args.sort,
nocat = args.nocat,
sourceconj = args.sourceconj,
conj = args.conj,
template_name = "derived",
force_cat = force_cat,
}
end
-- Implementation of {{borrowed}}/{{bor}}.
function export.borrowed(frame)
local parent_args = frame:getParent().args
local terms, args = export.parse_2_lang_args(parent_args)
return require(etymology_module).format_borrowed {
lang = args[1],
sources = args[2],
terms = terms,
sort_key = args.sort,
nocat = args.nocat,
sourceconj = args.sourceconj,
conj = args.conj,
force_cat = force_cat,
}
end
function export.inherited(frame)
local parent_args = frame:getParent().args
local terms, args = export.parse_2_lang_args(parent_args)
local sources = args[2]
if sources[2] then
-- Because this doesn't really make sense.
error("[[Template:inherited]] doesn't support multiple comma-separated sources")
end
return require(etymology_module).format_inherited {
lang = args[1],
terms = terms,
sort_key = args.sort,
nocat = args.nocat,
conj = args.conj,
force_cat = force_cat,
}
end
function export.cognate(frame)
local params = {
[1] = {
required = true,
sublist = true,
type = "language",
family = true,
default = "und"
},
[2] = true,
[3] = {alias_of = "alt"},
[4] = {alias_of = "t"},
sourceconj = true,
["conj"] = {set = allowed_conjs, default = ","},
sort = true,
}
local parent_args = frame:getParent().args
local terms, args = parse_etym_args(parent_args, params, false)
return require(etymology_module).format_cognate {
sources = args[1],
terms = terms,
sort_key = args.sort,
sourceconj = args.sourceconj,
conj = args.conj,
force_cat = force_cat,
}
end
function export.noncognate(frame)
return export.cognate(frame)
end
-- Supports various specialized types of borrowings, according to `frame.args.bortype`:
-- "learned" = {{lbor}}/{{learned borrowing}}
-- "semi-learned" = {{slbor}}/{{semi-learned borrowing}}
-- "orthographic" = {{obor}}/{{orthographic borrowing}}
-- "unadapted" = {{ubor}}/{{unadapted borrowing}}
-- "calque" = {{cal}}/{{calque}}
-- "partial-calque" = {{pcal}}/{{partial calque}}
-- "semantic-loan" = {{sl}}/{{semantic loan}}
-- "transliteration" = {{translit}}/{{transliteration}}
-- "phono-semantic-matching" = {{psm}}/{{phono-semantic matching}}
function export.specialized_borrowing(frame)
local parent_args = frame:getParent().args
local terms, args = export.parse_2_lang_args(parent_args, "has text")
local m_etymology_specialized = require(etymology_specialized_module)
return m_etymology_specialized.specialized_borrowing {
bortype = frame.args.bortype,
lang = args[1],
sources = args[2],
terms = terms,
sort_key = args.sort,
nocap = args.nocap,
notext = args.notext,
nocat = args.nocat,
sourceconj = args.sourceconj,
conj = args.conj,
senseid = args.senseid,
force_cat = force_cat,
}
end
-- Implementation of miscellaneous templates such as {{abbrev}}, {{back-formation}}, {{clipping}}, {{ellipsis}},
-- {{rebracketing}} and {{reduplication}} that have a single associated term.
function export.misc_variant(frame)
local iparams = {
["ignore-params"] = true,
text = {required = true},
oftext = true,
cat = {list = true}, -- allow and compress holes
conj = true,
}
local iargs = process_params(frame.args, iparams)
local boolean = {type = "boolean"}
local params = {
[1] = {required = true, type = "language", default = "und"},
[2] = true,
[3] = {alias_of = "alt"},
[4] = {alias_of = "t"},
nocap = boolean, -- should be processed in the template itself
notext = boolean,
nocat = boolean,
conj = {set = allowed_conjs},
sort = true,
}
-- |ignore-params= parameter to module invocation specifies
-- additional parameter names to allow in template invocation, separated by
-- commas. They must consist of ASCII letters or numbers or hyphens.
local ignore_params = iargs["ignore-params"]
if ignore_params then
ignore_params = trim(ignore_params)
if not ignore_params:match("^[%w%-,]+$") then
error("Invalid characters in |ignore-params=: " .. ignore_params:gsub("[%w%-,]+", ""))
end
for param in ignore_params:gmatch("[%w%-]+") do
if params[param] then
error("Duplicate param |" .. param
.. " in |ignore-params=: already specified in params")
end
params[param] = true
end
end
local m_param_utils = require(parameter_utilities_module)
local param_mods = m_param_utils.construct_param_mods {
{group = {"link", "q", "l", "ref"}},
}
local parent_args = frame:getParent().args
local terms, args = m_param_utils.parse_term_with_inline_modifiers_and_separate_params {
params = params,
param_mods = param_mods,
raw_args = parent_args,
termarg = 2,
track_module = "etymology",
-- Don't set lang here as we want to know whether there was a lang prefix or not.
sc = "sc",
parse_lang_prefix = true,
allow_multiple_lang_prefixes = true,
make_separate_g_into_list = true,
splitchar = ",",
subitem_param_handling = "last",
}
check_lang_matches_L2(args[1], args.nocat)
return require(etymology_module).format_misc_variant {
lang = args[1],
notext = args.notext,
text = iargs.text,
oftext = iargs.oftext,
terms = terms.terms,
sort_key = args.sort,
conj = args.conj or iargs.conj or "and",
nocat = args.nocat,
cats = iargs.cat,
force_cat = force_cat,
}
end
-- Implementation of miscellaneous templates such as {{doublet}} that can take multiple terms. Doesn't handle {{blend}}
-- or {{univerbation}}, which display + signs between elements and use compound_like in [[Module:affix/templates]].
function export.misc_variant_multiple_terms(frame)
local iparams = {
text = {required = true},
oftext = true,
cat = {list = true}, -- allow and compress holes
conj = true,
}
local iargs = process_params(frame.args, iparams)
local boolean = {type = "boolean"}
local params = {
[1] = {required = true, type = "language", template_default = "und"},
[2] = {list = true, allow_holes = true},
nocap = boolean, -- should be processed in the template itself
notext = boolean,
nocat = boolean,
conj = {set = allowed_conjs},
sort = true,
}
local m_param_utils = require(parameter_utilities_module)
local param_mods = m_param_utils.construct_param_mods {
-- We want to require an index for all params.
{default = true, require_index = true},
{group = {"link", "q", "l", "ref"}},
}
local parent_args = frame:getParent().args
local terms, args = m_param_utils.parse_list_with_inline_modifiers_and_separate_params {
params = params,
param_mods = param_mods,
raw_args = parent_args,
termarg = 2,
parse_lang_prefix = true,
allow_multiple_lang_prefixes = true,
track_module = "etymology-templates-doublet",
disallow_custom_separators = true,
-- For compatibility, we need to not skip completely unspecified items. It is common, for example, to do
-- {{suffix|lang||foo}} to generate "+ -foo".
dont_skip_items = true,
-- Don't set lang here as we want to know whether there was a lang prefix or not.
sc = "sc.default",
}
check_lang_matches_L2(args[1], args.nocat)
return require(etymology_module).format_misc_variant {
lang = args[1],
notext = args.notext,
text = iargs.text,
oftext = iargs.oftext,
terms = terms,
sort_key = args.sort,
conj = args.conj or iargs.conj or "and",
nocat = args.nocat,
cats = iargs.cat,
force_cat = force_cat,
}
end
-- Implementation of miscellaneous templates such as {{unknown}} that have no associated terms.
do
local function get_args(frame)
local boolean = {type = "boolean"}
local params = {
[1] = {required = true, type = "language", default = "und"},
["title"] = true,
["nocap"] = boolean, -- should be processed in the template itself
["notext"] = boolean,
["nocat"] = boolean,
["sort"] = true,
}
if frame.args.title2_alias then
params[2] = {alias_of = "title"}
end
local args = process_params(frame:getParent().args, params)
check_lang_matches_L2(args[1], args.nocat)
return args
end
function export.misc_variant_no_term(frame)
local args = get_args(frame)
return require(etymology_module).format_misc_variant_no_term {
lang = args[1],
notext = args.notext,
title = args.title or frame.args.text,
nocat = args.nocat,
cat = frame.args.cat,
sort_key = args.sort,
force_cat = force_cat,
}
end
-- This function works similarly to misc_variant_no_term(), but with some automatic linking to the glossary in
-- `title`.
function export.onomatopoeia(frame)
local args = get_args(frame)
local title = args.title
if title and (lower(title) == "imitatif" or lower(title) == "imitasi" or lower(title) == "tiruan" or lower(title) == "peniruan") then
title = "[[Lampiran:Glosari#imitatif|" .. title .. "]]"
end
return require(etymology_module).format_misc_variant_no_term {
lang = args[1],
notext = args.notext,
title = title or frame.args.text,
nocat = args.nocat,
cat = frame.args.cat,
sort_key = args.sort,
force_cat = force_cat,
}
end
end
return export
m0c5dm3agnadtn51s931wca767fhlor
Templat:link
10
11494
375814
228347
2026-09-24T17:41:23Z
SNN95
2113
375814
wikitext
text/x-wiki
<includeonly><onlyinclude>{{safesubst:<noinclude/>#invoke:links/templates|l_term_t}}</onlyinclude></includeonly><!--
-->{{l|ms|perkataan}}{{documentation}}
63xqt7kftsbudsx5jgjezgw2j4s02bl
Modul:scripts/chain
828
11501
375825
282488
2026-09-25T04:07:30Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/92389001|92389001]])
375825
Scribunto
text/plain
local export = {}
local scripts = mw.loadData("Module:scripts/data")
local function get_name(code)
local name = (scripts[code] or error(code))[1]
if type(name) == "table" then
return name.default
end
return name
end
local function format(code)
local name = get_name(code)
local category = name
if not category:find("^[Tt]ulisan") then
category = "Tulisan " .. category
end
return "<code>[[:Kategori:" .. category .. "|" .. name .. " <span style=\"color:var(--wikt-palette-green-9,green);\">(" .. code .. ")</span>]]</code>"
end
local function dump(data, prefix)
if type(data) == "string" then
return format(data)
else
local result = ""
local branch = "├───"
local next_level = prefix .. "│ "
for i,val in ipairs(data) do
if i == #data then
branch = "└───"
next_level = prefix .. " "
end
if #val == 0 then
result = result .. prefix .. branch .. dump(val.name) .. "<br/>"
else
result = result .. "{{(!}} class=mw-collapsible style=border-collapse:collapse\n{{!}}"
result = result .. prefix .. branch .. dump(val.name)
result = result .. "\n{{!-}}\n{{!}}"
result = result .. dump(val, next_level)
result = result .. "\n{{!)}}\n"
end
end
return result
end
end
local function deep_sort(current)
local result = {}
local is_table = {}
for key,val in pairs(current) do
if type(key) == "number" then
table.insert(result, val)
else
is_table[key] = true
table.insert(result, key)
end
end
table.sort(result, function(a,b)
return get_name(a) < get_name(b)
end)
local i = 2
while i<#result do
while scripts[result[i-1]] == scripts[result[i]] do
table.remove(result,i)
end
i = i + 1
end
for i=1,#result do
if is_table[result[i]] then
local name = result[i]
result[i] = deep_sort(current[result[i]])
result[i].name = name
else
result[i] = {name = result[i]}
end
end
return result
end
function export.show(frame)
local children = {}
local function find_ancestors(origin,key,val)
if val.parent then
return {val.parent}
end
end
for key,val in pairs(scripts) do
local ancestors = find_ancestors(key,key,val)
if ancestors then
for _, ancestor in ipairs(ancestors) do
if ancestor ~= key then
if children[ancestor] then
table.insert(children[ancestor], key)
else
children[ancestor] = {key}
end
end
end
end
end
local function make_nested(data)
local make_nil = {}
for key,val in pairs(data) do
if type(key) == "number" then
if children[val] then
data[val] = make_nested(children[val])
table.insert(make_nil, key)
children[val] = nil
end
else
data[key] = make_nested(val)
end
end
for _,key in ipairs(make_nil) do
data[key] = nil
end
return data
end
local nested = make_nested(children)
nested = deep_sort(nested)
local result = ""
for i=1,#nested do
result = result .. "\n\n\n{| class=mw-collapsible style=border-collapse:collapse\n|" .. format(nested[i].name) .. "\n|-\n|"
result = result .. dump(nested[i], " ")
result = result .. "\n|}"
end
return frame:preprocess(result)
end
return export
6c148v0g4w04cjxa4efo8jxgbt2i7cy
Modul:utilities/data
828
11807
375844
179981
2026-09-25T09:06:23Z
SNN95
2113
kemaskini
375844
Scribunto
text/plain
local data = {}
-- Namespaces which format_categories will allow: (main), Appendix, Thesaurus, Citations, Reconstruction
data.category_namespaces = require("Module:table").listToSet{0, 100, 110, 114, 118}
-- Script that should be applied to links in categories.
data.catfix_scripts = {
["ab"] = "Cyrl",
["ae"] = "Avst",
["ar"] = "Arab",
["ary"] = "Arab",
["be"] = "Cyrl",
["cmn"] = "Hani",
["cu"] = "Cyrs",
["de"] = "Latn",
["el"] = "Grek",
["en"] = "Latn",
["fa"] = "Arab",
["grc"] = "Polyt",
["he"] = "Hebr",
["hi"] = "Deva",
["hu"] = "Latn",
["ja"] = "Jpan",
["ka"] = "Geor",
["ko"] = "Kore",
["mr"] = "Deva",
["nan-hbl"] = "Hani",
["orv"] = "Cyrs",
["ro"] = "Latn",
["ru"] = "Cyrl",
["sa"] = "Deva",
["sq"] = "Latn",
["syl"] = "Sylo",
["xfa"] = "Ital",
["yue"] = "Hani",
["zh"] = "Hani",
["zle-ono"] = "Cyrs",
["zle-ort"] = "Cyrs",
--[[
[""] = "",
]]
}
return data
5r3vjfz59oyil3lm07bfrszeq7w30yp
Modul:quote
828
13170
375851
344584
2026-09-25T09:20:12Z
SNN95
2113
terjemah sikit
375851
Scribunto
text/plain
--[=[
This module contains functions to implement quote-* templates.
Author: Benwing2; conversion into Lua of {{quote-meta/source}} template,
written by Sgconlaw with some help from Erutuon and Benwing2.
The main interface is quote_t(). Note that the source display is handled by source(), which reads both the
arguments passed to it *and* the arguments passed to the parent template, with the former overriding the latter.
]=]
local export = {}
--[=[
FIXME:
1. Dates like `1 march 2025` with the day of the month being 1 and the month lowercased are treated as month-year-only.
The relevant code checks for a capitalized word; it should probably instead check for the actual month names or
abbrevs.
]=]
-- Named constants for all modules used, to make it easier to swap out sandbox versions.
local check_isxn_module = "Module:check isxn"
local debug_track_module = "Module:debug/track"
local en_utilities_module = "Module:en-utilities"
local italics_module = "Module:italics"
local labels_module = "Module:labels"
local languages_module = "Module:languages"
local languages_error_module = "Module:languages/error"
local links_module = "Module:links"
local number_utilities_module = "Module:number utilities"
local parameters_module = "Module:parameters"
local parse_utilities_module = "Module:parse utilities"
local qualifier_module = "Module:qualifier"
local roman_numerals_module = "Module:roman numerals"
local scribunto_module = "Module:Scribunto"
local script_utilities_module = "Module:script utilities"
local scripts_module = "Module:scripts"
local string_pattern_escape_module = "Module:string/patternEscape"
local string_replacement_escape_module = "Module:string/replacementEscape"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local template_parser_module = "Module:template parser"
local usex_module = "Module:usex"
local usex_templates_module = "Module:usex/templates"
local utilities_module = "Module:utilities"
local yesno_module = "Module:yesno"
local concat = table.concat
local insert = table.insert
local new_title = mw.title.new
local remove = table.remove
local require = require
local sort = table.sort
local u = mw.ustring.char
local ugsub = mw.ustring.gsub
local umatch = mw.ustring.match
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
-- Use HTML entities here to avoid parsing issues (esp. with brackets)
local SEMICOLON_SPACE = "; "
local SPACE_LBRAC = " ["
local RBRAC = "]"
local TEMP_LT = u(0xFFF1)
local TEMP_GT = u(0xFFF2)
local TEMP_LBRAC = u(0xFFF3)
local TEMP_RBRAC = u(0xFFF4)
local TEMP_SEMICOLON = u(0xFFF5)
local function apply_subst(...)
apply_subst = require(usex_module).apply_subst
return apply_subst(...)
end
local function check_isbn(...)
check_isbn = require(check_isxn_module).check_isbn
return check_isbn(...)
end
local function check_issn(...)
check_issn = require(check_isxn_module).check_issn
return check_issn(...)
end
local function debug_track(...)
debug_track = require(debug_track_module)
return debug_track(...)
end
local function decode_entities(...)
decode_entities = require(string_utilities_module).decode_entities
return decode_entities(...)
end
local function embedded_language_links(...)
embedded_language_links = require(links_module).embedded_language_links
return embedded_language_links(...)
end
local function escape_wikicode(...)
escape_wikicode = require(parse_utilities_module).escape_wikicode
return escape_wikicode(...)
end
local function find_best_script_without_lang(...)
find_best_script_without_lang = require(scripts_module).findBestScriptWithoutLang
return find_best_script_without_lang(...)
end
local function find_parameters(...)
find_parameters = require(template_parser_module).find_parameters
return find_parameters(...)
end
local function format_categories(...)
format_categories = require(utilities_module).format_categories
return format_categories(...)
end
local function format_processed_labels(...)
format_processed_labels = require(labels_module).format_processed_labels
return format_processed_labels(...)
end
local function format_qualifier(...)
format_qualifier = require(qualifier_module).format_qualifier
return format_qualifier(...)
end
local function format_usex(...)
format_usex = require(usex_module).format_usex
return format_usex(...)
end
local function get_lang(...)
get_lang = require(languages_module).getByCode
return get_lang(...)
end
local function get_number(...)
get_number = require(number_utilities_module).get_number
return get_number(...)
end
local function get_script(...)
get_script = require(scripts_module).getByCode
return get_script(...)
end
local function gsplit(...)
gsplit = require(string_utilities_module).gsplit
return gsplit(...)
end
local function page_should_be_ignored(...)
page_should_be_ignored = require(usex_templates_module).page_should_be_ignored
return page_should_be_ignored(...)
end
local function parse_inline_modifiers(...)
parse_inline_modifiers = require(parse_utilities_module).parse_inline_modifiers
return parse_inline_modifiers(...)
end
local function parse_inline_modifiers_from_segments(...)
parse_inline_modifiers_from_segments = require(parse_utilities_module).parse_inline_modifiers_from_segments
return parse_inline_modifiers_from_segments(...)
end
local function parse_multi_delimiter_balanced_segment_run(...)
parse_multi_delimiter_balanced_segment_run = require(parse_utilities_module).parse_multi_delimiter_balanced_segment_run
return parse_multi_delimiter_balanced_segment_run(...)
end
local function parse_term_with_lang(...)
parse_term_with_lang = require(parse_utilities_module).parse_term_with_lang
return parse_term_with_lang(...)
end
local function pattern_escape(...)
pattern_escape = require(string_pattern_escape_module)
return pattern_escape(...)
end
local function pluralize(...)
pluralize = require(en_utilities_module).pluralize
return pluralize(...)
end
local function process_params(...)
process_params = require(parameters_module).process
return process_params(...)
end
local function remove_links(...)
remove_links = require(links_module).remove_links
return remove_links(...)
end
local function roman_to_arabic(...)
roman_to_arabic = require(roman_numerals_module).roman_to_arabic
return roman_to_arabic(...)
end
local function replacement_escape(...)
replacement_escape = require(string_replacement_escape_module)
return replacement_escape(...)
end
local function scribunto_parameter_key(...)
scribunto_parameter_key = require(scribunto_module).scribunto_parameter_key
return scribunto_parameter_key(...)
end
local function serial_comma_join(...)
serial_comma_join = require(table_module).serialCommaJoin
return serial_comma_join(...)
end
local function shallow_copy(...)
shallow_copy = require(table_module).shallowCopy
return shallow_copy(...)
end
local function split(...)
split = require(string_utilities_module).split
return split(...)
end
local function split_alternating_runs(...)
split_alternating_runs = require(parse_utilities_module).split_alternating_runs
return split_alternating_runs(...)
end
local function split_and_process_raw_labels(...)
split_and_process_raw_labels = require(labels_module).split_and_process_raw_labels
return split_and_process_raw_labels(...)
end
local function split_on_comma(...)
split_on_comma = require(parse_utilities_module).split_on_comma
return split_on_comma(...)
end
local function tag_text(...)
tag_text = require(script_utilities_module).tag_text
return tag_text(...)
end
local function tag_transcription(...)
tag_transcription = require(script_utilities_module).tag_transcription
return tag_transcription(...)
end
local function tag_translit(...)
tag_translit = require(script_utilities_module).tag_translit
return tag_translit(...)
end
local function ulen(...)
ulen = require(string_utilities_module).len
return ulen(...)
end
local function unitalicize_brackets(...)
unitalicize_brackets = require(italics_module).unitalicize_brackets
return unitalicize_brackets(...)
end
local function upper(...)
upper = require(string_utilities_module).upper
return upper(...)
end
local function usub(...)
usub = require(string_utilities_module).sub
return usub(...)
end
local function yesno(...)
yesno = require(yesno_module)
return yesno(...)
end
local function track(page)
debug_track("quote/" .. page)
end
local function maintenance_line(text)
return '<span class="maintenance-line">(' .. text .. ")</span>"
end
local function isbn(text)
return "[[Special:BookSources/"
.. text
.. "|→ISBN]]"
.. check_isbn(
text,
' <span class="error" style="font-size:88%">Invalid ISBN</span>[[Category:Pages with ISBN errors]]'
)
end
local function issn(text)
return "[https://www.worldcat.org/issn/"
.. text
.. " →ISSN]"
.. check_issn(
text,
' <span class="error" style="font-size:88%">Invalid ISSN</span>[[Category:Pages with ISSN errors]]'
)
end
local function lccn(text)
text = text:gsub(" ", "")
if text:find("-") then
-- old-style LCCN; reformat per request by [[User:The Editor's Apprentice]]
local prefix, part1, part2 = text:match("^(.-)(%d+)%-(%d+)$")
if prefix then
if ulen(part2) < 6 then
part2 = ("0"):rep(6 - ulen(part2)) .. part2
end
text = prefix .. part1 .. part2
end
end
return "[https://lccn.loc.gov/" .. mw.uri.encode(text) .. " →LCCN]"
end
local function format_date(text)
return mw.getCurrentFrame():callParserFunction("#formatdate", text, "dmy")
end
-- Parse a raw lb= param (or nil) to individual label info objects and then concatenate them appropriately into a
-- qualifier input, respecting flags like `omit_preComma` and `omit_postSpace` in the label specs.
local function parse_and_format_labels(raw_lb, lang)
if not raw_lb then
return nil
end
local labels = split_and_process_raw_labels{labels = raw_lb, lang = lang, nocat = true}
labels = format_processed_labels{labels = labels, lang = lang, no_ib_content = true}
if labels ~= "" then -- not sure labels can be an empty string but it seems possible in some circumstances
return {labels}
end
end
-- Convert a comma-separated list of language codes to a comma-separated list of language names. `fullname` is the
-- name of the parameter from which the list of language codes was fetched.
local function format_langs(langs)
local names = {}
for i, lang in ipairs(langs) do
names[i] = lang:getCanonicalName()
end
if #names == 1 then
return names[1]
end
return serial_comma_join(names)
end
local function get_first_lang(langs)
return langs[1] or get_lang("und")
end
--[=[
Normally we parse off inline modifiers and language code prefixes in various places, e.g. he:מרים<tr:Miryem>. But we
exclude HTML entries with <span ...>, <i ...>, <br/> or similar in it, caused by wrapping an argument in {{l|...}},
{{lang|...}} or similar. Basically, all tags of the sort we parse here should consist of a less-than sign, plus letters,
plus a colon, e.g. <tr:...>, so if we see a tag on the outer level that isn't in this format, we don't try to parse it.
The restriction to the outer level is to allow generated HTML inside of e.g. qualifier modifiers, such as
foo<q:similar to {{m|fr|bar}}> (if we end up supporting such modifiers).
Also exclude things that look like URL's from being parsed as having language code prefixes.
]=]
local function val_should_not_be_parsed_for_annotations(val)
return val:find("^[^<]*<%l*[^%l:]") or val:find("^%l+://")
end
local param_mods = {
t = {
-- <t:...> and <gloss:...> are aliases.
item_dest = "gloss",
},
gloss = {},
alt = {},
tr = {},
ts = {},
subst = {},
sc = {type = "script"},
f = {
convert = function(arg, parse_err)
local prefix, val = arg:match("^(.-):([^ ].*)$")
if not prefix then
prefix = ""
val = arg
end
local tags, sc_code, sc = prefix:match("^(.*)/(.-)$")
if sc_code then
sc = get_script(sc_code) or
require(languages_error_module)(sc_code, parse_err, "script code", nil, "not real lang")
else
tags = prefix
end
local quals
if tags ~= "" then
quals = split_on_comma(tags)
for i, qual in ipairs(quals) do
local obj = get_lang(qual, nil, "allow etym") or get_script(qual)
quals[i] = obj or qual
end
end
return {
quals = quals,
sc = sc,
val = val,
}
end,
store = "insert",
},
q = {},
qq = {},
}
local function generate_obj_annotated_text(text, parse_err, paramname)
local obj = {}
if text:find(":[^ ]") or text:find("%[%[") then
local display, is_wikipedia_wikisource
obj.text, obj.lang, obj.link, display, is_wikipedia_wikisource =
parse_term_with_lang {
term = text,
parse_err = parse_err,
paramname = paramname
}
-- HACK: If object is a Wikipedia or Wikisource link, we need to convert it to a two-part link
-- to avoid the w: or s: prefix showing. We should probably avoid this by calling
-- language_link() (in [[Module:links]]) in format_annotated_text() instead of the hacky stuff
-- that we currently do; but we need to test that carefully to make sure it doesn't do things like
-- munge % signs.
if is_wikipedia_wikisource and not obj.text:find("%[%[") and not obj.text:find("%]%]") then
-- Don't directly set .alt, because then if the user specifies <alt:...>, it will cause an error in
-- parse_inline_modifiers().
obj.display = display
end
else
obj.text = text
obj.link = text
end
return obj
end
--[=[
Parse a textual property that may be in a foreign language or script and may be annotated with a language prefix and/or
inline modifiers. `val` is the value of the parameter and `fullname` is the name of the parameter from which the value
was retrieved. `explicit_gloss`, if specified and non-nil, overrides any gloss specified using the <t:...> or
<gloss:...> inline modifier.
If `val` is nil, the return value of this function is nil. Otherwise it is parsed for a language prefix (e.g.
'ar:مُؤَلِّف') and inline modifiers (e.g. 'ar:مُؤَلِّف<t:Author>'), and the return value is an object with the following
fields:
`lang`: The language object corresponding to the language prefix, if specified, or nil if no language prefix is
given.
`text`: The text after stripping off any language prefix and inline modifiers.
`link`: The link part of the text if it consists of a two-part link; otherwise, same as `text`.
`alt`: Display text specified using the <alt:...> modifier, if given; otherwise, nil.
`subst`: Substitutions used to generate the transliteration, in the same format as the subst= parameter.
`sc`: The script object corresponding to the <sc:...> modifier, if given; otherwise nil.
`tr`: The transliteration corresponding to the <tr:...> modifier, if given; otherwise nil.
`ts`: The transcription corresponding to the <ts:...> modifier, if given; otherwise nil.
`gloss`: The gloss/translation corresponding to the `explicit_gloss` parameter (if given and non-nil), otherwise
the <t:...> or <gloss:...> modifiers if given, otherwise nil.
`f`: Foreign versions of the text.
`q`: Left qualifiers.
`qq`: Right qualifiers.
Note that as a special case, if `val` contains HTML tags at the top level (e.g. '<span class="Arab">...</span>', as
might be generated by specifying {{lang|ar|مُؤَلِّف}}), no language prefix or inline modifiers are parsed, and the return
value has the `noscript` field set to true, which tells format_annotated_text() not to try to identify the script of
the text and CSS-tag the text accordingly, but to leave the text untagged.
This object can be passed to format_annotated_text() to format a string displaying the text (appropriately
script-tagged, unless `noscript` is set, as described above) and modifiers.
]=]
local function parse_annotated_text(val, fullname, explicit_gloss)
if not val then
return nil
end
-- When checking for inline modifiers, exclude HTML entry with <span ...>, <i ...>, <br/> or similar in it, caused
-- by wrapping an argument in {{l|...}}, {{lang|...}} or similar. Also exclude URL's from being parsed as having
-- language code prefixes. See val_should_not_be_parsed_for_annotations() for more information. If we find a
-- parameter value with top-level HTML in it, add 'noscript = true' to indicate that we should not try to do script
-- inference and tagging. (Otherwise, e.g. if you specify {{lang|ar|مُؤَلِّف}} as the author, you'll get an extra big
-- font coming from the fact that {{lang|...}} wraps the Arabic text in CSS that increases the size from the
-- default, and then we do script detection and again wrap the text in the same CSS, which increases the size even
-- more.)
if val_should_not_be_parsed_for_annotations(val) then
return {text = val, link = val, noscript = true, gloss = explicit_gloss}
end
local obj
if val:find("<") then
-- Check for inline modifier.
obj = parse_inline_modifiers(val, {
paramname = fullname,
param_mods = param_mods,
generate_obj = generate_obj_annotated_text,
})
else
obj = generate_obj_annotated_text(val, nil, fullname)
end
if explicit_gloss then
obj.gloss = explicit_gloss
end
return obj
end
local html_entity_char_to_replacement = {
["<"] = TEMP_LT,
[">"] = TEMP_GT,
["["] = TEMP_LBRAC,
["]"] = TEMP_RBRAC,
}
local function html_entity_replacement(entity, code_without_semicolon, hash, xcode, x, code)
-- Try to decode the entity. If successful, Replace certain special HTML entities (those that are bracket-like)
-- with single Unicode characters; otherwise, replace the semicolon with a special character so it won't get
-- interpreted as a delimiter.
local ch = decode_entities(entity)
if ch ~= entity then
return html_entity_char_to_replacement[ch] or code_without_semicolon .. TEMP_SEMICOLON
end
-- If the entity doesn't decode, escape it anyway iff it follows a valid format.
if hash == "" then
-- Any nonstandard MediaWiki-only entities have now been filtered off, so treat any non-ASCII characters as
-- invalid.
return xcode:match("^[^\128-\255]+$") and code_without_semicolon .. TEMP_SEMICOLON or entity
elseif x == "" then
return xcode:match("^%d+$") and code_without_semicolon .. TEMP_SEMICOLON or entity
end
return code:match("^%x+$") and code_without_semicolon .. TEMP_SEMICOLON or entity
end
local html_entity_replacement_to_char = {
[TEMP_LT] = "<",
[TEMP_GT] = ">",
[TEMP_LBRAC] = "[",
[TEMP_RBRAC] = "]",
[TEMP_SEMICOLON] = ";",
}
local function undo_html_entity_replacement(txt)
-- Pattern covers everything in html_entity_replacement_to_char.
return (txt:gsub("\239\191[\177-\181]", html_entity_replacement_to_char))
end
-- NOTE: We try hard to optimize this function for the common cases and avoid loading [[Module:parse utilities]]
-- in such cases. The cases we can handle without loading [[Module:parse utilities]] are single values (no
-- semicolons present) without inline modifiers or language prefixes, and multi-entity values (semicolons present)
-- without (a) brackets of any kind (including parens, braces and angle brackets; angle brackets typically indicate
-- inline modifiers and other brackets may protect a semicolon from being interpreted as a delimiter);
-- (b) ampersands (which may indicate HTML entities, which protect a semicolon from being interpreted as a
-- delimiter); and (c) colons not followed by a space (which may indicate a language prefix).
local function generate_obj_multivalued_annotated_text(text, parse_err, paramname, no_undo_html_entity_replacement)
local obj = generate_obj_annotated_text(text, parse_err, paramname)
if not no_undo_html_entity_replacement then
obj.text = undo_html_entity_replacement(obj.text)
obj.link = undo_html_entity_replacement(obj.link)
end
return obj
end
--[=[
Similar to parse_annotated_text() but the parameter value may contain multiple semicolon-separated entities, each with
their own inline modifiers. Some examples:
* mainauthor=Paula Pattengale; Terea Sonsthagen
* author=Katie Brick; J. Cody Nielsen; Greg Jao; Eric Paul Rogers; John A. Monson
* author=Suzanne Brockmann; Patrick G. Lawlor (Patrick Girard); Melanie Ewbank
* author=G Ristori; et al.
* author=Jason Scott; zh:王晰宁<t:Wang Xining>
* editors=zh:包文俊; zh:金心雯
* quotee=zh:張福運<t:Chang Fu-yun>; zh:張景文<t:Chang Ching-wen>
There may be embedded semicolons within brackets, braces or parens that should not be treated as delimiters, e.g.:
* author=Oliver Optic [pseudonym; {{w|William Taylor Adams}}]
* author=author=Shannon Drake (pen name; {{w|Heather Graham Pozzessere}})
* author=James (the Elder;) Humphrys
There may also be HTML entities with semicolons in them:
* author=[{{w|Gilbert Clerke}}]
* 2ndauthor=Martin Biddle & Sally Badham
* author=Peter Christen Asbjørnsen
There may be both embedded semicolons and HTML entities with semicolons in them:
* author=[{{w|Voltaire}} [pseudonym; François-Marie Arouet]]
In general we want to treat [ like an opening bracket and ] like a closing bracket. Beware that they may be
mismatched:
* author=Anonymous [{{w|Karl Maria Kertbeny}}]
Here, `val` is the value of the parameter and `fullname` is the name of the parameter from which the value was
retrieved. `explicit_gloss`, if specified and non-nil, overrides any gloss specified using the <t:...> or <gloss:...>
inline modifier, and `explicit_gloss_fullname` is the name of the parameter from which this value was retrieved. (If
`explicit_gloss` is specified and multiple values were seen, an error results.)
Return value is a list of objects of the same sort as returned by parse_annotated_text().
]=]
local function parse_multivalued_annotated_text(val, fullname, explicit_gloss, explicit_gloss_fullname)
if not val then
return nil
end
-- NOTE: In the code that follows, we use `entity` most of the time to refer to one of the semicolon-separated
-- values in the multivalued param. Entities are most commonly people (typically authors, editors, translators or
-- the like), but may be the names of publishers, locations, or other entities. "Entity" can also refer to HTML
-- entities; in the places where this occurs, the variable name contains 'html' in it.
local splitchar, english_delim
if val:find("^,") then
splitchar = ","
english_delim = "comma"
val = val:gsub("^,", "")
else
splitchar = ";"
english_delim = "semicolon"
end
-- Optimization #1: No semicolons/commas or angle brackets (indicating inline modifiers).
if not val:find("[<" .. splitchar .. "]") then
if val_should_not_be_parsed_for_annotations(val) then
return {{text = val, link = val, noscript = true}}
else
return {generate_obj_multivalued_annotated_text(val, nil, fullname, "no undo html entity replacement")}
end
end
-- Optimization #2: Semicolons/commas but no angle brackets (indicating inline modifiers), braces, brackets, or
-- parens (any of which would protect the semicolon/comma from interpretation as a delimiter), and no ampersand
-- (which might indicate an HTML entity with a terminating semicolon, which should not be interpreted as a
-- delimiter).
if not val:find("[<>%[%](){}&]") then
local entity_objs = {}
for entity in gsplit(val, "%s*" .. splitchar .. "%s*") do
if val_should_not_be_parsed_for_annotations(entity) then
insert(entity_objs, {
text = entity,
link = entity,
noscript = true
})
else
insert(entity_objs, generate_obj_multivalued_annotated_text(entity, nil, fullname, "no undo html entity replacement"))
end
end
return entity_objs
end
-- Escape HTML entities, and get rid of directionality markers.
local amp = val:find("&", nil, true)
if amp then
-- The pattern is more permissive than the usual entity pattern, as MediaWiki has some nonstandard entities
-- that have non-ASCII characters in their codes.
val = val:gsub("((&(#?)(([xX]?)([%w\128-\255]+)));)", html_entity_replacement)
end
-- Pattern covers left-to-right (U+200E) and right-to-left (U+200F).
val = val:gsub("\226\128[\142\143]", "")
-- Parse balanced segment runs, treating HTML entities for left and right bracket and left and right angle bracket
-- as matching literal versions of the same characters.
local entity_runs = parse_multi_delimiter_balanced_segment_run(
val,
{{"[" .. TEMP_LBRAC, "]" .. TEMP_RBRAC}, {"(", ")"}, {"{", "}"}, {"<" .. TEMP_LT, ">" .. TEMP_GT}},
true
)
if type(entity_runs) == "string" then
local undo_val = undo_html_entity_replacement(val)
-- Parse error due to unbalanced delimiters. Don't throw an error here; instead, don't attempt to parse off
-- any annotations, but return the value directly, maybe allowing script tagging (not allowing it if it appears
-- the text is already script-tagged).
return {{text = undo_val, link = undo_val, noscript = not not val_should_not_be_parsed_for_annotations(val)}}
end
-- Split on semicolon (or comma), possibly surrounded by whitespace.
local separated_groups = split_alternating_runs(entity_runs, "%s*" .. splitchar .. "%s*")
-- Process each value.
local entity_objs = {}
for _, entity_group in ipairs(separated_groups) do
-- Rejoin runs that don't involve <...>.
local j = 2
while j <= #entity_group do
if not entity_group[j]:find("^<.*>$") then
entity_group[j - 1] = entity_group[j - 1] .. entity_group[j] .. entity_group[j + 1]
remove(entity_group, j)
remove(entity_group, j)
else
j = j + 2
end
end
local oneval = undo_html_entity_replacement(concat(entity_group))
-- When checking for inline modifiers, exclude HTML entry with <span ...>, <i ...>, <br/> or similar in it,
-- caused by wrapping an argument in {{l|...}}, {{lang|...}} or similar. Also exclude URL's from being parsed
-- as having language code prefixes. This works analogously to parse_annotated_text(); see there for more.
if val_should_not_be_parsed_for_annotations(oneval) then
insert(entity_objs, {
text = oneval,
link = oneval,
noscript = true
})
else
local obj
if #entity_group > 1 then
-- Check for inline modifier.
obj = parse_inline_modifiers_from_segments({
group = entity_group,
arg = oneval,
props = {
paramname = fullname,
param_mods = param_mods,
generate_obj = generate_obj_multivalued_annotated_text,
},
})
else
obj = generate_obj_multivalued_annotated_text(entity_group[1], nil, fullname)
end
insert(entity_objs, obj)
end
end
if explicit_gloss then
if #entity_objs > 1 then
error(
(
"Can't specify |%s= along with multiple %s-separated entities in |%s=; use the <t:...> "
.. "inline modifier attached to the individual entities"
):format(explicit_gloss_fullname, english_delim, fullname)
)
end
entity_objs[1].gloss = explicit_gloss
end
return entity_objs
end
--[=[
Format a text property that may be in a foreign language or script, along with annotations. This is conceptually
similar to the full_link() function in [[Module:links]], but displays the annotations in a different format that is
more appropriate for bibliographic entries. The output looks like this:
TEXT [TRANSLIT /TRANSCRIPTION/, GLOSS]
`textobj` is as returned by parse_annotated_text(). `tag_text_func`, if supplied, is a function of one argument to further
wrap the text after it has been processed and CSS-tagged appropriately, directly before insertion. `tag_gloss_func` is a
similar function for the gloss.
]=]
local function format_annotated_text(textobj, tag_text_func, tag_gloss_func)
if not textobj then
return nil
end
local text, link = textobj.text, textobj.link
local subst, tr, ts, f, gloss = textobj.subst, textobj.tr, textobj.ts, textobj.f, textobj.gloss
-- Retrieve the display text, either specified by the user (.alt) or by parse_annotated_text() (.display) when a
-- Wikipedia link is given.
local alt = textobj.alt or textobj.display
if alt then
if link:find("%[%[") or link:find("%]%]") then
local errmsg = ("Can't currently handle embedded links in '%s', with <alt:...> text '%s'"):format(link, alt)
error(escape_wikicode(errmsg))
end
text = ("[[%s|%s]]"):format(link, alt)
end
-- See above for `noscript`, meaning HTML was found in the text value, probably generated using {{lang|...}}.
-- {{lang}} already script-tags the text and processes embedded language links, so we don't want to do it again (in
-- fact, the code below within the if-clause is similar to what {{lang}} does). In such a case, an explicit language
-- won't be available and find_best_script_without_lang() may not be accurate, so we can't do automatic transliteration.
if not textobj.noscript then
local lang = textobj.lang
-- As an optimization, don't do script detection on an argument that contains only ASCII.
local sc = textobj.sc
or lang and lang:findBestScript(text)
or not text:find("^[ -~]$") and find_best_script_without_lang(text)
or nil
-- As an optimization, don't do any of the following if there's no language, script, translit or transcription,
-- as will be the case with simple ASCII values.
if lang or sc or tr or ts then
if not lang then
lang = get_lang("und")
end
if tr == "-" then
tr = nil
elseif not tr and sc and not sc:getCode():find("Lat") then -- Latn, Latf, Latg, pjt-Latn
-- might return nil
local text_for_tr = text
if subst then
text_for_tr = apply_subst(text_for_tr, subst)
else
text_for_tr = remove_links(text)
end
tr = (lang:transliterate(text_for_tr, sc))
end
if text:find("%[%[") then
-- FIXME: embedded_language_links() replaces % signs with their URL-encoded equivalents,
-- which messes up URL's that may be present (e.g. if chapterurl= is given). IMO this
-- should not happen, and embedded_language_links() should do nothing if no embedded links
-- are present. To work around this, only call embedded_language_links() when there are
-- embedded links present.
text = embedded_language_links({
term = text,
lang = lang,
sc = sc,
})
end
if lang:getCode() ~= "und" or sc:getCode() ~= "Latn" then
text = tag_text(text, lang, sc)
end
if tr then
-- Should we link to the transliteration of languages with lang:link_tr()? Probably not because `text` is not
-- likely to be a term that has an entry.
tr = tag_translit(tr, lang, "usex")
end
if ts then
ts = tag_transcription(ts, lang, "usex")
end
end
end
text = unitalicize_brackets(text)
if tag_text_func then
text = tag_text_func(text)
end
local parts = {}
if textobj.q then
insert(parts, format_qualifier(textobj.q) .. " ")
end
insert(parts, text)
if tr or ts or f or gloss then
insert(parts, SPACE_LBRAC)
local subparts = {}
if tr or ts then
local tr_ts
if ts then
ts = "/" .. ts .. "/"
end
if tr and ts then
tr_ts = tr .. " " .. ts
else
tr_ts = tr or ts
end
insert(subparts, tr_ts)
end
if f then
for _, ff in ipairs(f) do
local sc = ff.sc
local lang
if not sc and ff.quals then
local qual = ff.quals[1]
if type(qual) == "string" then
-- do nothing; we'll do script detection farther down
elseif qual:hasType("script") then
sc = qual
else -- language
sc = qual:findBestScript(ff.val)
lang = qual
end
end
if not lang then
lang = get_lang("und")
end
sc = sc or find_best_script_without_lang(ff.val)
local val = embedded_language_links({
term = ff.val,
lang = lang,
sc = sc,
})
if lang:getCode() ~= "und" or sc:getCode() ~= "Latn" then
val = tag_text(val, lang, sc)
end
local qual_prefix
if ff.quals then
for i, qual in ipairs(ff.quals) do
if type(qual) ~= "string" and (qual:hasType("script") or qual:hasType("language")) then
ff.quals[i] = qual:getCanonicalName()
end
end
qual_prefix = concat(ff.quals, "/") .. ": "
else
qual_prefix = ""
end
insert(subparts, qual_prefix .. val)
end
end
if gloss then
gloss = '<span class="e-translation">' .. gloss .. "</span>"
gloss = unitalicize_brackets(gloss)
if tag_gloss_func then
gloss = tag_gloss_func(gloss)
end
insert(subparts, gloss)
end
insert(parts, concat(subparts, ", "))
insert(parts, RBRAC)
end
if textobj.qq then
insert(parts, " " .. format_qualifier(textobj.qq))
end
return concat(parts)
end
--[=[
Format a multivalued text property that may be in a foreign language or script, along with annotations. This is the
multivalued analog to format_annotated_text(), and formats each individual entity using format_annotated_text(),
joining the results with `delimiter`, which defaults to ", ". It `delimiter` is "and" or "or", join the results using
serial_comma_join() with the specified conjunction.
`textobjs` is as returned by parse_multivalued_annotated_text(). `tag_text_func` and `tag_gloss_func` are as in
format_annotated_text().
]=]
local function format_multivalued_annotated_text(textobjs, delimiter, tag_text_func, tag_gloss_func)
if not textobjs then
return nil
end
if #textobjs == 1 then
return format_annotated_text(textobjs[1], tag_text_func, tag_gloss_func)
end
local parts = {}
for _, textobj in ipairs(textobjs) do
insert(parts, format_annotated_text(textobj, tag_text_func, tag_gloss_func))
end
-- Change delimiter to semicolon if the items themselves contain commas (e.g., in ", Jr.")
local use_semicolon = false
for _, obj in ipairs(textobjs) do
-- Use remove_links to ignore commas in links
if obj.text and remove_links(obj.text):find(", ") then
use_semicolon = true
break
end
end
local n = #parts
if n > 0 and parts[n]:match("^'*et al[.']*$") then
-- Special handling for 'et al.'
parts[n] = "''et al.''"
if n == 2 then
-- author et al.
return concat(parts, " ")
else
-- author 1, author 2, et al.
return concat(parts, (use_semicolon and "; ") or ", ")
end
if delimiter == "and" or delimiter == "or" then
delimiter = ", "
end
return concat(parts, delimiter)
end
if delimiter == "and" or delimiter == "or" then
return serial_comma_join(parts, {conj = delimiter})
end
return concat(parts, delimiter or ((use_semicolon and "; ") or ", "))
end
-- Fancy version of ine() (if-not-empty). Converts empty string to nil, but also strips leading/trailing space.
local function ine(arg)
if not arg then
return nil
elseif type(arg) ~= "string" then
return arg
end
arg = mw.text.trim(arg)
if arg == "" then
return nil
end
return arg
end
local abbrs = {
["a."] = {anchor = "a.", full = "ante"},
["c."] = {anchor = "c.", full = "circa"},
["p."] = {anchor = "p.", full = "post"},
}
-- Process prefixes 'a.' (ante), 'c.' (circa) and 'p.' (post) at the beginning of an arbitrary date or year spec.
-- Returns two values, the formatted version of the prefix and the date spec minus the prefix. If no prefix is found,
-- returns an empty string and the full date.
local function process_ante_circa_post(date)
local prefix = usub(date, 1, 2)
local abbr = abbrs[prefix]
local abbr_prefix = ""
if abbr then
abbr_prefix = "''[[Appendix:Glossary#"
.. abbr.anchor
.. '|<abbr title="'
.. abbr.full
.. '">'
.. abbr.anchor
.. "</abbr>]]'' "
-- Remove lowercase letter, period, and space from beginning of date parameter.
date = ugsub(date, "^%l%.%s*", "")
end
return abbr_prefix, date
end
-- Format the arguments that specify the date of the quotation. These include the following:
-- |date=: The date. If |start_date= is given, this is the end date.
-- |year=, |month=: Year and month of quotation date or end of range, if |date= isn't given.
-- |start_date=: The start date, to specify a range.
-- |start_year=, |start_month=: Year and month of start of range, if |start_date= isn't given.
-- |accessdate=: Date a website was accessed; processed if no other date was given.
-- |nodate=: Indicate that no date is present; otherwise a maintenance line will be displayed if there is no date.
--
-- If `parampref` and/or `paramsuf` are given, this modifies all the date arguments accordingly. For example, if
-- `parampref` == "orig" and `paramsuf` is omitted, the date is specified using |origdate= or |origyear=/|origmonth=,
-- and the start of the range is |origstart_date=, etc. Similarly, if `parampref` is omitted and `paramsuf` is
-- "_published", the date is specified using |date_published= or |year_published=/|month_published=, and the start of
-- the range is |start_date_published=, etc.
--
-- `a` and `get_full_paramname` are functions with the same interpretation as the local functions of the same name in
-- source(). These are used to fetch parameters and get their full names. Note that this may cause all arguments to
-- have an index added to them (|date2=, |year2=, |month2=, etc.).
--
-- `alias_map` is as in source() and is used to map canonical arguments to their aliases when aliases were used.
--
-- If `bold_year` is given, displayed years are boldfaced unless boldface is present in the parameter value.
--
-- If `maintenance_line_no_date` is specified, it should be a string that will be returned if no date is found (i.e.
-- neither |date= nor |year=, or their appropriate equivalents per `parampref` and `paramsuf`, are specified, and
-- neither |nodate= is given to indicate that there is no date, or |accessdate= is given).
--
-- Returns two values: the formatted date and a boolean indicating whether to add a maintenance category
-- [[:Category:Requests for date in LANG entries]]. The first return value will be nil if nothing is to be added
-- (in which case the scond return value will always be nil).
local function format_date_args(
a,
get_full_paramname,
alias_map,
parampref,
paramsuf,
bold_year,
maintenance_line_no_date,
year_last
)
local output = {}
parampref = parampref or ""
paramsuf = paramsuf or ""
local function getp(param)
return a(parampref .. param .. paramsuf)
end
local function pname(param)
local fullname = get_full_paramname(parampref .. param .. paramsuf)
return alias_map[fullname] or fullname
end
-- Format `timestamp` (a timestamp referencing a date) according to the spec in `code`. `param` is the base name of
-- the parameter from which the timestamp was fetched, for error messages.
local function format_date_with_code(code, timestamp, param)
local language = mw.getContentLanguage()
local ok, date = pcall(language.formatDate, language, code, timestamp)
if ok then
return date
else
-- All the formats used in format_date_args() are fine, so the timestamp must be at fault.
error(
(
"Timestamp |%s=%s (possibly canonicalized from its original format) could not be parsed; see the "
.. "[[mw:Help:Extension:ParserFunctions##time|documentation for the #time parser function]]"
):format(pname(param), tostring(timestamp))
)
end
end
-- Try to figure out if the given timestamp has the day of the month explicitly given. We use the following
-- algorithm:
-- 1. Format as year-month-day; if the day is not 1, the day was explicitly given, since if only the year/month are
-- given, the day shows up as 1.
-- 2. If the day shows up as 1 and there isn't a 1 or 01 in the timestamp, the day wasn't explicitly given.
-- 3. Otherwise, if there are three separate numbers (e.g. 2022-07-01), or two separate numbers plus a capitalized
-- letter (taken as an English month, e.g. 2022 July 1), the day was explicitly given, otherwise not.
--
-- `param` is the base name of the parameter from which the timestamp was fetched.
local function date_has_day_specified(timestamp, param)
local day = format_date_with_code("j", timestamp, param)
if day ~= "1" then
return true
end
local english_month = timestamp:find("%u")
local canon_timestamp = mw.text.trim((timestamp:gsub("%D+", " ")))
local seen_nums = split(canon_timestamp, " ", true)
local saw_one = false
for _, num in ipairs(seen_nums) do
if num == "1" or num == "01" then
saw_one = true
break
end
end
if not saw_one then
return false
end
return #seen_nums >= 3 or english_month and #seen_nums >= 2
end
-- Format a date with boldfaced year, as e.g. '''2023''' August 3. `explicit_day_given` indicates whether to include
-- the day; if false, the return value will be e.g. '''2023''' August. `date_param` is the base name of the param
-- from which the date was fetched, for error messages.
local function format_bold_date(date, explicit_day_given, date_param)
local day_month_code = explicit_day_given and "j F" or "F"
local month_day_code = explicit_day_given and "F j" or "F"
if year_last then
if bold_year then
-- This formats like "3 August '''2023'''" (or "August '''2023'''" if day not explicitly given).
return format_date_with_code(day_month_code .. " '''Y'''", date, date_param)
else
-- This formats like "3 August 2023" (or "August 2023" if day not explicitly given).
return format_date_with_code(day_month_code .. " Y", date, date_param)
end
else
if bold_year then
-- This formats like "'''2023''' August 3" (or "'''2023''' August" if day not explicitly given).
return format_date_with_code("'''Y''' " .. month_day_code, date, date_param)
else
-- This formats like "2023 August 3" (or "2023 August" if day not explicitly given).
return format_date_with_code("Y " .. month_day_code, date, date_param)
end
end
end
-- The formatDate method of the mw.language object behaves like the {{#time:}} parser function, which doesn't
-- accept the formats "monthday monthname, year" or "year monthname monthday", but outputs garbage when it receives
-- them, behavior inherited from PHP. {{#formatdate:}} magic word is more forgiving. Fix dates so that, for
-- instance, the |date= parameter of {{quote-journal}} (which uses this code) and the |accessdate= parameter (which
-- uses {{#formatdate:}}) accept similar date formats. See:
-- * [[mw:Extension:Scribunto/Lua_reference_manual#mw.language:formatDate]]
-- * [[mw:Help:Extension:ParserFunctions##time]]
-- * [[mw:Help:Magic_words#Formatting]]
-- `date` is the date spec from the user, which is assumed to come from a parameter whose base name ends in "date";
-- `parampref` is the prefix added to "date" to get the parameter name.
local function fix_date(date, param_pref)
if tonumber(date) ~= nil then
error(
("|%s= should contain a full date (year, month, day of month); use |%s= for year"):format(
pname(param_pref .. "date"),
pname(param_pref .. "year")
)
)
elseif date and date:find("%s*%a+,%s*%d+%s*$") then
error(
("|%s= should contain a full date (year, month, day of month); use |%s=, |%s= for month and year"):format(
pname(param_pref .. "date"),
pname(param_pref .. "month"),
pname(param_pref .. "year")
)
)
end
if date then
-- Commas are replaced with spaces to prevent parsing issues
local fixed_date = ugsub(date, ", *", " ")
return (ugsub(ugsub(fixed_date or date, "(%d+ %a+),", "%1"), "^(%d%d%d%d) (%a+ %d%d?)$", "%2 %1"))
end
end
local start_date, date = fix_date(getp("start_date"), "start_"), fix_date(getp("date"), "")
local year = getp("year")
local month = getp("month")
local start_year = getp("start_year")
local start_month = getp("start_month")
if date and year then
error(("Only one of |%s= or |%s= should be specified"):format(pname("date"), pname("year")))
end
if date and month then
error(
("|%s= should only be specified in conjunction with |%s=, not with |%s="):format(
pname("month"),
pname("year"),
pname("date")
)
)
end
if start_date and start_year then
error(("Only one of |%s= or |%s= should be specified"):format(pname("start_date"), pname("start_year")))
end
if start_date and start_month then
error(
("|%s= should only be specified in conjunction with |%s=, not with |%s="):format(
pname("start_month"),
pname("start_year"),
pname("start_date")
)
)
end
if (start_date or start_year) and not (date or year) then
error(
("|%s= or |%s=/|%s= cannot be specified without specifying |%s= or |%s=/|%s="):format(
pname("start_date"),
pname("start_year"),
pname("start_month"),
pname("date"),
pname("year"),
pname("month")
)
)
end
local dash = " – "
local day_explicitly_given = date and date_has_day_specified(date, "date")
local start_day_explicitly_given = start_date and date_has_day_specified(start_date, "start_date")
-- Format a date with boldfaced year, as e.g. '''2023''' August 3 (if `explicit_day_given` specified) or
-- '''2023''' August (if `explicit_day_given` not specified). If no date specified, fall back to formatting based
-- on the year and (optionally) month params given in `yearobj` and `monthobj`, boldfacing the year if not already.
-- `date_param` is the base name of the param from which the date was fetched, for error messages.
local function format_date_or_year_month(date, yearobj, monthobj, explicit_day_given, date_param)
if date then
return format_bold_date(date, explicit_day_given, date_param)
else
-- Boldface a year spec if it's not already boldface.
if bold_year and not yearobj.text:find("'''") then
-- Clone the year object before modifying it because we may use it later to check against the current
-- year (if we're dealing with start_year).
yearobj = shallow_copy(yearobj)
yearobj.text = "'''" .. yearobj.text .. "'''"
if yearobj.alt then
yearobj.alt = "'''" .. yearobj.alt .. "'''"
end
end
if year_last then
return (monthobj and format_annotated_text(monthobj) .. " " or "") .. format_annotated_text(yearobj)
else
return format_annotated_text(yearobj) .. (monthobj and " " .. format_annotated_text(monthobj) or "")
end
end
end
local yearobj = parse_annotated_text(year, pname("year"))
local monthobj = parse_annotated_text(month, pname("month"))
local start_yearobj = parse_annotated_text(start_year, pname("start_year"))
local start_monthobj = parse_annotated_text(start_month, pname("start_month"))
if start_yearobj then
local abbr_prefix
abbr_prefix, start_yearobj.text = process_ante_circa_post(start_yearobj.text)
start_yearobj.link = select(2, process_ante_circa_post(start_yearobj.link))
insert(output, abbr_prefix)
elseif yearobj then
local abbr_prefix
abbr_prefix, yearobj.text = process_ante_circa_post(yearobj.text)
yearobj.link = select(2, process_ante_circa_post(yearobj.link))
insert(output, abbr_prefix)
end
if start_date or start_year then
local cur_year = yearobj and yearobj.text or format_date_with_code("Y", date, "date")
local cur_month = monthobj and monthobj.text or date and format_date_with_code("F", date, "date") or nil
local cur_day = date and day_explicitly_given and format_date_with_code("j", date, "date") or nil
local beg_year = start_yearobj and start_yearobj.text or format_date_with_code("Y", start_date, "start_date")
local beg_month = start_monthobj and start_monthobj.text
or start_date and format_date_with_code("F", start_date, "start_date")
or nil
local beg_day = start_date
and start_day_explicitly_given
and format_date_with_code("j", start_date, "start_date")
or nil
if year_last then
if beg_day then
insert(output, beg_day .. " " .. beg_month)
else
insert(output, beg_month)
end
if beg_year ~= cur_year then
insert(output, " " .. beg_year)
end
else
insert(output, format_date_or_year_month(
start_date,
start_yearobj,
start_monthobj,
start_day_explicitly_given,
"start_date"
))
end
if cur_year ~= beg_year then
-- Different years; insert current date in full.
if beg_month or cur_month then
insert(output, dash)
else
insert(output, "–")
end
insert(output, format_date_or_year_month(date, yearobj, monthobj, day_explicitly_given, "date"))
elseif cur_month and cur_month ~= beg_month then
local month_ins = monthobj and format_annotated_text(monthobj) or cur_month
-- Same year but different months; insert current month and (if available) current day.
if cur_day then
insert(output, dash)
if year_last then
insert(output, cur_day .. " " .. month_ins)
else
insert(output, month_ins .. " " .. cur_day)
end
else
if beg_day then
insert(output, dash)
else
insert(output, "–")
end
insert(output, month_ins)
end
elseif cur_day and cur_day ~= beg_day then
-- Same year and month but different days; insert current day.
insert(output, "–")
insert(output, cur_day)
else
-- Same year, month and day; or same year and month, and day not available; or same year, and month and
-- day not available. Do nothing. FIXME: Should we throw an error?
end
if year_last and beg_year == cur_year then
if cur_month then
insert(output, " " .. cur_year)
else
insert(output, " " .. cur_year)
end
end
elseif date or yearobj then
insert(output, format_date_or_year_month(date, yearobj, monthobj, day_explicitly_given, "date"))
elseif not maintenance_line_no_date then
-- Not main quote date. Return nil, caller will handle.
return nil, nil
elseif not getp("nodate") then
local accessdate = getp("accessdate")
if accessdate then
local explicit_day_given = date_has_day_specified(accessdate, "accessdate")
insert(output, format_bold_date(accessdate, explicit_day_given, "accessdate") .. " (last accessed)")
else
if mw.title.getCurrentTitle().namespace ~= 10 then
return maintenance_line(maintenance_line_no_date), true
end
return nil, nil
end
end
return ine(concat(output)), nil
end
local function tag_with_cite(txt)
return "<cite>" .. txt .. "</cite>"
end
-- Display the source line of the quote, above the actual quote text. This contains the majority of the logic of this
-- module (formerly contained in {{quote-meta/source}}).
function export.source(args, alias_map, format_as_cite, other_controls)
local tracking_categories = {}
local argslang = args[1] or args.lang
if not argslang then
-- For the moment, only trigger an error on mainspace pages and
-- other pages that are not user pages or pages containing discussions.
-- These are the same pages that appear in the appropriate tracking
-- categories. User and discussion pages have not generally been
-- fixed up to include a language code and so it's more helpful
-- to use a maintenance line than signal an error.
local current_title = mw.title.getCurrentTitle()
if not (current_title.namespace == 10 or page_should_be_ignored(current_title.fullText)) then
require(languages_error_module)(nil, 1)
end
end
-- Given a canonical param, convert it to the original parameter specified by the user (which may have been an
-- alias).
local function alias(param)
return alias_map[param] or param
end
local output, sep = {}
local overrides = other_controls and other_controls.overrides or {}
-- Add text to the output. The text goes into a list, and we concatenate all the list components together at the
-- end. To make it easier to handle comma-separated items, we keep track (in `sep`) of the separator (if any) that
-- needs to be inserted before the next item added. For example, if we're in the "newversion" code (ind ~= ""), and
-- there's no title and no URL, then the first time we add anything after the title, we don't want to add a
-- separating comma because the preceding text will say "republished " or "republished as " or "translated as " or
-- similar. In all- other cases, we do want to add a separating comma. The bare add() function reset the separator
-- to be nothing, while the add_with_sep() function resets the separator to be the value of `next_sep` (defaulting
-- to ", "), so the next time around we do add a comma to separate `text` from the preceding piece of text.
local function add(text)
if sep then
insert(output, sep)
end
insert(output, text)
sep = nil
end
local function add_with_sep(text, next_sep)
add(text)
sep = next_sep or ", "
end
-- FIXME: This is all very over-engineered, which makes it slow and memory-inefficient.
-- Return a function that generates the actual parameter name associated with a base param (e.g. "author", "last").
-- The actual parameter name may have an index added (an empty string for the first set of params, e.g. author=,
-- last=, or a numeric index for further sets of params, e.g. author2=, last2=, etc.).
local function make_get_full_paramname(ind)
return function(param)
return param .. ind
end
end
-- Function to fetch the actual parameter name associated with a base param (see make_get_full_paramname() above).
-- Assigned at various times below by calling make_get_full_paramname(). We do it this way so that we can have
-- wrapper functions that access params and define them only once.
local get_full_paramname
-- Return two values: the value of a parameter given the base param name (which may have a numeric index added),
-- and the parameter name from which the value was fetched (which may be an alias, i.e. you can't necessarily fetch
-- the parameter value from args[] given this name). The base parameter can be a list of such base params, which
-- are checked in turn, or nil, in which case nil is returned.
local function a_with_name(param)
if not param then
return nil
elseif type(param) ~= "table" then
local fullname = get_full_paramname(param)
return args[fullname], alias(fullname)
end
for _, par in ipairs(param) do
local val, fullname = a_with_name(par)
if val then
return val, alias(fullname)
end
end
return nil
end
-- Fetch the value of a parameter given the base param name (which may have a numeric index added). The base
-- parameter can be a list of such base params, which are checked in turn, or nil, in which case nil is returned.
local function a(param)
return (a_with_name(param))
end
-- Identical to a_with_name(param) except that it verifies that no space is present. Should be used for URL's.
local function aurl_with_name(param)
local value, fullname = a_with_name(param)
if value and value:find(" ") and not value:find("%[") then
error(("URL not allowed to contain a space, but saw |%s=%s"):format(fullname, value))
end
return value, fullname
end
-- Identical to a(param) except that it verifies that no space is present. Should be used for URL's.
local function aurl(param)
return (aurl_with_name(param))
end
-- Convenience function to fetch a parameter that may be in a foreign language or text (and may consequently have
-- a language prefix and/or inline modifiers), parse the annotations and convert the result into a formatted string.
-- This is the same as parse_and_format_annotated_text() below but also returns the full param name as the second
-- return value.
local function parse_and_format_annotated_text_with_name(param, tag_text_func, tag_gloss_func)
local val, fullname = a_with_name(param)
local obj = parse_annotated_text(val, fullname)
return format_annotated_text(obj, tag_text_func, tag_gloss_func), fullname
end
-- Convenience function to fetch a parameter that may be in a foreign language or text (and may consequently have
-- a language prefix and/or inline modifiers), parse the modifiers and convert the result into a formatted string.
-- This is a wrapper around parse_annotated_text() and format_annotated_text(). `param` is the base parameter name (see
-- a_with_name()), `tag_text_func` is an optional function to tag the parameter text after all other processing (e.g.
-- wrap in <cite>...</cite> tags), and `tag_gloss_func` is a similar function for the parameter translation/gloss.
local function parse_and_format_annotated_text(param, tag_text_func, tag_gloss_func)
return (parse_and_format_annotated_text_with_name(param, tag_text_func, tag_gloss_func))
end
-- Convenience function to fetch a multivalued parameter that may be in a foreign language or text (and may
-- consequently have a language prefix and/or inline modifiers), parse the modifiers and convert the result into a
-- formatted string. This is the multivalued analog to parse_and_format_annotated_text_with_name() and returns two
-- values, the formatted string and the full name of the parameter fetched. `delimiter` is as in
-- format_multivalued_annotated_text().
local function parse_and_format_multivalued_annotated_text_with_name(param, delimiter, tag_text_func, tag_gloss_func)
local val, fullname = a_with_name(param)
local objs = parse_multivalued_annotated_text(val, fullname)
local num_objs = objs and #objs or 0
return format_multivalued_annotated_text(objs, delimiter, tag_text_func, tag_gloss_func), fullname, num_objs
end
-- Convenience function to fetch a multivalued parameter that may be in a foreign language or text (and may
-- consequently have a language prefix and/or inline modifiers), parse the modifiers and convert the result into a
-- formatted string. This is the multivalued analog to parse_and_format_annotated_text(). `delimiter` is as in
-- format_multivalued_annotated_text().
local function parse_and_format_multivalued_annotated_text(param, delimiter, tag_text_func, tag_gloss_func)
return (parse_and_format_multivalued_annotated_text_with_name(param, delimiter, tag_text_func, tag_gloss_func))
end
-- This determines whether to display "Mary Bloggs, transl." (if there's no author preceding) or "translated by
-- Mary Bloggs" (if there's an author preceding).
local author_outputted = false
-- When formatting as a citation, the priority is to display a name and a date before the book/chapter title
-- this tracks whether or not the author/date has been displayed
local date_outputted, formatted_date, formatted_origdate = false
local function add_date(no_paren)
if not date_outputted then
if no_paren then
sep = ", "
else
sep = " "
end
if formatted_date then
if no_paren then
add(formatted_date)
else
add("(" .. formatted_date .. ")")
end
end
if formatted_origdate then
add(SPACE_LBRAC .. formatted_origdate .. RBRAC)
end
sep = ", "
date_outputted = true
end
end
local function is_anonymous(val)
return val:match("^[Aa]nonymous$") or val:match("^[Aa]non%.?$")
end
-- Add a formatted author (whose values may be specified using `author` or, for compatibility purposes, split
-- among various parameters):
-- * `author` is the value of the author param (e.g. "author", "author2" or "2ndauthor"), and `author_fullname` is
-- the full parameter name holding that value;
-- * `trans_author` is the optional value of the param holding the gloss/translation of the author, and
-- `trans_author_fullname` is the full parameter name holding that value (or nil for no such parameter);
-- * `authorlink` is the value of the authorlink param, which holds the Wikipedia link of the author(s) in `author`,
-- and `authorlink_fullname` is the full parameter name holding that value;
-- * `trans_authorlink` is the optional value of the param holding the Wikipedia link of the gloss/translation of
-- the author, and `trans_authorlink_fullname` is the full parameter name holding that value (or nil for no such
-- parameter);
-- * `first` is the value of the parameter holding the first name of the author, and `first_fullname` is the full
-- parameter name holding that value;
-- * `trans_first` is the value of the corresponding parameter holding the gloss/translation of the first name
-- (e.g. "trans-first"), and `trans_first_fullname` is the full parameter name holding that value (or nil for
-- no such parameter);
-- * `last` is the value of the parameter holding the last name of the author, and `last_fullname` is the full
-- parameter name holding that value;
-- * `trans_last` is the value of the corresponding parameter holding the gloss/translation of the last name
-- (e.g. "trans-last"), and `trans_last_fullname` is the full parameter name holding that value (or nil for
-- no such parameter).
-- * `last_first` if set, when parameters `first` and `last` are used, display the author name as "last, first"
local function add_author(
author,
author_fullname,
trans_author,
trans_author_fullname,
authorlink,
authorlink_fullname,
trans_authorlink,
trans_authorlink_fullname,
first,
first_fullname,
trans_first,
trans_first_fullname,
last,
last_fullname,
trans_last,
trans_last_fullname,
last_first
)
local function make_author_with_url(txt, txtparam, authorlink, authorlink_param)
if authorlink then
if authorlink:find("%[%[") then
error(("Can't specify links in |%s=%s"):format(authorlink_param, authorlink))
end
if txt:find("%[%[") then
error(("Can't specify links in %s=%s"):format(txtparam, txt))
end
return "[[w:" .. authorlink .. "|" .. txt .. "]]"
else
return txt
end
end
local num_authorobjs
if author then
local authorobjs =
parse_multivalued_annotated_text(author, author_fullname, trans_author, trans_author_fullname)
num_authorobjs = #authorobjs
if num_authorobjs == 1 then
if is_anonymous(authorobjs[1].text) then
authorobjs[1].text = "anonymous author"
authorobjs[1].link = "anonymous author"
end
if authorlink then
authorobjs[1].text = make_author_with_url(
authorobjs[1].text,
"|" .. author_fullname,
authorlink,
"|" .. authorlink_fullname
)
authorobjs[1].link = make_author_with_url(
authorobjs[1].link,
"|" .. author_fullname,
authorlink,
"|" .. authorlink_fullname
)
end
if authorobjs[1].gloss and trans_authorlink then
authorobjs[1].gloss = make_author_with_url(
authorobjs[1].gloss,
("<t:...> in |%s"):format(author_fullname),
trans_authorlink,
"|" .. trans_author_fullname
)
end
add(format_multivalued_annotated_text(authorobjs))
elseif trans_authorlink then
error(
(
"Can't specify |%s= along with multiple semicolon-separated entities in |%s=; use the "
.. "<t:...> inline modifier attached to the individual entities and put the link directly "
.. "in the value of the inline modifier"
):format(trans_authorlink_fullname, author_fullname)
)
else
-- Allow an authorlink with multiple authors, e.g. for use with |author=Max Mills; Harvey Mills
-- with |authorlink=Max and Harvey. For this we have to generate the entire text and link it
-- all.
local formatted_text = format_multivalued_annotated_text(authorobjs)
if authorlink then
formatted_text = make_author_with_url(
formatted_text,
"|" .. author_fullname,
authorlink,
"|" .. authorlink_fullname
)
end
add(formatted_text)
end
else
num_authorobjs = 1
-- Author separated into first name + last name. We don't currently support non-Latin-script
-- authors separated this way and probably never will.
if first then
if last_first then
author = last .. ", " .. first
else
author = first .. " " .. last
end
else
author = last
end
if authorlink then
local authorparam = first and ("|%s |%s"):format(first_fullname, last_fullname) or "|" .. last_fullname
author = make_author_with_url(author, authorparam, authorlink, authorlink_fullname)
end
local trans_author
if trans_last then
if trans_first then
trans_author = trans_first .. " " .. trans_last
else
trans_author = trans_last
end
if trans_authorlink then
local trans_authorparam = trans_first
and ("|%s |%s"):format(trans_first_fullname, trans_last_fullname)
or "|" .. trans_last_fullname
trans_author = make_author_with_url(
trans_author,
trans_authorparam,
trans_authorlink,
trans_authorlink_fullname
)
end
end
add(author)
if trans_author then
add(SPACE_LBRAC)
add(trans_author)
add(RBRAC)
end
end
author_outputted = true
return num_authorobjs
end
local function add_authorlike(
param,
prefix_with_preceding_authors,
suffix_without_preceding_authors,
suffix_if_multiple,
anonymous_suffix
)
local delimiter = author_outputted and "and" or ", "
local entities, _, num_entities = parse_and_format_multivalued_annotated_text_with_name(param, delimiter)
if not entities then
return
end
if is_anonymous(entities) then
-- If tlr=anonymous or similar given, display as "anonymous translator" or similar. If a specific
-- anonymous suffix not given, try to derive the anonymous suffix from the non-preceding-author suffix.
if not anonymous_suffix then
local cleaned_suffix = suffix_without_preceding_authors
:gsub(" ", " ")
:gsub(" ", " ")
:gsub(" ", " ")
:gsub("[", "[")
:gsub("]", "]")
cleaned_suffix = mw.text.trim(cleaned_suffix)
if not anonymous_suffix then
anonymous_suffix = " " .. cleaned_suffix:match("^, (.*)$")
end
if not anonymous_suffix then
anonymous_suffix = " " .. cleaned_suffix:match("^%((.*)%)$")
end
if not anonymous_suffix then
anonymous_suffix = " " .. cleaned_suffix:match("^%[(.*)%]$")
end
if not anonymous_suffix then
anonymous_suffix = suffix_without_preceding_authors
end
end
add_with_sep("anonymous" .. anonymous_suffix)
elseif prefix_with_preceding_authors and (author_outputted or not suffix_without_preceding_authors) then
add_with_sep(prefix_with_preceding_authors .. entities)
elseif suffix_if_multiple and num_entities > 1 then
add_with_sep(entities .. suffix_if_multiple)
else
add_with_sep(entities .. suffix_without_preceding_authors)
end
author_outputted = true
end
local function add_authorlabel()
local default_authorlabel = a("default-authorlabel")
if default_authorlabel and yesno(a("authorlabel"), true) then
sep = nil
add_with_sep(" " .. default_authorlabel)
end
end
local function has_new_title_or_author()
return args["2ndauthor"]
or args["2ndlast"]
or args.chapter2
or args.title2
or args.tlr2
or args.mainauthor2
or args.editor2
or args.editors2
or args.compiler2
or args.compilers2
or args.director2
or args.directors2
end
local function has_newversion()
return args.newversion or args.location2 or has_new_title_or_author()
end
-- Handle chapter=, section=, etc. `param` is the base name of the parameter in question, e.g. "chapter" or
-- "section". If numeric (either Arabic or Roman), add `numeric_prefix`; otherwise, parse as textual (allowing for
-- language prefixes, inline modifiers, etc.), prefix with `textual_prefix` (if given) and suffix with
-- `textual_suffix` (if given). Also checks for and handles the following (assuming param == "chapter"):
-- * chapterurl=: URL of the chapter.
-- * trans-chapter=: Chapter translation (can be given using an inline modifier <t:...>).
-- * chapter_number=: Chapter number, when chapter= is also given (otherwise put the chapter number in chapter=).
-- * chapter_plain=: Plain version of the chapter number; the "chapter " prefix isn't added.
-- * chapter_series=: Series that the chapter is within (used e.g. for journal articles part of a series).
-- * chapter_seriesvolume=: Volume of the series (compare seriesvolume=).
--
-- Returns nil if no value specified for the main parameter, otherwise the formatted value.
local function format_chapterlike(param, numeric_prefix, textual_prefix, textual_suffix)
local chap, chap_fullname = a_with_name(param)
local chap_num, chap_num_fullname = a_with_name(param .. "_number")
local chap_plain, chap_plain_fullname = parse_and_format_annotated_text_with_name(param .. "_plain")
if chap_num and chap_plain then
error(("Specify only one of |%s= or %s="):format(chap_num_fullname, chap_plain_fullname))
end
local chap_series, chap_series_fullname =
parse_and_format_annotated_text_with_name(param .. "_series", tag_with_cite, tag_with_cite)
local chap_seriesvolume, chap_seriesvolume_fullname =
parse_and_format_annotated_text_with_name(param .. "_seriesvolume")
if chap_series then
chap_series = ", " .. chap_series
end
if chap_seriesvolume then
if not chap_series then
error(("Cannot specify |%s= without %s="):format(chap_series_fullname, chap_seriesvolume_fullname))
end
chap_series = chap_series .. " (" .. chap_seriesvolume .. ")"
end
local chapterurl
local function make_chapter_with_url(chap)
if chapterurl then
return "[" .. chapterurl .. " " .. chap .. "]"
else
return chap
end
end
if not chap then
if chap_num then
error(
("Cannot specify |%s= without |%s=; put the numeric value in |%s= directly"):format(
chap_num_fullname,
chap_fullname,
chap_fullname
)
)
end
if chap_plain then
chapterurl = aurl(param .. "url")
return make_chapter_with_url(chap_plain .. (chap_series or ""))
end
return nil
end
local cleaned_chap = chap:gsub("<sup>[^<>]*</sup>", ""):gsub("[*+#]", "")
chapterurl = aurl(param .. "url")
local formatted
if numeric_prefix and get_number(cleaned_chap) then
-- Arabic chapter number
formatted = numeric_prefix .. make_chapter_with_url(chap)
elseif
numeric_prefix
and cleaned_chap:match("^[mdclxviMDCLXVI]+$")
and roman_to_arabic(cleaned_chap, true)
and (not overrides[param] or not overrides[param].noroman)
then
-- Roman chapter number
formatted = numeric_prefix .. make_chapter_with_url(upper(chap))
else
-- strip leading ! (used to force text-mode for titles that look like Roman numerals like "mil" and "dill")
if chap:find("^!") then
chap = chap:gsub("^!", "")
end
-- Must be a chapter name
local chapterobj = parse_annotated_text(chap, chap_fullname, a("trans-" .. param))
chapterobj.text = make_chapter_with_url(chapterobj.text)
chapterobj.link = make_chapter_with_url(chapterobj.link)
formatted = (textual_prefix or "") .. format_annotated_text(chapterobj) .. (textual_suffix or "")
end
if chap_num or chap_plain then
-- NOTE: Up above we throw an error if both chap_num and chap_plain are specified.
formatted = formatted .. " (" .. (chap_plain or numeric_prefix .. chap_num) .. ")"
end
if chap_series then
formatted = formatted .. chap_series
end
return formatted
end
-- This handles everything after displaying the author, starting with the chapter and ending with page, column,
-- line and then other=. It is currently called twice: Once to handle the main portion of the citation, and once to
-- handle a "newversion" citation. `ind` is either "" for the main portion or a number (currently only 2) for a
-- "newversion" citation. In a few places we conditionalize on `ind` to take actions depending on its value.
local function postauthor(ind, num_authors, format_as_cite)
get_full_paramname = make_get_full_paramname(ind)
if author_outputted then
add_authorlabel()
end
local coauthors = parse_and_format_multivalued_annotated_text("coauthors", "and")
if coauthors then
local with_prefix = ""
if author_outputted then
with_prefix = "dengan "
if num_authors == 1 then
sep = " "
end
end
add_with_sep(with_prefix .. coauthors)
author_outputted = true
end
add_authorlike("quotee", "memetik ", ", pemetik", ", pemetik")
if format_as_cite and author_outputted and not date_outputted then
add_date()
sep = ", "
end
add_authorlike("chapter_tlr", "diterjemah oleh ", ", penterjemah", nil, " penterjemah")
local function add_sg_and_pl_authorlike(noun, verbed)
local sgparam = noun
local plparam = noun .. "s"
local sgval, sgval_fullname = a_with_name(sgparam)
local plval, plval_fullname = a_with_name(plparam)
if sgval and plval then
error(("Can't specify both |%s= and |%s="):format(sgval_fullname, plval_fullname))
end
if sgval or plval then
local verbed_by = verbed .. " oleh "
local comma_sgnoun = ", " .. noun
local comma_plnoun = ", " .. noun .. "s"
add_authorlike(sgparam, verbed_by, comma_sgnoun, comma_plnoun)
add_authorlike(plparam, verbed_by, comma_plnoun)
end
end
local formatted_entry = format_chapterlike("entry", nil, "“", "”")
local formatted_chapter = format_chapterlike("chapter", "chapter ", "“", "”")
local function add_entry()
if formatted_entry then
add_with_sep(formatted_entry)
if not a("notitle") then
add("dalam ")
author_outputted = false
else
author_outputted = true
end
formatted_entry = nil
end
end
local function add_chapter()
add_entry()
if formatted_chapter then
add_with_sep(formatted_chapter)
if not a("notitle") then
add("dalam ")
author_outputted = false
end
formatted_chapter = nil
end
end
local function add_actor_role(format_as_cite)
local role = parse_and_format_multivalued_annotated_text("role", "and")
local actor_val, actor_fullname = a_with_name("actor")
local actor_objs = parse_multivalued_annotated_text(actor_val, actor_fullname)
local actor = format_multivalued_annotated_text(actor_objs, "and")
if format_as_cite then
if role then
if actor then
add_with_sep(actor .. " sebagai ")
end
sep = nil
add_with_sep(role)
elseif actor then
add_with_sep(actor .. " (" .. (#actor_objs > 1 and "actors" or "actor") .. ")")
end
else
if role then
add_with_sep("diucapkan oleh " .. role)
if actor then
sep = nil
add_with_sep(" (" .. actor .. ")")
end
elseif actor then
add_with_sep(actor .. " (" .. (#actor_objs > 1 and "actors" or "actor") .. ")")
end
end
end
if format_as_cite then
if date_outputted then
add_chapter()
end
local output_len = #output
local mainauthor = parse_and_format_multivalued_annotated_text("mainauthor")
if mainauthor then
add_with_sep(mainauthor)
end
-- quote-* templates display "jobbed by name" after the author, controlled by the author_outputted flag
author_outputted = false
add_authorlike("tlr", "diterjemah oleh ", ", penterjemah", nil, " penterjemah")
author_outputted = false
add_sg_and_pl_authorlike("editor", "edited")
add_sg_and_pl_authorlike("compiler", "compiled")
add_sg_and_pl_authorlike("director", "directed")
add_authorlike("lyricist", nil, " (lirik)", nil, " penulis lirik")
add_authorlike("lyrics-translator", nil, " (terjemahan)", nil, " penterjemah lirik")
add_authorlike("composer", nil, " (muzik)", nil, " penggubal")
add_actor_role("format_as_cite")
-- if the output length has changed, a credit name has been printed
-- and we can print the date
if output_len ~= #output then
author_outputted = true
add_date()
end
add_chapter()
else
add_chapter()
local mainauthor = parse_and_format_multivalued_annotated_text("mainauthor")
if mainauthor then
add_with_sep(mainauthor)
author_outputted = true
end
add_authorlike("tlr", "diterjemah oleh ", ", penterjemah", nil, " penterjemah")
add_sg_and_pl_authorlike("editor", "edited")
add_sg_and_pl_authorlike("compiler", "compiled")
add_sg_and_pl_authorlike("director", "directed")
add_authorlike("lyricist", nil, " (lirik)", nil, " penulis lirik")
add_authorlike("lyrics-translator", nil, " (terjemahan)", nil, " penterjemah lirik")
add_authorlike("composer", nil, " (muzik)", nil, " penggubal")
end
local title, title_fullname = a_with_name("title")
local need_comma = false
if title then
local titleobj = parse_annotated_text(title, title_fullname, a("trans-title"))
add(format_annotated_text(titleobj, tag_with_cite, tag_with_cite))
local series = parse_and_format_annotated_text("series")
if series then
add(" (" .. series)
local seriesvolume = parse_and_format_annotated_text("seriesvolume")
if seriesvolume then
add(SEMICOLON_SPACE .. seriesvolume)
end
add(")")
end
need_comma = true
elseif ind == "" then
if not a("notitle") then
add(maintenance_line("Please provide the book title or journal name"))
need_comma = true
end
end
local archiveurl, archiveurl_fullname = aurl_with_name("archiveurl")
local url, url_fullname = aurl_with_name("url")
local urls, urls_fullname = aurl_with_name("urls")
if url and urls then
error(("Supply only one of |%s= and |%s="):format(url_fullname, urls_fullname))
end
local function verify_title_supplied(url_name)
-- There are too many cases of this to throw an error at this time.
-- if not title then
-- error(("If |%s= is given, |%s= must also be supplied"):format(url_name, title_fullname))
-- end
end
if archiveurl or url then
verify_title_supplied(archiveurl and archiveurl_fullname or url_fullname)
sep = nil
add("‎<sup>[" .. (archiveurl or url) .. "]</sup>")
elseif urls then
verify_title_supplied(urls_fullname)
sep = nil
add("‎<sup>" .. urls .. "</sup>")
end
-- display (in Language) if language is provided and is not English and not overriden by termlang or worklang
if format_as_cite and ind == "" and not (args.termlang or args.worklang) and (args[1] or args.lang) then
local lang = get_first_lang(args[1] or args.lang)
if lang then
local langcode = lang:getCode()
if not (langcode == "und" or langcode == "ms") then
local langs = format_langs(args[1] or args.lang)
if langs then
add(" (dalam bahasa " .. langs .. ")")
end
end
end
end
if need_comma then
sep = ", "
end
local edition, edition_fullname = parse_and_format_annotated_text_with_name("edition")
local edition_plain, edition_plain_fullname = parse_and_format_annotated_text_with_name("edition_plain")
if edition and edition_plain then
error(("Supply only one of |%s= and |%s="):format(edition_fullname, edition_plain_fullname))
end
if edition then
add_with_sep("edisi " .. edition)
end
if edition_plain then
add_with_sep(edition_plain)
end
-- Display a numeric param such as page=, volume=, column=. For each `paramname`, four params are actually
-- recognized, e.g. for paramname == "page", the params page=, pages=, page_plain= and pageurl= are recognized
-- and checked (or the same with an index, e.g. page2=, pages2=, page_plain2= and pageurl2= respectively if
-- ind == "2"). Only one of the first three can be specified; an error results if more than one are given.
-- If none are given, the return value is nil; otherwise it is a string. The numeric spec is taken directly
-- from e.g. page_plain= if given; otherwise if e.g. pages= is given, or if page= is given and looks like a
-- combination of numbers (i.e. it has a hyphen or dash in it, a comma, or the word " and "), it is prefixed
-- by `singular_desc` + "s" (e.g. "pages "), otherwise it is prefixed by just `singular_desc` (e.g. "page ").
-- (As a special case, if either e.g. page=unnumbered or pages=unnumbered is given, the numeric spec is
-- "unnumbered page".) The resulting spec is returned directly unless e.g. pageurl= is given, in which case
-- it is linked to the specified URL. Note that any of the specs can be foreign text, e.g. foreign numbers
-- (including with optional inline modifiers), and such text is handled appropriately.
local function format_numeric_param(paramname, singular_desc)
local sgval = a_with_name(paramname)
local sgobj = parse_annotated_text(sgval, paramname)
local plparamname = paramname .. "s"
local plval = a_with_name(plparamname)
local plobj = parse_annotated_text(plval, plparamname)
local plainval, plain_fullname = parse_and_format_annotated_text_with_name(paramname .. "_plain")
local numspec
if not sgval and not plval and not plainval then
return
elseif plainval and (sgval or plval) then
error(("Can't specify " .. plain_fullname .. " with " .. paramname .. " or " .. plparamname))
elseif sgval and plval then
-- if both singular and plural, display "page 1 of 1-10"
numspec = singular_desc .. " " .. sgval .. " daripada " .. plval
else
-- Merge page= and pages= and treat alike because people often mix them up in both directions.
if plainval then
numspec = plainval
else
local val = sgobj and sgobj.text or plobj.text
if val == "unnumbered" then
numspec = "unnumbered " .. singular_desc
else
local desc
if val:find("^!") then
val = val:gsub("^!", "")
desc = sgval and singular_desc or pluralize(singular_desc)
else
local check_val = val
if check_val:find("%[") then
check_val = remove_links(check_val)
-- convert URL's of the form [URL DISPLAY] to the displayed value
check_val = check_val:gsub("%[[^ %[%]]* ([^%[%]]*)%]", "%1")
end
-- in case of negative page numbers (do they exist?), don't treat as multiple pages
check_val = check_val:gsub("^%-", "")
-- replace HTML entity en-dashes and em-dashes with their literal codes
check_val = check_val:gsub("–", "–")
check_val = check_val:gsub("–", "–")
check_val = check_val:gsub("—", "—")
check_val = check_val:gsub("—", "—")
-- Check for en-dash or em-dash, or two numbers (possibly with stuff after like 12a-15b)
-- separated by a hyphen or by comma a followed by a space (to avoid firing on thousands separators).
if
umatch(check_val, "[–—]")
or check_val:find(" and ")
or check_val:match("%d+[^ ]* *%- *%d+")
or check_val:match("%d+[^ ]* *, +%d+")
then
desc = pluralize(singular_desc)
else
desc = singular_desc
end
end
local obj = sgobj or plobj
obj.text = val
if obj.link:find("^!") then
obj.link = obj.link:gsub("^!", "")
end
val = format_annotated_text(obj)
numspec = desc .. " " .. val
end
end
end
local url = a(paramname .. "url")
if url then
return "[" .. url .. " " .. numspec .. "]"
else
return numspec
end
end
local volume = format_numeric_param("volume", a("volume_prefix") or "volume")
if volume then
add_with_sep(volume)
end
local issue = format_numeric_param("issue", a("issue_prefix") or "number")
if issue then
add_with_sep(issue)
end
-- number= is an alias for issue= (except in {{quote-av}}, where it is the episode number)
local number = format_numeric_param("number", a("number_prefix") or "number")
if number then
add_with_sep(number)
end
local annotations = {}
local genre = a("genre")
if genre then
insert(annotations, genre)
end
local format = a("format")
if format then
insert(annotations, format)
end
local medium = a("medium")
if medium then
insert(annotations, medium)
end
-- Now handle the display of language annotations like "(in French)" or
-- "(quotation in Nauruan; overall work in German)".
local quotelang = args[1] or args.lang
if not quotelang then
if ind == "" then
-- This can only happen for certain non-mainspace pages, e.g. Talk pages; otherwise an error is thrown
-- above.
insert(annotations, maintenance_line("Please specify the language of the quote using |1="))
else
-- do nothing in newversion= portion
end
elseif ind == "" then
local worklang = a("worklang")
local termlang = a("termlang")
worklang = worklang or quotelang
termlang = termlang or quotelang
if worklang == quotelang then
if worklang == termlang then
-- do nothing
else
insert(annotations, "dalam " .. format_langs(quotelang))
end
else
if quotelang ~= termlang then
insert(annotations, "petikan dalam bahasa " ..format_langs(quotelang))
end
insert(annotations, "karya secara keseluruhannya dalam bahasa " .. format_langs(worklang))
end
else
local lang2 = a("lang2")
if lang2 then
insert(annotations, "dalam " .. format_langs(lang2))
end
end
if #annotations > 0 then
sep = nil
add_with_sep(" (" .. concat(annotations, SEMICOLON_SPACE) .. ")")
end
local artist = parse_and_format_multivalued_annotated_text("artist", "and")
if artist then
add_with_sep("performed by " .. artist)
end
local feat = parse_and_format_multivalued_annotated_text("feat", "and")
if feat then
sep = " "
add_with_sep("ft. " .. feat)
end
if not format_as_cite then
add_actor_role()
end
local others = parse_and_format_annotated_text("others")
if others then
add_with_sep(others)
end
local quoted_in = parse_and_format_annotated_text("quoted_in", tag_with_cite, tag_with_cite)
if quoted_in then
add_with_sep("dipetik dalam " .. quoted_in)
insert(tracking_categories, "Quotations using quoted-in parameter")
end
local location = parse_and_format_multivalued_annotated_text("location", "; ")
local publisher = parse_and_format_multivalued_annotated_text("publisher", "; ")
if publisher then
if location then
add_with_sep(location) -- colon
sep = ": " -- colon
end
add_with_sep(publisher)
elseif location then
add_with_sep(location)
end
if not date_outputted then
add_date("no_paren")
end
local source = parse_and_format_multivalued_annotated_text("source", "and")
if source then
add_with_sep("sourced from " .. source)
end
local original = parse_and_format_annotated_text("original", tag_with_cite, tag_with_cite)
local by = parse_and_format_multivalued_annotated_text("by", "and")
local origtype = a("deriv") or "translation"
if original or by then
add_with_sep(origtype .. " of " .. (original or "original") .. (by and " by " .. by or ""))
end
-- Handle origlang=, origworklang=. How we handle them depends on whether the original title or author are explicitly
-- given.
local origlang = a("origlang")
local origworklang = a("origworklang")
local origlangtext, origworklangtext
if origlang then
origlangtext = "dalam " .. format_langs(origlang)
end
if origworklang then
origworklangtext = "overall work in " .. format_langs(origworklang)
end
if origlang or origworklang then
if original or by then
local orig_annotations = {}
if origlangtext then
insert(orig_annotations, origlangtext)
end
if origworklangtext then
insert(orig_annotations, origworklangtext)
end
sep = nil
add_with_sep(" (" .. concat(orig_annotations, SEMICOLON_SPACE) .. ")")
else
add_with_sep(origtype .. " of original" .. (origlangtext and " " .. origlangtext or ""))
if origworklangtext then
sep = nil
add_with_sep(" (" .. origworklangtext .. ")")
end
end
end
if ind ~= "" and has_newversion() then
local formatted_new_date = format_date_args(a, get_full_paramname, alias_map, "", "", nil, "Please provide a date or year", true)
if formatted_new_date then
add_with_sep(formatted_new_date)
end
end
-- Fetch date_published=/year_published=/month_published= and format appropriately.
local formatted_date_published = format_date_args(a, get_full_paramname, alias_map, "", "_published", nil, nil, true)
local platform = parse_and_format_multivalued_annotated_text("platform", "and")
if formatted_date_published then
add_with_sep("published " .. formatted_date_published .. (platform and " via " .. platform or ""))
elseif platform then
add_with_sep("via " .. platform)
end
-- From here on out, there should always be a preceding item, so we
-- can dispense with add_with_sep() and always insert the comma.
sep = nil
local function small(txt)
add(", <small>")
add(txt)
add("</small>")
end
-- Add an identifier to a book or article database such as DOI, ISBN, JSTOR, etc. `param_or_params`
-- is a string identifying the base param, or a list of such strings to check in turn. If found, the value
-- of the parameter is processed using `process` (a function of one argument, defaulting to mw.uri.encode()),
-- and then the actual URL to insert is generated by preceding with `pretext`, following with `posttext`,
-- and running the resulting string through small(), which first adds a comma and then the URL in small font.
local function add_identifier(param_or_params, pretext, posttext, process)
local val = a(param_or_params)
if val then
val = (process or mw.uri.encode)(val)
small(pretext .. val .. posttext)
end
end
add_identifier("bibcode", "[https://adsabs.harvard.edu/abs/", " →Bibcode]")
add_identifier("doi", '<span class="neverexpand">[https://doi.org/', " →DOI]</span>")
add_identifier("isbn", "", "", isbn)
add_identifier("issn", "", "", issn)
add_identifier("jstor", "[https://www.jstor.org/stable/", " →JSTOR]")
add_identifier("lccn", "", "", lccn)
add_identifier("oclc", "[https://search.worldcat.org/title/", " →OCLC]")
add_identifier("ol", "[https://openlibrary.org/works/OL", "/ →OL]")
add_identifier("pmid", "[https://www.ncbi.nlm.nih.gov/pubmed/", " →PMID]")
add_identifier("pmcid", "[https://www.ncbi.nlm.nih.gov/pmc/articles/", "/ →PMCID]")
add_identifier("ssrn", "[https://ssrn.com/abstract=", " →SSRN]")
-- add_identifier("urn", "", "", urn)
local id = a("id")
if id then
small(id)
end
archiveurl, archiveurl_fullname = aurl_with_name("archiveurl")
if archiveurl then
add(", diarkibkan daripada ")
local url, url_fullname = aurl_with_name("url")
if not url then
-- attempt to infer original URL from archive URL; this works at
-- least for Wayback Machine (web.archive.org) URL's
url = archiveurl:match("/(https?:.*)$")
if not url then
error(
("When |%s= is specified, |%s= must also be included"):format(archiveurl_fullname, url_fullname)
)
end
end
add("[" .. url .. " yang asal] on ")
local archivedate, archivedate_fullname = a_with_name("archivedate")
if archivedate then
add(format_date(archivedate))
elseif string.sub(archiveurl, 1, 28) == "https://web.archive.org/web/" then
-- If the archive is from the Wayback Machine, then it already contains the date
-- Get the date and format it
local wayback_date = string.sub(archiveurl, 29, 29 + 7)
wayback_date = string.sub(wayback_date, 1, 4)
.. "-"
.. string.sub(wayback_date, 5, 6)
.. "-"
.. string.sub(wayback_date, 7, 8)
add(format_date(wayback_date))
else
error(
("When |%s= is specified, |%s= must also be included"):format(
archiveurl_fullname,
archivedate_fullname
)
)
end
end
if a("accessdate") then
--Otherwise do not display here, as already used as a fallback for missing date= or year= earlier.
if (a("date") or a("nodate") or a("year")) and not a("archivedate") then
add(", retrieved " .. format_date(a("accessdate")))
end
end
local formatted_section = format_chapterlike("section", "section ")
if formatted_section then
add(", ")
add(formatted_section)
end
-- video game stuff
local system = parse_and_format_annotated_text("system")
if system then
add(", " .. system)
end
local scene = parse_and_format_annotated_text("scene")
if scene then
add(", scene: " .. scene)
end
local level = parse_and_format_annotated_text("level")
if level then
add(", level/area: " .. level)
end
local note = parse_and_format_annotated_text("note")
if note then
add(", " .. note)
end
local note_plain = parse_and_format_annotated_text("note_plain")
if note_plain then
add(" " .. note_plain)
end
-- Wrapper around format_numeric_param that inserts the formatted text with optional preceding text.
local function handle_numeric_param(paramname, singular_desc, pretext)
local numspec = format_numeric_param(paramname, singular_desc)
if numspec then
add((pretext or "") .. numspec)
end
end
handle_numeric_param("page", a("page_prefix") or "page", ", ")
handle_numeric_param("column", a("column_prefix") or "column", ", ")
handle_numeric_param("line", a("line_prefix") or "line", ", ")
-- FIXME: Does this make sense? What is other=?
local other = parse_and_format_annotated_text("other")
if other then
add(", " .. other)
end
end
local function add_authors(args, last_first)
-- Find maximum indexed author or last name.
local maxind = math.max(args.author.maxindex, args.last.maxindex)
-- Include max index of ancillary params so we get an error message about their use without the primary params.
local ancillary_params =
{"trans-author", "authorlink", "trans-authorlink", "first", "trans-first", "trans-last"}
for _, ancillary in ipairs(ancillary_params) do
maxind = math.max(maxind, args[ancillary].maxindex)
end
local num_authors = 0
for i = 1, maxind do
local ind = i == 1 and "" or i
local author, last = args.author[i], args.last[i]
if author or last then
local this_num_authors = add_author(
author,
"author" .. ind,
args["trans-author"][i],
"trans-author" .. ind,
args.authorlink[i],
"authorlink" .. ind,
args["trans-authorlink"][i],
"trans-authorlink" .. ind,
args.first[i],
"first" .. ind,
args["trans-first"][i],
"trans-first" .. ind,
last,
"last" .. ind,
args["trans-last"][i],
"trans-last" .. ind,
last_first
)
num_authors = num_authors + this_num_authors
sep = last_first and "; " or ", "
else
for _, cant_have in ipairs(ancillary_params) do
if args[cant_have][i] then
error(("Can't have |%s%s= without |author%s= or |last%s="):format(cant_have, ind, ind, ind))
end
end
end
end
return num_authors
end
local function add_newversion()
-- If there's a "newversion" section, add the new-version text.
if has_newversion() then
sep = nil
--Test for new version of work.
add(SEMICOLON_SPACE)
if args.newversion then -- newversion= is intended for English text, e.g. "quoted in" or "republished as".
add(args.newversion)
elseif not args.edition2 then
if has_new_title_or_author() then
add("diterbitkan semula sebagai")
else
add("diterbitkan semula")
end
end
add(" ")
return ""
else
return ", "
end
end
------------------- Now we start outputting text ----------------------
local need_comma = false
-- Set this now so a() works just below.
get_full_paramname = make_get_full_paramname("")
if args.brackets then
add("[")
end
bold_year = not format_as_cite
year_last = format_as_cite
formatted_date, need_date = format_date_args(
a,
get_full_paramname,
alias_map,
nil,
nil,
bold_year,
"Can we [[:Category:Requests for date|date]] this quote?",
year_last
)
-- Fetch origdate=/origyear=/origmonth= and format appropriately.
formatted_origdate = format_date_args(a, get_full_paramname, alias_map, "orig", nil, nil, nil, year_last)
local num_authors, need_date
if format_as_cite then
num_authors = add_authors(args, "last_first")
if author_outputted then
sep = " "
end
-- Display all the text that comes after the author, for the main portion.
postauthor("", num_authors, "format_as_cite")
author_outputted = false
sep = add_newversion()
-- Add the newversion author(s).
if args["2ndauthor"] or args["2ndlast"] then
num_authors = add_author(
args["2ndauthor"],
"2ndauthor",
nil,
nil,
args["2ndauthorlink"],
"2ndauthorlink",
nil,
nil,
args["2ndfirst"],
"2ndfirst",
nil,
nil,
args["2ndlast"],
"2ndlast",
nil,
nil,
"last_first"
)
sep = ", "
else
for _, cant_have in ipairs{"2ndauthorlink", "2ndfirst"} do
if args[cant_have] then
error(("Can't have |%s= without |2ndauthor= or |2ndlast="):format(cant_have))
end
end
end
-- Display all the text that comes after the author, for the "newversion" section.
postauthor(2, num_authors, "format_as_cite")
else
if formatted_date then
need_comma = true
add(formatted_date)
end
if formatted_origdate then
need_comma = true
add(SPACE_LBRAC .. formatted_origdate .. RBRAC)
end
if need_comma then
sep = ", "
end
date_outputted = true
num_authors = add_authors(args)
-- Display all the text that comes after the author, for the main portion.
postauthor("", num_authors)
author_outputted = false
sep = add_newversion()
-- Add the newversion author(s).
if args["2ndauthor"] or args["2ndlast"] then
num_authors = add_author(
args["2ndauthor"],
"2ndauthor",
nil,
nil,
args["2ndauthorlink"],
"2ndauthorlink",
nil,
nil,
args["2ndfirst"],
"2ndfirst",
nil,
nil,
args["2ndlast"],
"2ndlast",
nil,
nil
)
sep = ", "
else
for _, cant_have in ipairs{"2ndauthorlink", "2ndfirst"} do
if args[cant_have] then
error(("Can't have |%s= without |2ndauthor= or |2ndlast="):format(cant_have))
end
end
end
-- Display all the text that comes after the author, for the "newversion" section.
postauthor(2, num_authors)
end
if not args.nocolon then
sep = nil
add(":")
elseif args.usenodot and not args.nodot then
add(".")
end
-- Concatenate output portions to form output text.
local output_text = concat(output)
-- Remainder of code handles adding categories. We add one or more of the following categories:
--
-- 1. [[Category:LANG terms with quotations]], based on the first language code in termlang= or 1=. Added to
-- mainspace, Reconstruction: and Appendix: pages as well as Citations: pages if the corresponding mainspace
-- page exists. Not added if nocat= is given. Note that [[Module:usex]] adds the same category using the same
-- logic, but we do it here too because we may not have a quotation to format. (We add in those circumstances
-- because typically when there's no quotation to format, it's because it's formatted manually underneath the
-- citation, or using {{ja-x}}, {{th-x}} or similar.)
-- 2. [[Category:Requests for date in LANG entries]], based on the first language code in 1=. Added to mainspace,
-- Reconstruction:, Appendix: and Citations: pages unless nocat= is given.
-- 3. [[Category:Quotations using nocat parameter]], if nocat= is given. Added to mainspace, Reconstruction:,
-- Appendix: and Citations: pages.
local categories = {}
local termlang = get_first_lang(args.termlang or argslang)
if args.nocat then
if format_as_cite then
insert(tracking_categories, "Citations using nocat parameter")
else
insert(tracking_categories, "Quotations using nocat parameter")
end
else
-- don't create language tracking categories for citations
if not format_as_cite then
local title
if args.pagename then -- for testing, doc pages, etc.
title = mw.title.new(args.pagename)
if not title then
error(("Bad value for `args.pagename`: '%s'"):format(args.pagename))
end
else
title = mw.title.getCurrentTitle()
end
-- Only add [[Citations:foo]] to [[:Category:LANG terms with quotations]] if [[foo]] exists.
local ok_to_add_cat
if title.nsText ~= "Citations" then
ok_to_add_cat = true
else
local mainspace_title = mw.title.new(title.text)
if mainspace_title and mainspace_title.exists then
ok_to_add_cat = true
end
end
if ok_to_add_cat then
insert(categories, termlang:getFullName() .. " terms with quotations")
end
end
if need_date then
local argslangobj = get_first_lang(argslang)
insert(categories, "Requests for date in " .. argslangobj:getCanonicalName() .. " entries")
end
end
return output_text .. (
not lang and "" or
(#categories > 0 and format_categories(categories, lang, args.sort) or "") .. (
#tracking_categories > 0
and format_categories(
tracking_categories,
lang,
args.sort,
nil,
not page_should_be_ignored(mw.title.getCurrentTitle().fullText)
)
or ""
)
)
end
-- Alias specs for type= and type2=. Each spec is `{canon, aliases, with_newversion}` where `canon` is the canonical
-- parameter (with "2" added if type2= is being handled), `aliases` is a comma-separated string of aliases (with "2"
-- added if type2= is being handled, except for numeric params), and `with_newversion` indicates whether we should
-- process this spec if type2= is being handled.
local type_alias_specs = {
av = {
{"author", "writer,writers", true},
{"chapter", "episode", true},
{"chapterurl", "episodeurl", true},
{"trans-chapter", "trans-episode", true},
{"chapter_tlr", "episode_tlr,episode_tlrs,episode_translator,episode_translators", true},
{"chapter_series", "episode_series", true},
{"chapter_seriesvolume", "episode_seriesvolume", true},
{"chapter_number", "episode_number", true},
{"chapter_plain", "episode_plain", true},
{"volume", "season", true},
{"volumes", "seasons", true},
{"volume_plain", "season_plain", true},
{"volumeurl", "seasonurl", true},
{"platform", "network", true},
},
book = {
{"author", "3"},
{"title", "4"},
{"url", "5"},
{"year", "2"},
{"page", "6"},
{"text", "7"},
{"t", "8"},
},
journal = {
{"year", "2"},
{"author", "3"},
{"chapter", "title,article,4", true},
{"chapterurl", "titleurl,articleurl", true},
{"trans-chapter", "trans-title,trans-article", true},
{"chapter_tlr", "title_tlr,title_tlrs,title_translator,title_translators,article_tlr,article_tlrs,article_translator,article_translators", true},
{"chapter_series", "title_series,article_series", true},
{"chapter_seriesvolume", "title_seriesvolume,article_seriesvolume", true},
{"chapter_number", "title_number,article_number", true},
{"chapter_plain", "title_plain,article_plain", true},
{"title", "journal,magazine,newspaper,work,5", true},
{"trans-title", "trans-journal,trans-magazine,trans-newspaper,trans-work", true},
{"tlr", "journal_tlr,journal_tlrs,journal_translator,journal_translators,magazine_tlr,magazine_tlrs,magazine_translator,magazine_translators,newspaper_tlr,newspaper_tlrs,newspaper_translator,newspaper_translators,work_tlr,work_tlrs,work_translator,work_translators", true},
{"url", "6,journalurl,magazineurl,newspaperurl,workurl", true},
{"page", "7"},
{"source", "newsagency,news_agency", true},
{"text", "8"},
{"t", "9"},
},
}
-- Process interally-handled aliases related to type= or type2=. `args` is a table of arguments; `typ` is the value of
-- type= or type2=; newversion=true if we're dealing with type2=; alias_map is used to keep track of alias mappings
-- seen.
local function process_type_aliases(args, typ, newversion, alias_map)
local ind = newversion and "2" or ""
local deprecated = ine(args.lang)
if not type_alias_specs[typ] then
local possible_values = {}
for possible, _ in pairs(type_alias_specs) do
insert(possible_values, possible)
end
sort(possible_values)
error(
("Unrecognized value '%s' for type%s=; possible values are %s"):format(
typ,
ind,
concat(possible_values, ",")
)
)
end
for _, alias_spec in ipairs(type_alias_specs[typ]) do
local canon, aliases, with_newversion = unpack(alias_spec)
if with_newversion or not newversion then
canon = canon .. ind
aliases = split(aliases, ",", true)
local saw_alias = nil
for _, alias in ipairs(aliases) do
if alias:match("^%d+$") then
alias = tonumber(alias)
if deprecated then
alias = alias - 1
end
else
alias = alias .. ind
end
if args[alias] then
if saw_alias == nil then
saw_alias = alias
else
error(("|%s= and |%s= are aliases; cannot specify a value for both"):format(saw_alias, alias))
end
end
end
if saw_alias and (not newversion or type(saw_alias) == "string") then
if args[canon] then
error(("|%s= is an alias of |%s=; cannot specify a value for both"):format(saw_alias, canon))
end
args[canon] = args[saw_alias]
-- Wipe out the original after copying. This is important in case of a param that has general significance
-- but has been redefined (e.g. {{quote-av}} redefines number= for the episode number, and
-- {{quote-journal}} redefines title= for the chapter= (article). It's also important due to unhandled
-- parameter checking.
args[saw_alias] = nil
alias_map[canon] = saw_alias
end
end
end
end
-- Clone and combine frame's and parent's args while also assigning nil to empty strings. Handle aliases and ignores.
local function clone_args(direct_args, parent_args)
local args = {}
-- Processing parent args must come first so that direct args override parent args. Note that if a direct arg is
-- specified but is blank, it will still override the parent arg (with nil).
for pname, param in pairs(parent_args) do
-- [[Special:WhatLinksHere/Wiktionary:Tracking/quote/param/PARAM]]
track("param/" .. pname)
args[pname] = ine(param)
end
-- Process ignores. The value of `ignore` is a comma-separated list of parameter names to ignore (erase). We need to
-- do this before aliases due to {{quote-song}}, which sets chapter= to the value of title= in the direct params and
-- sets title= to the value of album= using an alias. If we do the ignores after aliases, we get an error during alias
-- processing, saying that title= and its alias album= are both present.
local ignores = ine(direct_args.ignore)
if ignores then
for ignore in gsplit(ignores, "%s*,%s*") do
args[ignore] = nil
end
end
local alias_map = {}
local other_controls = {}
-- noroman
local noroman = ine(direct_args.noroman)
if noroman then
other_controls.overrides = other_controls.overrides or {}
for param in gsplit(noroman, "%s*,%s*") do
other_controls.overrides[param] = (other_controls.overrides[param] or {})
other_controls.overrides[param].noroman = true
end
end
-- Process internally-specified aliases using type= or type2=.
local typ = args.type or direct_args.type
if typ then
process_type_aliases(args, typ, false, alias_map)
end
local typ2 = args.type2 or direct_args.type2
if typ2 then
process_type_aliases(args, typ2, true, alias_map)
end
-- Process externally-specified aliases. The value of `alias` is a list of semicolon-separated specs, each of which
-- is of the form DEST:SOURCE,SOURCE,... where DEST is the canonical name of a parameter and SOURCE refers to an
-- alias. Whitespace is allowed between all delimiters. The order of aliases may be important. For example, for
-- {{quote-journal}}, title= contains the article name and is an alias of underlying chapter=, while journal= or
-- work= contains the journal name and is an alias of underlying title=. As a result, the title -> chapter alias
-- must be specified before the journal/work -> title alias.
--
-- Whenever we copy a value from argument SOURCE to argument DEST, we record an entry for the pair in alias_map, so
-- that when we would display an error message about DEST, we display SOURCE instead.
--
-- Do alias processing (and ignore and error_if processing) before processing direct_args so that e.g. we can set up
-- an alias of title -> chapter and then set title= to something else in the direct args ({{quote-hansard}} does
-- this).
--
-- FIXME: Delete this once we've converted all alias processing to internal.
local aliases = ine(direct_args.alias)
if aliases then
-- Allow and discard a trailing semicolon, to make managing multiple aliases easier.
aliases = ugsub(aliases, "%s*;$", "")
for alias_spec in gsplit(aliases, "%s*;%s*") do
local alias_spec_parts = split(alias_spec, "%s*:%s*")
if #alias_spec_parts ~= 2 then
error(("Alias spec '%s' should have one colon in it"):format(alias_spec))
end
local dest, sources = unpack(alias_spec_parts)
sources = split(sources, "%s*,%s*")
local saw_source = nil
for _, source in ipairs(sources) do
if source:match("^%d+$") then
source = tonumber(source)
end
if args[source] then
if saw_source == nil then
saw_source = source
else
error(("|%s= and |%s= are aliases; cannot specify a value for both"):format(saw_source, source))
end
end
end
if saw_source then
if args[dest] then
error(("|%s= is an alias of |%s=; cannot specify a value for both"):format(saw_source, dest))
end
args[dest] = args[saw_source]
-- Wipe out the original after copying. This important in case of a param that has general significance
-- but has been redefined (e.g. {{quote-av}} redefines number= for the episode number, and
-- {{quote-journal}} redefines title= for the chapter= (article). It's also important due to unhandled
-- parameter checking.
args[saw_source] = nil
alias_map[dest] = saw_source
end
end
end
-- Process error_if. The value of `error_if` is a comma-separated list of parameter names to throw an error if seen
-- in parent_args (they are params we overwrite in the direct args).
local error_ifs = ine(direct_args.error_if)
if error_ifs then
for error_if in gsplit(error_ifs, "%s*,%s*") do
if ine(parent_args[error_if]) then
error(
("Cannot specify a value |%s=%s as it would be overwritten or ignored"):format(
error_if,
ine(parent_args[error_if])
)
)
end
end
end
for pname, param in pairs(direct_args) do
-- ignore control params
if pname ~= "ignore" and pname ~= "alias" and pname ~= "error_if" and pname ~= "noroman" then
args[pname] = ine(param)
end
end
return args, alias_map, other_controls
end
local function get_args(frame_args, parent_args, require_lang)
-- FIXME: We are processing arguments twice, once in clone_args() and then again in [[Module:parameters]]. This is
-- wasteful of memory.
local cloned_args, alias_map, other_controls = clone_args(frame_args, parent_args)
local deprecated = ine(parent_args.lang)
local alias_of_t = {alias_of = "t"}
local boolean = {type = "boolean"}
local language_sublist = {type = "language", sublist = true}
local list_allow_holes = {list = true, allow_holes = true}
local script = {type = "script"}
-- First, the "single" params that don't have FOO2 or FOOn versions.
local params = {
[deprecated and "lang" or 1] = {required = require_lang, type = "language", sublist = true, default = "und"},
["lang2"] = language_sublist,
["newversion"] = true,
["author"] = list_allow_holes,
["2ndauthor"] = true,
["trans-author"] = list_allow_holes,
["authorlink"] = list_allow_holes,
["2ndauthorlink"] = true,
["trans-authorlink"] = list_allow_holes,
["first"] = list_allow_holes,
["2ndfirst"] = true,
["trans-first"] = list_allow_holes,
["last"] = list_allow_holes,
["2ndlast"] = true,
["trans-last"] = list_allow_holes,
["nocat"] = boolean,
["nocolon"] = boolean,
-- quote params
text = true,
passage = {alias_of = "text"},
tr = true,
transliteration = {alias_of = "tr"},
ts = true,
transcription = {alias_of = "ts"},
norm = true,
normalization = {alias_of = "norm"},
sc = script,
normsc = script,
sort = true,
subst = true,
footer = true,
lit = true,
t = true,
translation = alias_of_t,
gloss = alias_of_t,
lb = true,
brackets = boolean,
-- original quote params
origtext = true,
origtr = true,
origts = true,
orignorm = true,
origsc = script,
orignormsc = script,
origsubst = true,
origlb = true,
usenodot = boolean,
nodot = boolean,
inline = boolean,
asquote = boolean,
ascite = boolean,
}
-- Most params have PARAM2 variants for use with `newversion`. FIXME: We should generalize this to
-- allow PARAMN variants for any N.
local function add_with_2(param, value)
params[param] = value
params[param .. "2"] = value
end
local function alias_with_2(alias, canon)
params[alias] = {alias_of = canon}
params[alias .. "2"] = {alias_of = canon .. "2"}
end
for _, param12 in ipairs{
"worklang",
"termlang",
"origlang",
"origworklang"
} do
add_with_2(param12, language_sublist)
end
-- Then the newversion params (which have FOO2 versions).
for _, param12 in ipairs{
-- author-like params; author params themselves are either list params (author=, last=, etc.) or single params
-- (2ndauthor=, 2ndlast=, etc.)
"coauthors",
"quotee",
"tlr",
"editor",
"editors",
"mainauthor",
"compiler",
"compilers",
"director",
"directors",
"lyricist",
"lyrics-translator",
"composer",
"role",
"actor",
"artist",
"feat",
-- author control params
"default-authorlabel",
"authorlabel",
-- title
"title",
"trans-title",
"series",
"seriesvolume",
"notitle",
-- entry
"entry",
"entryurl",
"trans-entry",
-- chapter
"chapter",
"chapterurl",
"chapter_number",
"chapter_plain",
"chapter_series",
"chapter_seriesvolume",
"trans-chapter",
"chapter_tlr",
-- section
"section",
"sectionurl",
"section_number",
"section_plain",
"section_series",
"section_seriesvolume",
"trans-section",
-- other video-game params
"system",
"scene",
"level",
-- URL
"url",
"urls",
"archiveurl",
-- edition
"edition",
"edition_plain",
-- ID params
"bibcode",
"doi",
"isbn",
"issn",
"jstor",
"lccn",
"oclc",
"ol",
"pmid",
"pmcid",
"ssrn",
"urn",
"id",
-- misc date params; most date params handled below
"archivedate",
"accessdate",
"nodate",
-- numeric params handled below
-- other params
"type",
"genre",
"format",
"medium",
"others",
"quoted_in",
"location",
"publisher",
"original",
"by",
"deriv",
"note",
"note_plain",
"other",
"source",
"platform",
} do
add_with_2(param12, true)
end
-- Then the aliases of newversion params (which have FOO2 versions).
for _, param12_aliased in ipairs{
{"role", "roles"},
{"role", "speaker"},
{"tlr", "tlrs"},
{"tlr", "translator"},
{"tlr", "translators"},
{"chapter_tlr", "chapter_tlrs"},
{"chapter_tlr", "chapter_translator"},
{"chapter_tlr", "chapter_translators"},
{"doi", "DOI"},
{"isbn", "ISBN"},
{"issn", "ISSN"},
{"jstor", "JSTOR"},
{"lccn", "LCCN"},
{"oclc", "OCLC"},
{"ol", "OL"},
{"pmid", "PMID"},
{"pmcid", "PMCID"},
{"ssrn", "SSRN"},
{"urn", "URN"},
} do
local canon, alias = unpack(param12_aliased)
alias_with_2(alias, canon)
end
-- Then the date params.
for _, datelike in ipairs{{"", ""}, {"orig", ""}, {"", "_published"}} do
local pref, suf = unpack(datelike)
for _, arg in ipairs{"date", "year", "month", "start_date", "start_year", "start_month"} do
add_with_2(pref .. arg .. suf, true)
end
end
local numeric_param_suffixes = {"", "s", "_plain", "url", "_prefix"}
-- Then the numeric params.
for _, numeric in ipairs{"volume", "issue", "number", "line", "page", "column"} do
for _, suf in ipairs(numeric_param_suffixes) do
add_with_2(numeric .. suf, true)
end
end
-- And the aliases of numeric params.
for _, numeric_aliased in ipairs{{"volume", "vol"}} do
local canon, alias = unpack(numeric_aliased)
for _, suf in ipairs(numeric_param_suffixes) do
alias_with_2(alias .. suf, canon .. suf)
end
end
return process_params(cloned_args, params), alias_map, other_controls
end
local function get_origtext_params(args)
local origtext, origtextlang, origsc, orignormsc
if args.origtext then
-- Wiktionary language codes have at least two lowercase letters followed possibly by lowercase letters and/or
-- hyphens (there are more restrictions but this is close enough). Also check for nonstandard Latin etymology
-- language codes (e.g. VL. or LL.). (There used to be more nonstandard codes but they have all been
-- eliminated.)
origtextlang, origtext = args.origtext:match("^(%l%l[%l-]*):([^ ].*)$")
if not origtextlang then
-- Special hack for Latin variants, which can have nonstandard etym codes, e.g. VL., LL.
origtextlang, origtext = args.origtext:match("^(%uL%.):([^ ].*)$")
end
if not origtextlang then
error("origtext= should begin with a language code prefix")
end
origtextlang = get_lang(origtextlang, nil, "allow etym") or
error("origtext= should begin with a language code prefix")
origsc = args.origsc
orignormsc = args.orignormsc
else
for _, noparam in ipairs{"origtr", "origts", "origsc", "orignorm", "orignormsc", "origsubst", "origlb"} do
if args[noparam] then
error(("Cannot specify %s= without origtext="):format(noparam))
end
end
end
return origtext, origtextlang, origsc, orignormsc
end
local function get_quote(args, is_cite)
local text = args.text
local gloss = args.t
local tr = args.tr
local ts = args.ts
local norm = args.norm
local sc = args.sc
local normsc = args.normsc
-- Fetch original-text parameters.
local origtext, origtextlang, origsc, orignormsc = get_origtext_params(args)
-- If any quote-related args are present, display the actual quote; otherwise, display nothing.
if text or gloss or tr or ts or norm or args.origtext then
-- Pass "und" here rather than cause an error; there will be an error on mainspace, Citations, etc. pages
-- in any case in source() if the language is omitted.
local lang = get_first_lang(args[1] or args.lang)
local termlang = args.termlang and get_first_lang(args.termlang) or lang
local usex_data = {
lang = lang,
termlang = termlang,
usex = text,
sc = sc,
translation = gloss,
normalization = norm,
normsc = normsc,
transliteration = tr,
transcription = ts,
brackets = args.brackets,
subst = args.subst,
lit = args.lit,
footer = args.footer,
qq = parse_and_format_labels(args.lb, lang),
quote = "quote-meta",
orig = origtext,
origlang = origtextlang,
origsc = origsc,
orignorm = args.orignorm,
orignormsc = orignormsc,
origtr = args.origtr,
origts = args.origts,
origsubst = args.origsubst,
origqq = parse_and_format_labels(args.origlb, lang),
noreq = args.noreq,
nocat = is_cite or args.nocat,
}
if args.inline then
-- don't let usex format the footer, otherwise it gets inlined with the rest of the quoted text
usex_data.footer = nil
usex_data.inline = 1
text = format_usex(usex_data)
if text then
text = " “" .. text .. "”"
else
text = ""
end
if args.footer then
text = text .. "<dl><dd>" .. args.footer .. "</dd></dl>"
end
else
text = "<dl><dd>" .. format_usex(usex_data) .. "</dd></dl>"
end
elseif args.footer then
text = "<dl><dd>" .. args.footer .. "</dd></dl>"
end
return text
end
-- External interface, meant to be called from a template. Replaces {{quote-meta}} and meant to be the primary
-- interface for {{quote-*}} templates.
function export.quote_t(frame)
local args, alias_map, other_controls = get_args(frame.args, frame:getParent().args, "require_lang")
if args.ascite then
return export.cite_t(frame)
end
return show_quote(args, alias_map, other_controls)
end
function show_quote(args, alias_map, other_controls)
local parts = {}
insert(parts, '<div class="citation-whole"><span class="cited-source">')
insert(parts, export.source(args, alias_map, nil, other_controls))
insert(parts, "</span>")
insert(parts, get_quote(args))
insert(parts, "</div>")
local retval = concat(parts)
return deprecated and frame:expandTemplate({
title = "check deprecated lang param usage",
args = {retval, lang = args.lang},
}) or retval
end
-- External interface, meant to be called from a template. Replaces {{cite-meta}} and meant to be the primary
-- interface for {{cite-*}} templates.
function export.cite_t(frame)
local parent_args = {}
for k, v in pairs(frame:getParent().args) do
parent_args[k] = v
end
-- use "und" as lang if none provided
if parent_args[1] == nil then
parent_args[1] = "und"
end
local args, alias_map, other_controls = get_args(frame.args, parent_args)
if args.asquote then
return show_quote(args, alias_map, other_controls)
end
return show_cite(args, alias_map, other_controls)
end
function show_cite(args, alias_map, other_controls)
local parts = {}
-- don't nag for translations
if args.text and not args.t then
args.noreq = 1
end
local len_visible = args.text and ulen((args.text:gsub("<[^<>]+>", ""))) or 0
if len_visible == 0 then
if not args.t or args.t == "-" then
args.nocolon = true
end
elseif args.inline == nil then
args.inline = not (
args.block_text
or len_visible > 300
or (args.t and not args.t == "-" and len_visible < 80)
or string.match(args.text, "<br>")
)
end
insert(parts, '<span class="citation-whole"><span class="cited-source">')
insert(parts, export.source(args, alias_map, "format_as_cite", other_controls))
insert(parts, "</span>")
insert(parts, get_quote(args, "is_cite"))
insert(parts, "</span>")
local retval = concat(parts)
return deprecated
and frame:expandTemplate({
title = "check deprecated lang param usage",
args = {retval, lang = args.lang},
})
or retval
end
-- External interface, meant to be called from a template.
function export.call_quote_template(frame)
return export.call_template(frame)
end
-- External interface, for calling a template with overloaded parameters.
function export.overload(frame)
local iargs, overloaded_args = process_params(frame.args, {
[1] = {required = true}
}, true)
local parent_args = frame:getParent().args
for k, v in pairs(parent_args) do
overloaded_args[k] = v
end
return frame:expandTemplate{title = iargs[1], args = overloaded_args}
end
-- External interface, meant to be called from a template.
function export.call_template(frame)
local parameter_sublist = {type = "parameter", sublist = true}
local iargs, other_direct_args = process_params(frame.args, {
["template"] = true,
["textparam"] = parameter_sublist,
["pageparam"] = parameter_sublist,
["propagateparams"] = parameter_sublist,
["allowparams"] = {sublist = true}, -- Doesn't use type = "parameter", because any that end in :list get processed differently.
}, true)
local function fetch_param(source, params)
for _, param in ipairs(params) do
if source[param] then
return source[param]
end
end
return nil
end
local params = {
["text"] = true,
["passage"] = true,
["footer"] = true, -- always propagates, but gets special formatting
}
-- paramaters that can be used during template declaration or included in template calls
-- if the template is called with param=- it will set any declared value to nil
local always_propagate = {
["brackets"] = true,
["usenodot"] = true,
["nodot"] = true,
["ascite"] = true,
["asquote"] = true,
}
for k,v in pairs(always_propagate) do
params[k] = v
end
local textparam = iargs.textparam or {}
for _, param in ipairs(textparam) do
params[param] = true
end
local pageparam = iargs.pageparam or {}
local pageparam1 = pageparam[1]
if pageparam1 ~= nil then
params["page"], params["pages"] = true, true
for _, param in ipairs(pageparam) do
params[param] = true
end
end
local allowparams, allow_all, list = iargs.allowparams, false
if allowparams ~= nil then
for _, allow in ipairs(allowparams) do
local param = allow:match("^(.*):list$")
if param then
if list == nil then
list = {list = true}
end
params[scribunto_parameter_key(param)] = list
elseif allow == "*" then
track("no parameter checking")
allow_all = true
else
params[scribunto_parameter_key(allow)] = true
end
end
else
local parent = frame:getParent()
local template_name = parent:getTitle()
local template_title = new_title(template_name)
local content = template_title:getContent()
local allowed_params, seen = {}, {}
-- Detect all params used by the parent template. param:get_name() takes the
-- parent frame arg table as an argument so that preprocessing will take
-- them into account, since it will matter if the name contains another
-- parameter (e.g. the outer param in "{{{foo{{{bar}}}baz}}}" will change
-- depending on the value for bar=). `seen` memoizes results based on the
-- raw parameter text (which is stored as a string in the parameter object),
-- which avoids unnecessary param:get_name() calls, which are non-trivial.
for param in find_parameters(content) do
local raw = param.raw
if not seen[raw] then
allow = param:get_name(template_args)
params[scribunto_parameter_key(allow)] = true
seen[raw] = true
end
end
end
local propagateparams = iargs.propagateparams or {}
for _, param in ipairs(propagateparams) do
params[param] = true
end
local parent_args = frame:getParent().args
local args = process_params(parent_args, params, allow_all)
parent_args = shallow_copy(parent_args)
if textparam[1] ~= "-" then
other_direct_args.passage = args.text or args.passage or fetch_param(args, textparam)
end
if not (pageparam1 == nil or pageparam1 == "-") then
other_direct_args.page = fetch_param(args, pageparam) or args.page or other_direct_args.page or nil
other_direct_args.pages = args.pages or other_direct_args.pages or nil
if other_direct_args.page == "-" then
other_direct_args.page = nil
end
if other_direct_args.pages == "-" then
other_direct_args.pages = nil
end
end
if args.footer then
other_direct_args.footer = frame:expandTemplate{title = "small", args = {args.footer}}
end
for param, _ in pairs(always_propagate) do
if args[param] == "-" then
other_direct_args[param] = nil
else
other_direct_args[param] = args[param] or other_direct_args[param] or nil
end
end
-- authorlink=- can be used to prevent copying of author= to authorlink= but we don't want to propagate this to
-- the actual {{quote-*}} code.
if other_direct_args.authorlink == "-" then
other_direct_args.authorlink = nil
end
for _, param in ipairs(propagateparams) do
if args[param] then
other_direct_args[param] = args[param]
end
end
return frame:expandTemplate{title = iargs.template or "quote-book", args = other_direct_args}
end
local paramdoc_param_replacements = {
passage = {
param_with_synonym = "<<synonym>>, {{para|text}}, or {{para|passage}}",
param_no_synonym = "{{para|text}} or {{para|passage}}",
text = [=[
* <<params>> – the passage to be quoted.]=],
},
page = {
param_with_synonym = "<<synonym>> or {{para|page}}, or {{para|pages}}",
param_no_synonym = "{{para|page}} or {{para|pages}}",
text = [=[
* <<params>> – '''mandatory in some cases''': the page number(s) quoted from. When quoting a range of pages, note the following:
** Separate the first and last pages of the range with an [[en dash]], like this: {{para|pages|10–11}}.
** You must also use {{para|pageref}} to indicate the page to be linked to (usually the page on which the Wiktionary entry appears).
: This parameter must be specified to have the template link to the online version of the work.]=],
},
page_with_roman_preface = {
param_with_synonym = {"inherit", "page"},
param_no_synonym = {"inherit", "page"},
text = [=[
* <<params>> – '''mandatory in some cases''': the page number(s) quoted from. If quoting from the preface, specify the page number(s) in lowercase Roman numerals. When quoting a range of pages, note the following:
** Separate the first and last page number of the range with an [[en dash]], like this: {{para|pages|10–11}} or {{para|pages|iii–iv}}.
** You must also use {{para|pageref}} to indicate the page to be linked to (usually the page on which the Wiktionary entry appears).
: This parameter must be specified to have the template link to the online version of the work.]=],
},
chapter = {
param_with_synonym = "<<synonym>> or {{para|chapter}}",
param_no_synonym = "{{para|chapter}}",
text = [=[
* <<params>> – the name of the chapter quoted from.]=],
},
roman_chapter = {
param_with_synonym = {"inherit", "chapter"},
param_no_synonym = {"inherit", "chapter"},
text = [=[
* <<params>> – the chapter number quoted from in uppercase Roman numerals.]=],
},
arabic_chapter = {
param_with_synonym = {"inherit", "chapter"},
param_no_synonym = {"inherit", "chapter"},
text = [=[
* <<params>> – the chapter number quoted from in Arabic numerals.]=],
},
trailing_params = {
text = [=[
* {{para|footer}} – a comment on the passage quoted.
* {{para|brackets}} – use {{para|brackets|on}} to surround a quotation with [[bracket#Noun|brackets]]. This indicates that the quotation either contains a mere mention of a term (for example, “some people find the word '''''manoeuvre''''' hard to spell”) rather than an actual use of it (for example, “we need to '''manoeuvre''' carefully to avoid causing upset”), or does not provide an actual instance of a term but provides information about related terms.]=],
},
}
function export.paramdoc(frame)
local parargs = frame:getParent().args
local args = process_params(parargs, {
[1] = true,
})
local text = args[1]
local function do_param_with_optional_synonym(param, text_to_sub, paramtext_synonym, paramtext_no_synonym)
local function sub_param(synonym)
local subbed_paramtext
if synonym then
subbed_paramtext = paramtext_synonym:gsub("<<synonym>>", "{{para|" .. replacement_escape(synonym) .. "}}")
else
subbed_paramtext = paramtext_no_synonym
end
return frame:preprocess((text_to_sub:gsub("<<params>>", replacement_escape(subbed_paramtext))))
end
text = text:gsub("<<" .. pattern_escape(param) .. ">>", function()
return sub_param()
end)
text = text:gsub("<<" .. pattern_escape(param) .. ":(.-)>>", sub_param)
end
local function fetch_text(param_to_replace, key)
local spec = paramdoc_param_replacements[param_to_replace]
local val = spec[key]
if type(val) == "string" then
return val
end
if type(val) == "table" and val[1] == "inherit" then
return fetch_text(val[2], key)
end
error(
"Internal error: Unrecognized value for param '"
.. param_to_replace
.. "', key '"
.. key
.. "': "
.. mw.dumpObject(val)
)
end
for param_to_replace, spec in pairs(paramdoc_param_replacements) do
if not spec.param_no_synonym then
-- Text to substitute directly.
text = text:gsub("<<" .. pattern_escape(param_to_replace) .. ">>", function()
return frame:preprocess(fetch_text(param_to_replace, "text"))
end)
else
do_param_with_optional_synonym(
param_to_replace,
fetch_text(param_to_replace, "text"),
fetch_text(param_to_replace, "param_with_synonym"),
fetch_text(param_to_replace, "param_no_synonym")
)
end
end
-- Remove final newline so template code can add a newline after invocation
text = text:gsub("\n$", "")
return text
end
return export
18jmq7ar05tofez8bfn0lzkeo338f3n
375852
375851
2026-09-25T09:21:24Z
SNN95
2113
375852
Scribunto
text/plain
--[=[
This module contains functions to implement quote-* templates.
Author: Benwing2; conversion into Lua of {{quote-meta/source}} template,
written by Sgconlaw with some help from Erutuon and Benwing2.
The main interface is quote_t(). Note that the source display is handled by source(), which reads both the
arguments passed to it *and* the arguments passed to the parent template, with the former overriding the latter.
]=]
local export = {}
--[=[
FIXME:
1. Dates like `1 march 2025` with the day of the month being 1 and the month lowercased are treated as month-year-only.
The relevant code checks for a capitalized word; it should probably instead check for the actual month names or
abbrevs.
]=]
-- Named constants for all modules used, to make it easier to swap out sandbox versions.
local check_isxn_module = "Module:check isxn"
local debug_track_module = "Module:debug/track"
local en_utilities_module = "Module:en-utilities"
local italics_module = "Module:italics"
local labels_module = "Module:labels"
local languages_module = "Module:languages"
local languages_error_module = "Module:languages/error"
local links_module = "Module:links"
local number_utilities_module = "Module:number utilities"
local parameters_module = "Module:parameters"
local parse_utilities_module = "Module:parse utilities"
local qualifier_module = "Module:qualifier"
local roman_numerals_module = "Module:roman numerals"
local scribunto_module = "Module:Scribunto"
local script_utilities_module = "Module:script utilities"
local scripts_module = "Module:scripts"
local string_pattern_escape_module = "Module:string/patternEscape"
local string_replacement_escape_module = "Module:string/replacementEscape"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local template_parser_module = "Module:template parser"
local usex_module = "Module:usex"
local usex_templates_module = "Module:usex/templates"
local utilities_module = "Module:utilities"
local yesno_module = "Module:yesno"
local concat = table.concat
local insert = table.insert
local new_title = mw.title.new
local remove = table.remove
local require = require
local sort = table.sort
local u = mw.ustring.char
local ugsub = mw.ustring.gsub
local umatch = mw.ustring.match
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
-- Use HTML entities here to avoid parsing issues (esp. with brackets)
local SEMICOLON_SPACE = "; "
local SPACE_LBRAC = " ["
local RBRAC = "]"
local TEMP_LT = u(0xFFF1)
local TEMP_GT = u(0xFFF2)
local TEMP_LBRAC = u(0xFFF3)
local TEMP_RBRAC = u(0xFFF4)
local TEMP_SEMICOLON = u(0xFFF5)
local function apply_subst(...)
apply_subst = require(usex_module).apply_subst
return apply_subst(...)
end
local function check_isbn(...)
check_isbn = require(check_isxn_module).check_isbn
return check_isbn(...)
end
local function check_issn(...)
check_issn = require(check_isxn_module).check_issn
return check_issn(...)
end
local function debug_track(...)
debug_track = require(debug_track_module)
return debug_track(...)
end
local function decode_entities(...)
decode_entities = require(string_utilities_module).decode_entities
return decode_entities(...)
end
local function embedded_language_links(...)
embedded_language_links = require(links_module).embedded_language_links
return embedded_language_links(...)
end
local function escape_wikicode(...)
escape_wikicode = require(parse_utilities_module).escape_wikicode
return escape_wikicode(...)
end
local function find_best_script_without_lang(...)
find_best_script_without_lang = require(scripts_module).findBestScriptWithoutLang
return find_best_script_without_lang(...)
end
local function find_parameters(...)
find_parameters = require(template_parser_module).find_parameters
return find_parameters(...)
end
local function format_categories(...)
format_categories = require(utilities_module).format_categories
return format_categories(...)
end
local function format_processed_labels(...)
format_processed_labels = require(labels_module).format_processed_labels
return format_processed_labels(...)
end
local function format_qualifier(...)
format_qualifier = require(qualifier_module).format_qualifier
return format_qualifier(...)
end
local function format_usex(...)
format_usex = require(usex_module).format_usex
return format_usex(...)
end
local function get_lang(...)
get_lang = require(languages_module).getByCode
return get_lang(...)
end
local function get_number(...)
get_number = require(number_utilities_module).get_number
return get_number(...)
end
local function get_script(...)
get_script = require(scripts_module).getByCode
return get_script(...)
end
local function gsplit(...)
gsplit = require(string_utilities_module).gsplit
return gsplit(...)
end
local function page_should_be_ignored(...)
page_should_be_ignored = require(usex_templates_module).page_should_be_ignored
return page_should_be_ignored(...)
end
local function parse_inline_modifiers(...)
parse_inline_modifiers = require(parse_utilities_module).parse_inline_modifiers
return parse_inline_modifiers(...)
end
local function parse_inline_modifiers_from_segments(...)
parse_inline_modifiers_from_segments = require(parse_utilities_module).parse_inline_modifiers_from_segments
return parse_inline_modifiers_from_segments(...)
end
local function parse_multi_delimiter_balanced_segment_run(...)
parse_multi_delimiter_balanced_segment_run = require(parse_utilities_module).parse_multi_delimiter_balanced_segment_run
return parse_multi_delimiter_balanced_segment_run(...)
end
local function parse_term_with_lang(...)
parse_term_with_lang = require(parse_utilities_module).parse_term_with_lang
return parse_term_with_lang(...)
end
local function pattern_escape(...)
pattern_escape = require(string_pattern_escape_module)
return pattern_escape(...)
end
local function pluralize(...)
pluralize = require(en_utilities_module).pluralize
return pluralize(...)
end
local function process_params(...)
process_params = require(parameters_module).process
return process_params(...)
end
local function remove_links(...)
remove_links = require(links_module).remove_links
return remove_links(...)
end
local function roman_to_arabic(...)
roman_to_arabic = require(roman_numerals_module).roman_to_arabic
return roman_to_arabic(...)
end
local function replacement_escape(...)
replacement_escape = require(string_replacement_escape_module)
return replacement_escape(...)
end
local function scribunto_parameter_key(...)
scribunto_parameter_key = require(scribunto_module).scribunto_parameter_key
return scribunto_parameter_key(...)
end
local function serial_comma_join(...)
serial_comma_join = require(table_module).serialCommaJoin
return serial_comma_join(...)
end
local function shallow_copy(...)
shallow_copy = require(table_module).shallowCopy
return shallow_copy(...)
end
local function split(...)
split = require(string_utilities_module).split
return split(...)
end
local function split_alternating_runs(...)
split_alternating_runs = require(parse_utilities_module).split_alternating_runs
return split_alternating_runs(...)
end
local function split_and_process_raw_labels(...)
split_and_process_raw_labels = require(labels_module).split_and_process_raw_labels
return split_and_process_raw_labels(...)
end
local function split_on_comma(...)
split_on_comma = require(parse_utilities_module).split_on_comma
return split_on_comma(...)
end
local function tag_text(...)
tag_text = require(script_utilities_module).tag_text
return tag_text(...)
end
local function tag_transcription(...)
tag_transcription = require(script_utilities_module).tag_transcription
return tag_transcription(...)
end
local function tag_translit(...)
tag_translit = require(script_utilities_module).tag_translit
return tag_translit(...)
end
local function ulen(...)
ulen = require(string_utilities_module).len
return ulen(...)
end
local function unitalicize_brackets(...)
unitalicize_brackets = require(italics_module).unitalicize_brackets
return unitalicize_brackets(...)
end
local function upper(...)
upper = require(string_utilities_module).upper
return upper(...)
end
local function usub(...)
usub = require(string_utilities_module).sub
return usub(...)
end
local function yesno(...)
yesno = require(yesno_module)
return yesno(...)
end
local function track(page)
debug_track("quote/" .. page)
end
local function maintenance_line(text)
return '<span class="maintenance-line">(' .. text .. ")</span>"
end
local function isbn(text)
return "[[Special:BookSources/"
.. text
.. "|→ISBN]]"
.. check_isbn(
text,
' <span class="error" style="font-size:88%">Invalid ISBN</span>[[Category:Pages with ISBN errors]]'
)
end
local function issn(text)
return "[https://www.worldcat.org/issn/"
.. text
.. " →ISSN]"
.. check_issn(
text,
' <span class="error" style="font-size:88%">Invalid ISSN</span>[[Category:Pages with ISSN errors]]'
)
end
local function lccn(text)
text = text:gsub(" ", "")
if text:find("-") then
-- old-style LCCN; reformat per request by [[User:The Editor's Apprentice]]
local prefix, part1, part2 = text:match("^(.-)(%d+)%-(%d+)$")
if prefix then
if ulen(part2) < 6 then
part2 = ("0"):rep(6 - ulen(part2)) .. part2
end
text = prefix .. part1 .. part2
end
end
return "[https://lccn.loc.gov/" .. mw.uri.encode(text) .. " →LCCN]"
end
local function format_date(text)
return mw.getCurrentFrame():callParserFunction("#formatdate", text, "dmy")
end
-- Parse a raw lb= param (or nil) to individual label info objects and then concatenate them appropriately into a
-- qualifier input, respecting flags like `omit_preComma` and `omit_postSpace` in the label specs.
local function parse_and_format_labels(raw_lb, lang)
if not raw_lb then
return nil
end
local labels = split_and_process_raw_labels{labels = raw_lb, lang = lang, nocat = true}
labels = format_processed_labels{labels = labels, lang = lang, no_ib_content = true}
if labels ~= "" then -- not sure labels can be an empty string but it seems possible in some circumstances
return {labels}
end
end
-- Convert a comma-separated list of language codes to a comma-separated list of language names. `fullname` is the
-- name of the parameter from which the list of language codes was fetched.
local function format_langs(langs)
local names = {}
for i, lang in ipairs(langs) do
names[i] = lang:getCanonicalName()
end
if #names == 1 then
return names[1]
end
return serial_comma_join(names)
end
local function get_first_lang(langs)
return langs[1] or get_lang("und")
end
--[=[
Normally we parse off inline modifiers and language code prefixes in various places, e.g. he:מרים<tr:Miryem>. But we
exclude HTML entries with <span ...>, <i ...>, <br/> or similar in it, caused by wrapping an argument in {{l|...}},
{{lang|...}} or similar. Basically, all tags of the sort we parse here should consist of a less-than sign, plus letters,
plus a colon, e.g. <tr:...>, so if we see a tag on the outer level that isn't in this format, we don't try to parse it.
The restriction to the outer level is to allow generated HTML inside of e.g. qualifier modifiers, such as
foo<q:similar to {{m|fr|bar}}> (if we end up supporting such modifiers).
Also exclude things that look like URL's from being parsed as having language code prefixes.
]=]
local function val_should_not_be_parsed_for_annotations(val)
return val:find("^[^<]*<%l*[^%l:]") or val:find("^%l+://")
end
local param_mods = {
t = {
-- <t:...> and <gloss:...> are aliases.
item_dest = "gloss",
},
gloss = {},
alt = {},
tr = {},
ts = {},
subst = {},
sc = {type = "script"},
f = {
convert = function(arg, parse_err)
local prefix, val = arg:match("^(.-):([^ ].*)$")
if not prefix then
prefix = ""
val = arg
end
local tags, sc_code, sc = prefix:match("^(.*)/(.-)$")
if sc_code then
sc = get_script(sc_code) or
require(languages_error_module)(sc_code, parse_err, "script code", nil, "not real lang")
else
tags = prefix
end
local quals
if tags ~= "" then
quals = split_on_comma(tags)
for i, qual in ipairs(quals) do
local obj = get_lang(qual, nil, "allow etym") or get_script(qual)
quals[i] = obj or qual
end
end
return {
quals = quals,
sc = sc,
val = val,
}
end,
store = "insert",
},
q = {},
qq = {},
}
local function generate_obj_annotated_text(text, parse_err, paramname)
local obj = {}
if text:find(":[^ ]") or text:find("%[%[") then
local display, is_wikipedia_wikisource
obj.text, obj.lang, obj.link, display, is_wikipedia_wikisource =
parse_term_with_lang {
term = text,
parse_err = parse_err,
paramname = paramname
}
-- HACK: If object is a Wikipedia or Wikisource link, we need to convert it to a two-part link
-- to avoid the w: or s: prefix showing. We should probably avoid this by calling
-- language_link() (in [[Module:links]]) in format_annotated_text() instead of the hacky stuff
-- that we currently do; but we need to test that carefully to make sure it doesn't do things like
-- munge % signs.
if is_wikipedia_wikisource and not obj.text:find("%[%[") and not obj.text:find("%]%]") then
-- Don't directly set .alt, because then if the user specifies <alt:...>, it will cause an error in
-- parse_inline_modifiers().
obj.display = display
end
else
obj.text = text
obj.link = text
end
return obj
end
--[=[
Parse a textual property that may be in a foreign language or script and may be annotated with a language prefix and/or
inline modifiers. `val` is the value of the parameter and `fullname` is the name of the parameter from which the value
was retrieved. `explicit_gloss`, if specified and non-nil, overrides any gloss specified using the <t:...> or
<gloss:...> inline modifier.
If `val` is nil, the return value of this function is nil. Otherwise it is parsed for a language prefix (e.g.
'ar:مُؤَلِّف') and inline modifiers (e.g. 'ar:مُؤَلِّف<t:Author>'), and the return value is an object with the following
fields:
`lang`: The language object corresponding to the language prefix, if specified, or nil if no language prefix is
given.
`text`: The text after stripping off any language prefix and inline modifiers.
`link`: The link part of the text if it consists of a two-part link; otherwise, same as `text`.
`alt`: Display text specified using the <alt:...> modifier, if given; otherwise, nil.
`subst`: Substitutions used to generate the transliteration, in the same format as the subst= parameter.
`sc`: The script object corresponding to the <sc:...> modifier, if given; otherwise nil.
`tr`: The transliteration corresponding to the <tr:...> modifier, if given; otherwise nil.
`ts`: The transcription corresponding to the <ts:...> modifier, if given; otherwise nil.
`gloss`: The gloss/translation corresponding to the `explicit_gloss` parameter (if given and non-nil), otherwise
the <t:...> or <gloss:...> modifiers if given, otherwise nil.
`f`: Foreign versions of the text.
`q`: Left qualifiers.
`qq`: Right qualifiers.
Note that as a special case, if `val` contains HTML tags at the top level (e.g. '<span class="Arab">...</span>', as
might be generated by specifying {{lang|ar|مُؤَلِّف}}), no language prefix or inline modifiers are parsed, and the return
value has the `noscript` field set to true, which tells format_annotated_text() not to try to identify the script of
the text and CSS-tag the text accordingly, but to leave the text untagged.
This object can be passed to format_annotated_text() to format a string displaying the text (appropriately
script-tagged, unless `noscript` is set, as described above) and modifiers.
]=]
local function parse_annotated_text(val, fullname, explicit_gloss)
if not val then
return nil
end
-- When checking for inline modifiers, exclude HTML entry with <span ...>, <i ...>, <br/> or similar in it, caused
-- by wrapping an argument in {{l|...}}, {{lang|...}} or similar. Also exclude URL's from being parsed as having
-- language code prefixes. See val_should_not_be_parsed_for_annotations() for more information. If we find a
-- parameter value with top-level HTML in it, add 'noscript = true' to indicate that we should not try to do script
-- inference and tagging. (Otherwise, e.g. if you specify {{lang|ar|مُؤَلِّف}} as the author, you'll get an extra big
-- font coming from the fact that {{lang|...}} wraps the Arabic text in CSS that increases the size from the
-- default, and then we do script detection and again wrap the text in the same CSS, which increases the size even
-- more.)
if val_should_not_be_parsed_for_annotations(val) then
return {text = val, link = val, noscript = true, gloss = explicit_gloss}
end
local obj
if val:find("<") then
-- Check for inline modifier.
obj = parse_inline_modifiers(val, {
paramname = fullname,
param_mods = param_mods,
generate_obj = generate_obj_annotated_text,
})
else
obj = generate_obj_annotated_text(val, nil, fullname)
end
if explicit_gloss then
obj.gloss = explicit_gloss
end
return obj
end
local html_entity_char_to_replacement = {
["<"] = TEMP_LT,
[">"] = TEMP_GT,
["["] = TEMP_LBRAC,
["]"] = TEMP_RBRAC,
}
local function html_entity_replacement(entity, code_without_semicolon, hash, xcode, x, code)
-- Try to decode the entity. If successful, Replace certain special HTML entities (those that are bracket-like)
-- with single Unicode characters; otherwise, replace the semicolon with a special character so it won't get
-- interpreted as a delimiter.
local ch = decode_entities(entity)
if ch ~= entity then
return html_entity_char_to_replacement[ch] or code_without_semicolon .. TEMP_SEMICOLON
end
-- If the entity doesn't decode, escape it anyway iff it follows a valid format.
if hash == "" then
-- Any nonstandard MediaWiki-only entities have now been filtered off, so treat any non-ASCII characters as
-- invalid.
return xcode:match("^[^\128-\255]+$") and code_without_semicolon .. TEMP_SEMICOLON or entity
elseif x == "" then
return xcode:match("^%d+$") and code_without_semicolon .. TEMP_SEMICOLON or entity
end
return code:match("^%x+$") and code_without_semicolon .. TEMP_SEMICOLON or entity
end
local html_entity_replacement_to_char = {
[TEMP_LT] = "<",
[TEMP_GT] = ">",
[TEMP_LBRAC] = "[",
[TEMP_RBRAC] = "]",
[TEMP_SEMICOLON] = ";",
}
local function undo_html_entity_replacement(txt)
-- Pattern covers everything in html_entity_replacement_to_char.
return (txt:gsub("\239\191[\177-\181]", html_entity_replacement_to_char))
end
-- NOTE: We try hard to optimize this function for the common cases and avoid loading [[Module:parse utilities]]
-- in such cases. The cases we can handle without loading [[Module:parse utilities]] are single values (no
-- semicolons present) without inline modifiers or language prefixes, and multi-entity values (semicolons present)
-- without (a) brackets of any kind (including parens, braces and angle brackets; angle brackets typically indicate
-- inline modifiers and other brackets may protect a semicolon from being interpreted as a delimiter);
-- (b) ampersands (which may indicate HTML entities, which protect a semicolon from being interpreted as a
-- delimiter); and (c) colons not followed by a space (which may indicate a language prefix).
local function generate_obj_multivalued_annotated_text(text, parse_err, paramname, no_undo_html_entity_replacement)
local obj = generate_obj_annotated_text(text, parse_err, paramname)
if not no_undo_html_entity_replacement then
obj.text = undo_html_entity_replacement(obj.text)
obj.link = undo_html_entity_replacement(obj.link)
end
return obj
end
--[=[
Similar to parse_annotated_text() but the parameter value may contain multiple semicolon-separated entities, each with
their own inline modifiers. Some examples:
* mainauthor=Paula Pattengale; Terea Sonsthagen
* author=Katie Brick; J. Cody Nielsen; Greg Jao; Eric Paul Rogers; John A. Monson
* author=Suzanne Brockmann; Patrick G. Lawlor (Patrick Girard); Melanie Ewbank
* author=G Ristori; et al.
* author=Jason Scott; zh:王晰宁<t:Wang Xining>
* editors=zh:包文俊; zh:金心雯
* quotee=zh:張福運<t:Chang Fu-yun>; zh:張景文<t:Chang Ching-wen>
There may be embedded semicolons within brackets, braces or parens that should not be treated as delimiters, e.g.:
* author=Oliver Optic [pseudonym; {{w|William Taylor Adams}}]
* author=author=Shannon Drake (pen name; {{w|Heather Graham Pozzessere}})
* author=James (the Elder;) Humphrys
There may also be HTML entities with semicolons in them:
* author=[{{w|Gilbert Clerke}}]
* 2ndauthor=Martin Biddle & Sally Badham
* author=Peter Christen Asbjørnsen
There may be both embedded semicolons and HTML entities with semicolons in them:
* author=[{{w|Voltaire}} [pseudonym; François-Marie Arouet]]
In general we want to treat [ like an opening bracket and ] like a closing bracket. Beware that they may be
mismatched:
* author=Anonymous [{{w|Karl Maria Kertbeny}}]
Here, `val` is the value of the parameter and `fullname` is the name of the parameter from which the value was
retrieved. `explicit_gloss`, if specified and non-nil, overrides any gloss specified using the <t:...> or <gloss:...>
inline modifier, and `explicit_gloss_fullname` is the name of the parameter from which this value was retrieved. (If
`explicit_gloss` is specified and multiple values were seen, an error results.)
Return value is a list of objects of the same sort as returned by parse_annotated_text().
]=]
local function parse_multivalued_annotated_text(val, fullname, explicit_gloss, explicit_gloss_fullname)
if not val then
return nil
end
-- NOTE: In the code that follows, we use `entity` most of the time to refer to one of the semicolon-separated
-- values in the multivalued param. Entities are most commonly people (typically authors, editors, translators or
-- the like), but may be the names of publishers, locations, or other entities. "Entity" can also refer to HTML
-- entities; in the places where this occurs, the variable name contains 'html' in it.
local splitchar, english_delim
if val:find("^,") then
splitchar = ","
english_delim = "comma"
val = val:gsub("^,", "")
else
splitchar = ";"
english_delim = "semicolon"
end
-- Optimization #1: No semicolons/commas or angle brackets (indicating inline modifiers).
if not val:find("[<" .. splitchar .. "]") then
if val_should_not_be_parsed_for_annotations(val) then
return {{text = val, link = val, noscript = true}}
else
return {generate_obj_multivalued_annotated_text(val, nil, fullname, "no undo html entity replacement")}
end
end
-- Optimization #2: Semicolons/commas but no angle brackets (indicating inline modifiers), braces, brackets, or
-- parens (any of which would protect the semicolon/comma from interpretation as a delimiter), and no ampersand
-- (which might indicate an HTML entity with a terminating semicolon, which should not be interpreted as a
-- delimiter).
if not val:find("[<>%[%](){}&]") then
local entity_objs = {}
for entity in gsplit(val, "%s*" .. splitchar .. "%s*") do
if val_should_not_be_parsed_for_annotations(entity) then
insert(entity_objs, {
text = entity,
link = entity,
noscript = true
})
else
insert(entity_objs, generate_obj_multivalued_annotated_text(entity, nil, fullname, "no undo html entity replacement"))
end
end
return entity_objs
end
-- Escape HTML entities, and get rid of directionality markers.
local amp = val:find("&", nil, true)
if amp then
-- The pattern is more permissive than the usual entity pattern, as MediaWiki has some nonstandard entities
-- that have non-ASCII characters in their codes.
val = val:gsub("((&(#?)(([xX]?)([%w\128-\255]+)));)", html_entity_replacement)
end
-- Pattern covers left-to-right (U+200E) and right-to-left (U+200F).
val = val:gsub("\226\128[\142\143]", "")
-- Parse balanced segment runs, treating HTML entities for left and right bracket and left and right angle bracket
-- as matching literal versions of the same characters.
local entity_runs = parse_multi_delimiter_balanced_segment_run(
val,
{{"[" .. TEMP_LBRAC, "]" .. TEMP_RBRAC}, {"(", ")"}, {"{", "}"}, {"<" .. TEMP_LT, ">" .. TEMP_GT}},
true
)
if type(entity_runs) == "string" then
local undo_val = undo_html_entity_replacement(val)
-- Parse error due to unbalanced delimiters. Don't throw an error here; instead, don't attempt to parse off
-- any annotations, but return the value directly, maybe allowing script tagging (not allowing it if it appears
-- the text is already script-tagged).
return {{text = undo_val, link = undo_val, noscript = not not val_should_not_be_parsed_for_annotations(val)}}
end
-- Split on semicolon (or comma), possibly surrounded by whitespace.
local separated_groups = split_alternating_runs(entity_runs, "%s*" .. splitchar .. "%s*")
-- Process each value.
local entity_objs = {}
for _, entity_group in ipairs(separated_groups) do
-- Rejoin runs that don't involve <...>.
local j = 2
while j <= #entity_group do
if not entity_group[j]:find("^<.*>$") then
entity_group[j - 1] = entity_group[j - 1] .. entity_group[j] .. entity_group[j + 1]
remove(entity_group, j)
remove(entity_group, j)
else
j = j + 2
end
end
local oneval = undo_html_entity_replacement(concat(entity_group))
-- When checking for inline modifiers, exclude HTML entry with <span ...>, <i ...>, <br/> or similar in it,
-- caused by wrapping an argument in {{l|...}}, {{lang|...}} or similar. Also exclude URL's from being parsed
-- as having language code prefixes. This works analogously to parse_annotated_text(); see there for more.
if val_should_not_be_parsed_for_annotations(oneval) then
insert(entity_objs, {
text = oneval,
link = oneval,
noscript = true
})
else
local obj
if #entity_group > 1 then
-- Check for inline modifier.
obj = parse_inline_modifiers_from_segments({
group = entity_group,
arg = oneval,
props = {
paramname = fullname,
param_mods = param_mods,
generate_obj = generate_obj_multivalued_annotated_text,
},
})
else
obj = generate_obj_multivalued_annotated_text(entity_group[1], nil, fullname)
end
insert(entity_objs, obj)
end
end
if explicit_gloss then
if #entity_objs > 1 then
error(
(
"Can't specify |%s= along with multiple %s-separated entities in |%s=; use the <t:...> "
.. "inline modifier attached to the individual entities"
):format(explicit_gloss_fullname, english_delim, fullname)
)
end
entity_objs[1].gloss = explicit_gloss
end
return entity_objs
end
--[=[
Format a text property that may be in a foreign language or script, along with annotations. This is conceptually
similar to the full_link() function in [[Module:links]], but displays the annotations in a different format that is
more appropriate for bibliographic entries. The output looks like this:
TEXT [TRANSLIT /TRANSCRIPTION/, GLOSS]
`textobj` is as returned by parse_annotated_text(). `tag_text_func`, if supplied, is a function of one argument to further
wrap the text after it has been processed and CSS-tagged appropriately, directly before insertion. `tag_gloss_func` is a
similar function for the gloss.
]=]
local function format_annotated_text(textobj, tag_text_func, tag_gloss_func)
if not textobj then
return nil
end
local text, link = textobj.text, textobj.link
local subst, tr, ts, f, gloss = textobj.subst, textobj.tr, textobj.ts, textobj.f, textobj.gloss
-- Retrieve the display text, either specified by the user (.alt) or by parse_annotated_text() (.display) when a
-- Wikipedia link is given.
local alt = textobj.alt or textobj.display
if alt then
if link:find("%[%[") or link:find("%]%]") then
local errmsg = ("Can't currently handle embedded links in '%s', with <alt:...> text '%s'"):format(link, alt)
error(escape_wikicode(errmsg))
end
text = ("[[%s|%s]]"):format(link, alt)
end
-- See above for `noscript`, meaning HTML was found in the text value, probably generated using {{lang|...}}.
-- {{lang}} already script-tags the text and processes embedded language links, so we don't want to do it again (in
-- fact, the code below within the if-clause is similar to what {{lang}} does). In such a case, an explicit language
-- won't be available and find_best_script_without_lang() may not be accurate, so we can't do automatic transliteration.
if not textobj.noscript then
local lang = textobj.lang
-- As an optimization, don't do script detection on an argument that contains only ASCII.
local sc = textobj.sc
or lang and lang:findBestScript(text)
or not text:find("^[ -~]$") and find_best_script_without_lang(text)
or nil
-- As an optimization, don't do any of the following if there's no language, script, translit or transcription,
-- as will be the case with simple ASCII values.
if lang or sc or tr or ts then
if not lang then
lang = get_lang("und")
end
if tr == "-" then
tr = nil
elseif not tr and sc and not sc:getCode():find("Lat") then -- Latn, Latf, Latg, pjt-Latn
-- might return nil
local text_for_tr = text
if subst then
text_for_tr = apply_subst(text_for_tr, subst)
else
text_for_tr = remove_links(text)
end
tr = (lang:transliterate(text_for_tr, sc))
end
if text:find("%[%[") then
-- FIXME: embedded_language_links() replaces % signs with their URL-encoded equivalents,
-- which messes up URL's that may be present (e.g. if chapterurl= is given). IMO this
-- should not happen, and embedded_language_links() should do nothing if no embedded links
-- are present. To work around this, only call embedded_language_links() when there are
-- embedded links present.
text = embedded_language_links({
term = text,
lang = lang,
sc = sc,
})
end
if lang:getCode() ~= "und" or sc:getCode() ~= "Latn" then
text = tag_text(text, lang, sc)
end
if tr then
-- Should we link to the transliteration of languages with lang:link_tr()? Probably not because `text` is not
-- likely to be a term that has an entry.
tr = tag_translit(tr, lang, "usex")
end
if ts then
ts = tag_transcription(ts, lang, "usex")
end
end
end
text = unitalicize_brackets(text)
if tag_text_func then
text = tag_text_func(text)
end
local parts = {}
if textobj.q then
insert(parts, format_qualifier(textobj.q) .. " ")
end
insert(parts, text)
if tr or ts or f or gloss then
insert(parts, SPACE_LBRAC)
local subparts = {}
if tr or ts then
local tr_ts
if ts then
ts = "/" .. ts .. "/"
end
if tr and ts then
tr_ts = tr .. " " .. ts
else
tr_ts = tr or ts
end
insert(subparts, tr_ts)
end
if f then
for _, ff in ipairs(f) do
local sc = ff.sc
local lang
if not sc and ff.quals then
local qual = ff.quals[1]
if type(qual) == "string" then
-- do nothing; we'll do script detection farther down
elseif qual:hasType("script") then
sc = qual
else -- language
sc = qual:findBestScript(ff.val)
lang = qual
end
end
if not lang then
lang = get_lang("und")
end
sc = sc or find_best_script_without_lang(ff.val)
local val = embedded_language_links({
term = ff.val,
lang = lang,
sc = sc,
})
if lang:getCode() ~= "und" or sc:getCode() ~= "Latn" then
val = tag_text(val, lang, sc)
end
local qual_prefix
if ff.quals then
for i, qual in ipairs(ff.quals) do
if type(qual) ~= "string" and (qual:hasType("script") or qual:hasType("language")) then
ff.quals[i] = qual:getCanonicalName()
end
end
qual_prefix = concat(ff.quals, "/") .. ": "
else
qual_prefix = ""
end
insert(subparts, qual_prefix .. val)
end
end
if gloss then
gloss = '<span class="e-translation">' .. gloss .. "</span>"
gloss = unitalicize_brackets(gloss)
if tag_gloss_func then
gloss = tag_gloss_func(gloss)
end
insert(subparts, gloss)
end
insert(parts, concat(subparts, ", "))
insert(parts, RBRAC)
end
if textobj.qq then
insert(parts, " " .. format_qualifier(textobj.qq))
end
return concat(parts)
end
--[=[
Format a multivalued text property that may be in a foreign language or script, along with annotations. This is the
multivalued analog to format_annotated_text(), and formats each individual entity using format_annotated_text(),
joining the results with `delimiter`, which defaults to ", ". It `delimiter` is "and" or "or", join the results using
serial_comma_join() with the specified conjunction.
`textobjs` is as returned by parse_multivalued_annotated_text(). `tag_text_func` and `tag_gloss_func` are as in
format_annotated_text().
]=]
local function format_multivalued_annotated_text(textobjs, delimiter, tag_text_func, tag_gloss_func)
if not textobjs then
return nil
end
if #textobjs == 1 then
return format_annotated_text(textobjs[1], tag_text_func, tag_gloss_func)
end
local parts = {}
for _, textobj in ipairs(textobjs) do
insert(parts, format_annotated_text(textobj, tag_text_func, tag_gloss_func))
end
-- Change delimiter to semicolon if the items themselves contain commas (e.g., in ", Jr.")
local use_semicolon = false
for _, obj in ipairs(textobjs) do
-- Use remove_links to ignore commas in links
if obj.text and remove_links(obj.text):find(", ") then
use_semicolon = true
break
end
end
local n = #parts
if n > 0 and parts[n]:match("^'*et al[.']*$") then
-- Special handling for 'et al.'
parts[n] = "''et al.''"
if n == 2 then
-- author et al.
return concat(parts, " ")
else
-- author 1, author 2, et al.
return concat(parts, (use_semicolon and "; ") or ", ")
end
if delimiter == "and" or delimiter == "or" then
delimiter = ", "
end
return concat(parts, delimiter)
end
if delimiter == "and" or delimiter == "or" then
return serial_comma_join(parts, {conj = delimiter})
end
return concat(parts, delimiter or ((use_semicolon and "; ") or ", "))
end
-- Fancy version of ine() (if-not-empty). Converts empty string to nil, but also strips leading/trailing space.
local function ine(arg)
if not arg then
return nil
elseif type(arg) ~= "string" then
return arg
end
arg = mw.text.trim(arg)
if arg == "" then
return nil
end
return arg
end
local abbrs = {
["a."] = {anchor = "a.", full = "ante"},
["c."] = {anchor = "c.", full = "circa"},
["p."] = {anchor = "p.", full = "post"},
}
-- Process prefixes 'a.' (ante), 'c.' (circa) and 'p.' (post) at the beginning of an arbitrary date or year spec.
-- Returns two values, the formatted version of the prefix and the date spec minus the prefix. If no prefix is found,
-- returns an empty string and the full date.
local function process_ante_circa_post(date)
local prefix = usub(date, 1, 2)
local abbr = abbrs[prefix]
local abbr_prefix = ""
if abbr then
abbr_prefix = "''[[Appendix:Glossary#"
.. abbr.anchor
.. '|<abbr title="'
.. abbr.full
.. '">'
.. abbr.anchor
.. "</abbr>]]'' "
-- Remove lowercase letter, period, and space from beginning of date parameter.
date = ugsub(date, "^%l%.%s*", "")
end
return abbr_prefix, date
end
-- Format the arguments that specify the date of the quotation. These include the following:
-- |date=: The date. If |start_date= is given, this is the end date.
-- |year=, |month=: Year and month of quotation date or end of range, if |date= isn't given.
-- |start_date=: The start date, to specify a range.
-- |start_year=, |start_month=: Year and month of start of range, if |start_date= isn't given.
-- |accessdate=: Date a website was accessed; processed if no other date was given.
-- |nodate=: Indicate that no date is present; otherwise a maintenance line will be displayed if there is no date.
--
-- If `parampref` and/or `paramsuf` are given, this modifies all the date arguments accordingly. For example, if
-- `parampref` == "orig" and `paramsuf` is omitted, the date is specified using |origdate= or |origyear=/|origmonth=,
-- and the start of the range is |origstart_date=, etc. Similarly, if `parampref` is omitted and `paramsuf` is
-- "_published", the date is specified using |date_published= or |year_published=/|month_published=, and the start of
-- the range is |start_date_published=, etc.
--
-- `a` and `get_full_paramname` are functions with the same interpretation as the local functions of the same name in
-- source(). These are used to fetch parameters and get their full names. Note that this may cause all arguments to
-- have an index added to them (|date2=, |year2=, |month2=, etc.).
--
-- `alias_map` is as in source() and is used to map canonical arguments to their aliases when aliases were used.
--
-- If `bold_year` is given, displayed years are boldfaced unless boldface is present in the parameter value.
--
-- If `maintenance_line_no_date` is specified, it should be a string that will be returned if no date is found (i.e.
-- neither |date= nor |year=, or their appropriate equivalents per `parampref` and `paramsuf`, are specified, and
-- neither |nodate= is given to indicate that there is no date, or |accessdate= is given).
--
-- Returns two values: the formatted date and a boolean indicating whether to add a maintenance category
-- [[:Category:Requests for date in LANG entries]]. The first return value will be nil if nothing is to be added
-- (in which case the scond return value will always be nil).
local function format_date_args(
a,
get_full_paramname,
alias_map,
parampref,
paramsuf,
bold_year,
maintenance_line_no_date,
year_last
)
local output = {}
parampref = parampref or ""
paramsuf = paramsuf or ""
local function getp(param)
return a(parampref .. param .. paramsuf)
end
local function pname(param)
local fullname = get_full_paramname(parampref .. param .. paramsuf)
return alias_map[fullname] or fullname
end
-- Format `timestamp` (a timestamp referencing a date) according to the spec in `code`. `param` is the base name of
-- the parameter from which the timestamp was fetched, for error messages.
local function format_date_with_code(code, timestamp, param)
local language = mw.getContentLanguage()
local ok, date = pcall(language.formatDate, language, code, timestamp)
if ok then
return date
else
-- All the formats used in format_date_args() are fine, so the timestamp must be at fault.
error(
(
"Timestamp |%s=%s (possibly canonicalized from its original format) could not be parsed; see the "
.. "[[mw:Help:Extension:ParserFunctions##time|documentation for the #time parser function]]"
):format(pname(param), tostring(timestamp))
)
end
end
-- Try to figure out if the given timestamp has the day of the month explicitly given. We use the following
-- algorithm:
-- 1. Format as year-month-day; if the day is not 1, the day was explicitly given, since if only the year/month are
-- given, the day shows up as 1.
-- 2. If the day shows up as 1 and there isn't a 1 or 01 in the timestamp, the day wasn't explicitly given.
-- 3. Otherwise, if there are three separate numbers (e.g. 2022-07-01), or two separate numbers plus a capitalized
-- letter (taken as an English month, e.g. 2022 July 1), the day was explicitly given, otherwise not.
--
-- `param` is the base name of the parameter from which the timestamp was fetched.
local function date_has_day_specified(timestamp, param)
local day = format_date_with_code("j", timestamp, param)
if day ~= "1" then
return true
end
local english_month = timestamp:find("%u")
local canon_timestamp = mw.text.trim((timestamp:gsub("%D+", " ")))
local seen_nums = split(canon_timestamp, " ", true)
local saw_one = false
for _, num in ipairs(seen_nums) do
if num == "1" or num == "01" then
saw_one = true
break
end
end
if not saw_one then
return false
end
return #seen_nums >= 3 or english_month and #seen_nums >= 2
end
-- Format a date with boldfaced year, as e.g. '''2023''' August 3. `explicit_day_given` indicates whether to include
-- the day; if false, the return value will be e.g. '''2023''' August. `date_param` is the base name of the param
-- from which the date was fetched, for error messages.
local function format_bold_date(date, explicit_day_given, date_param)
local day_month_code = explicit_day_given and "j F" or "F"
local month_day_code = explicit_day_given and "F j" or "F"
if year_last then
if bold_year then
-- This formats like "3 August '''2023'''" (or "August '''2023'''" if day not explicitly given).
return format_date_with_code(day_month_code .. " '''Y'''", date, date_param)
else
-- This formats like "3 August 2023" (or "August 2023" if day not explicitly given).
return format_date_with_code(day_month_code .. " Y", date, date_param)
end
else
if bold_year then
-- This formats like "'''2023''' August 3" (or "'''2023''' August" if day not explicitly given).
return format_date_with_code("'''Y''' " .. month_day_code, date, date_param)
else
-- This formats like "2023 August 3" (or "2023 August" if day not explicitly given).
return format_date_with_code("Y " .. month_day_code, date, date_param)
end
end
end
-- The formatDate method of the mw.language object behaves like the {{#time:}} parser function, which doesn't
-- accept the formats "monthday monthname, year" or "year monthname monthday", but outputs garbage when it receives
-- them, behavior inherited from PHP. {{#formatdate:}} magic word is more forgiving. Fix dates so that, for
-- instance, the |date= parameter of {{quote-journal}} (which uses this code) and the |accessdate= parameter (which
-- uses {{#formatdate:}}) accept similar date formats. See:
-- * [[mw:Extension:Scribunto/Lua_reference_manual#mw.language:formatDate]]
-- * [[mw:Help:Extension:ParserFunctions##time]]
-- * [[mw:Help:Magic_words#Formatting]]
-- `date` is the date spec from the user, which is assumed to come from a parameter whose base name ends in "date";
-- `parampref` is the prefix added to "date" to get the parameter name.
local function fix_date(date, param_pref)
if tonumber(date) ~= nil then
error(
("|%s= should contain a full date (year, month, day of month); use |%s= for year"):format(
pname(param_pref .. "date"),
pname(param_pref .. "year")
)
)
elseif date and date:find("%s*%a+,%s*%d+%s*$") then
error(
("|%s= should contain a full date (year, month, day of month); use |%s=, |%s= for month and year"):format(
pname(param_pref .. "date"),
pname(param_pref .. "month"),
pname(param_pref .. "year")
)
)
end
if date then
-- Commas are replaced with spaces to prevent parsing issues
local fixed_date = ugsub(date, ", *", " ")
return (ugsub(ugsub(fixed_date or date, "(%d+ %a+),", "%1"), "^(%d%d%d%d) (%a+ %d%d?)$", "%2 %1"))
end
end
local start_date, date = fix_date(getp("start_date"), "start_"), fix_date(getp("date"), "")
local year = getp("year")
local month = getp("month")
local start_year = getp("start_year")
local start_month = getp("start_month")
if date and year then
error(("Only one of |%s= or |%s= should be specified"):format(pname("date"), pname("year")))
end
if date and month then
error(
("|%s= should only be specified in conjunction with |%s=, not with |%s="):format(
pname("month"),
pname("year"),
pname("date")
)
)
end
if start_date and start_year then
error(("Only one of |%s= or |%s= should be specified"):format(pname("start_date"), pname("start_year")))
end
if start_date and start_month then
error(
("|%s= should only be specified in conjunction with |%s=, not with |%s="):format(
pname("start_month"),
pname("start_year"),
pname("start_date")
)
)
end
if (start_date or start_year) and not (date or year) then
error(
("|%s= or |%s=/|%s= cannot be specified without specifying |%s= or |%s=/|%s="):format(
pname("start_date"),
pname("start_year"),
pname("start_month"),
pname("date"),
pname("year"),
pname("month")
)
)
end
local dash = " – "
local day_explicitly_given = date and date_has_day_specified(date, "date")
local start_day_explicitly_given = start_date and date_has_day_specified(start_date, "start_date")
-- Format a date with boldfaced year, as e.g. '''2023''' August 3 (if `explicit_day_given` specified) or
-- '''2023''' August (if `explicit_day_given` not specified). If no date specified, fall back to formatting based
-- on the year and (optionally) month params given in `yearobj` and `monthobj`, boldfacing the year if not already.
-- `date_param` is the base name of the param from which the date was fetched, for error messages.
local function format_date_or_year_month(date, yearobj, monthobj, explicit_day_given, date_param)
if date then
return format_bold_date(date, explicit_day_given, date_param)
else
-- Boldface a year spec if it's not already boldface.
if bold_year and not yearobj.text:find("'''") then
-- Clone the year object before modifying it because we may use it later to check against the current
-- year (if we're dealing with start_year).
yearobj = shallow_copy(yearobj)
yearobj.text = "'''" .. yearobj.text .. "'''"
if yearobj.alt then
yearobj.alt = "'''" .. yearobj.alt .. "'''"
end
end
if year_last then
return (monthobj and format_annotated_text(monthobj) .. " " or "") .. format_annotated_text(yearobj)
else
return format_annotated_text(yearobj) .. (monthobj and " " .. format_annotated_text(monthobj) or "")
end
end
end
local yearobj = parse_annotated_text(year, pname("year"))
local monthobj = parse_annotated_text(month, pname("month"))
local start_yearobj = parse_annotated_text(start_year, pname("start_year"))
local start_monthobj = parse_annotated_text(start_month, pname("start_month"))
if start_yearobj then
local abbr_prefix
abbr_prefix, start_yearobj.text = process_ante_circa_post(start_yearobj.text)
start_yearobj.link = select(2, process_ante_circa_post(start_yearobj.link))
insert(output, abbr_prefix)
elseif yearobj then
local abbr_prefix
abbr_prefix, yearobj.text = process_ante_circa_post(yearobj.text)
yearobj.link = select(2, process_ante_circa_post(yearobj.link))
insert(output, abbr_prefix)
end
if start_date or start_year then
local cur_year = yearobj and yearobj.text or format_date_with_code("Y", date, "date")
local cur_month = monthobj and monthobj.text or date and format_date_with_code("F", date, "date") or nil
local cur_day = date and day_explicitly_given and format_date_with_code("j", date, "date") or nil
local beg_year = start_yearobj and start_yearobj.text or format_date_with_code("Y", start_date, "start_date")
local beg_month = start_monthobj and start_monthobj.text
or start_date and format_date_with_code("F", start_date, "start_date")
or nil
local beg_day = start_date
and start_day_explicitly_given
and format_date_with_code("j", start_date, "start_date")
or nil
if year_last then
if beg_day then
insert(output, beg_day .. " " .. beg_month)
else
insert(output, beg_month)
end
if beg_year ~= cur_year then
insert(output, " " .. beg_year)
end
else
insert(output, format_date_or_year_month(
start_date,
start_yearobj,
start_monthobj,
start_day_explicitly_given,
"start_date"
))
end
if cur_year ~= beg_year then
-- Different years; insert current date in full.
if beg_month or cur_month then
insert(output, dash)
else
insert(output, "–")
end
insert(output, format_date_or_year_month(date, yearobj, monthobj, day_explicitly_given, "date"))
elseif cur_month and cur_month ~= beg_month then
local month_ins = monthobj and format_annotated_text(monthobj) or cur_month
-- Same year but different months; insert current month and (if available) current day.
if cur_day then
insert(output, dash)
if year_last then
insert(output, cur_day .. " " .. month_ins)
else
insert(output, month_ins .. " " .. cur_day)
end
else
if beg_day then
insert(output, dash)
else
insert(output, "–")
end
insert(output, month_ins)
end
elseif cur_day and cur_day ~= beg_day then
-- Same year and month but different days; insert current day.
insert(output, "–")
insert(output, cur_day)
else
-- Same year, month and day; or same year and month, and day not available; or same year, and month and
-- day not available. Do nothing. FIXME: Should we throw an error?
end
if year_last and beg_year == cur_year then
if cur_month then
insert(output, " " .. cur_year)
else
insert(output, " " .. cur_year)
end
end
elseif date or yearobj then
insert(output, format_date_or_year_month(date, yearobj, monthobj, day_explicitly_given, "date"))
elseif not maintenance_line_no_date then
-- Not main quote date. Return nil, caller will handle.
return nil, nil
elseif not getp("nodate") then
local accessdate = getp("accessdate")
if accessdate then
local explicit_day_given = date_has_day_specified(accessdate, "accessdate")
insert(output, format_bold_date(accessdate, explicit_day_given, "accessdate") .. " (last accessed)")
else
if mw.title.getCurrentTitle().namespace ~= 10 then
return maintenance_line(maintenance_line_no_date), true
end
return nil, nil
end
end
return ine(concat(output)), nil
end
local function tag_with_cite(txt)
return "<cite>" .. txt .. "</cite>"
end
-- Display the source line of the quote, above the actual quote text. This contains the majority of the logic of this
-- module (formerly contained in {{quote-meta/source}}).
function export.source(args, alias_map, format_as_cite, other_controls)
local tracking_categories = {}
local argslang = args[1] or args.lang
if not argslang then
-- For the moment, only trigger an error on mainspace pages and
-- other pages that are not user pages or pages containing discussions.
-- These are the same pages that appear in the appropriate tracking
-- categories. User and discussion pages have not generally been
-- fixed up to include a language code and so it's more helpful
-- to use a maintenance line than signal an error.
local current_title = mw.title.getCurrentTitle()
if not (current_title.namespace == 10 or page_should_be_ignored(current_title.fullText)) then
require(languages_error_module)(nil, 1)
end
end
-- Given a canonical param, convert it to the original parameter specified by the user (which may have been an
-- alias).
local function alias(param)
return alias_map[param] or param
end
local output, sep = {}
local overrides = other_controls and other_controls.overrides or {}
-- Add text to the output. The text goes into a list, and we concatenate all the list components together at the
-- end. To make it easier to handle comma-separated items, we keep track (in `sep`) of the separator (if any) that
-- needs to be inserted before the next item added. For example, if we're in the "newversion" code (ind ~= ""), and
-- there's no title and no URL, then the first time we add anything after the title, we don't want to add a
-- separating comma because the preceding text will say "republished " or "republished as " or "translated as " or
-- similar. In all- other cases, we do want to add a separating comma. The bare add() function reset the separator
-- to be nothing, while the add_with_sep() function resets the separator to be the value of `next_sep` (defaulting
-- to ", "), so the next time around we do add a comma to separate `text` from the preceding piece of text.
local function add(text)
if sep then
insert(output, sep)
end
insert(output, text)
sep = nil
end
local function add_with_sep(text, next_sep)
add(text)
sep = next_sep or ", "
end
-- FIXME: This is all very over-engineered, which makes it slow and memory-inefficient.
-- Return a function that generates the actual parameter name associated with a base param (e.g. "author", "last").
-- The actual parameter name may have an index added (an empty string for the first set of params, e.g. author=,
-- last=, or a numeric index for further sets of params, e.g. author2=, last2=, etc.).
local function make_get_full_paramname(ind)
return function(param)
return param .. ind
end
end
-- Function to fetch the actual parameter name associated with a base param (see make_get_full_paramname() above).
-- Assigned at various times below by calling make_get_full_paramname(). We do it this way so that we can have
-- wrapper functions that access params and define them only once.
local get_full_paramname
-- Return two values: the value of a parameter given the base param name (which may have a numeric index added),
-- and the parameter name from which the value was fetched (which may be an alias, i.e. you can't necessarily fetch
-- the parameter value from args[] given this name). The base parameter can be a list of such base params, which
-- are checked in turn, or nil, in which case nil is returned.
local function a_with_name(param)
if not param then
return nil
elseif type(param) ~= "table" then
local fullname = get_full_paramname(param)
return args[fullname], alias(fullname)
end
for _, par in ipairs(param) do
local val, fullname = a_with_name(par)
if val then
return val, alias(fullname)
end
end
return nil
end
-- Fetch the value of a parameter given the base param name (which may have a numeric index added). The base
-- parameter can be a list of such base params, which are checked in turn, or nil, in which case nil is returned.
local function a(param)
return (a_with_name(param))
end
-- Identical to a_with_name(param) except that it verifies that no space is present. Should be used for URL's.
local function aurl_with_name(param)
local value, fullname = a_with_name(param)
if value and value:find(" ") and not value:find("%[") then
error(("URL not allowed to contain a space, but saw |%s=%s"):format(fullname, value))
end
return value, fullname
end
-- Identical to a(param) except that it verifies that no space is present. Should be used for URL's.
local function aurl(param)
return (aurl_with_name(param))
end
-- Convenience function to fetch a parameter that may be in a foreign language or text (and may consequently have
-- a language prefix and/or inline modifiers), parse the annotations and convert the result into a formatted string.
-- This is the same as parse_and_format_annotated_text() below but also returns the full param name as the second
-- return value.
local function parse_and_format_annotated_text_with_name(param, tag_text_func, tag_gloss_func)
local val, fullname = a_with_name(param)
local obj = parse_annotated_text(val, fullname)
return format_annotated_text(obj, tag_text_func, tag_gloss_func), fullname
end
-- Convenience function to fetch a parameter that may be in a foreign language or text (and may consequently have
-- a language prefix and/or inline modifiers), parse the modifiers and convert the result into a formatted string.
-- This is a wrapper around parse_annotated_text() and format_annotated_text(). `param` is the base parameter name (see
-- a_with_name()), `tag_text_func` is an optional function to tag the parameter text after all other processing (e.g.
-- wrap in <cite>...</cite> tags), and `tag_gloss_func` is a similar function for the parameter translation/gloss.
local function parse_and_format_annotated_text(param, tag_text_func, tag_gloss_func)
return (parse_and_format_annotated_text_with_name(param, tag_text_func, tag_gloss_func))
end
-- Convenience function to fetch a multivalued parameter that may be in a foreign language or text (and may
-- consequently have a language prefix and/or inline modifiers), parse the modifiers and convert the result into a
-- formatted string. This is the multivalued analog to parse_and_format_annotated_text_with_name() and returns two
-- values, the formatted string and the full name of the parameter fetched. `delimiter` is as in
-- format_multivalued_annotated_text().
local function parse_and_format_multivalued_annotated_text_with_name(param, delimiter, tag_text_func, tag_gloss_func)
local val, fullname = a_with_name(param)
local objs = parse_multivalued_annotated_text(val, fullname)
local num_objs = objs and #objs or 0
return format_multivalued_annotated_text(objs, delimiter, tag_text_func, tag_gloss_func), fullname, num_objs
end
-- Convenience function to fetch a multivalued parameter that may be in a foreign language or text (and may
-- consequently have a language prefix and/or inline modifiers), parse the modifiers and convert the result into a
-- formatted string. This is the multivalued analog to parse_and_format_annotated_text(). `delimiter` is as in
-- format_multivalued_annotated_text().
local function parse_and_format_multivalued_annotated_text(param, delimiter, tag_text_func, tag_gloss_func)
return (parse_and_format_multivalued_annotated_text_with_name(param, delimiter, tag_text_func, tag_gloss_func))
end
-- This determines whether to display "Mary Bloggs, transl." (if there's no author preceding) or "translated by
-- Mary Bloggs" (if there's an author preceding).
local author_outputted = false
-- When formatting as a citation, the priority is to display a name and a date before the book/chapter title
-- this tracks whether or not the author/date has been displayed
local date_outputted, formatted_date, formatted_origdate = false
local function add_date(no_paren)
if not date_outputted then
if no_paren then
sep = ", "
else
sep = " "
end
if formatted_date then
if no_paren then
add(formatted_date)
else
add("(" .. formatted_date .. ")")
end
end
if formatted_origdate then
add(SPACE_LBRAC .. formatted_origdate .. RBRAC)
end
sep = ", "
date_outputted = true
end
end
local function is_anonymous(val)
return val:match("^[Aa]nonymous$") or val:match("^[Aa]non%.?$")
end
-- Add a formatted author (whose values may be specified using `author` or, for compatibility purposes, split
-- among various parameters):
-- * `author` is the value of the author param (e.g. "author", "author2" or "2ndauthor"), and `author_fullname` is
-- the full parameter name holding that value;
-- * `trans_author` is the optional value of the param holding the gloss/translation of the author, and
-- `trans_author_fullname` is the full parameter name holding that value (or nil for no such parameter);
-- * `authorlink` is the value of the authorlink param, which holds the Wikipedia link of the author(s) in `author`,
-- and `authorlink_fullname` is the full parameter name holding that value;
-- * `trans_authorlink` is the optional value of the param holding the Wikipedia link of the gloss/translation of
-- the author, and `trans_authorlink_fullname` is the full parameter name holding that value (or nil for no such
-- parameter);
-- * `first` is the value of the parameter holding the first name of the author, and `first_fullname` is the full
-- parameter name holding that value;
-- * `trans_first` is the value of the corresponding parameter holding the gloss/translation of the first name
-- (e.g. "trans-first"), and `trans_first_fullname` is the full parameter name holding that value (or nil for
-- no such parameter);
-- * `last` is the value of the parameter holding the last name of the author, and `last_fullname` is the full
-- parameter name holding that value;
-- * `trans_last` is the value of the corresponding parameter holding the gloss/translation of the last name
-- (e.g. "trans-last"), and `trans_last_fullname` is the full parameter name holding that value (or nil for
-- no such parameter).
-- * `last_first` if set, when parameters `first` and `last` are used, display the author name as "last, first"
local function add_author(
author,
author_fullname,
trans_author,
trans_author_fullname,
authorlink,
authorlink_fullname,
trans_authorlink,
trans_authorlink_fullname,
first,
first_fullname,
trans_first,
trans_first_fullname,
last,
last_fullname,
trans_last,
trans_last_fullname,
last_first
)
local function make_author_with_url(txt, txtparam, authorlink, authorlink_param)
if authorlink then
if authorlink:find("%[%[") then
error(("Can't specify links in |%s=%s"):format(authorlink_param, authorlink))
end
if txt:find("%[%[") then
error(("Can't specify links in %s=%s"):format(txtparam, txt))
end
return "[[w:" .. authorlink .. "|" .. txt .. "]]"
else
return txt
end
end
local num_authorobjs
if author then
local authorobjs =
parse_multivalued_annotated_text(author, author_fullname, trans_author, trans_author_fullname)
num_authorobjs = #authorobjs
if num_authorobjs == 1 then
if is_anonymous(authorobjs[1].text) then
authorobjs[1].text = "anonymous author"
authorobjs[1].link = "anonymous author"
end
if authorlink then
authorobjs[1].text = make_author_with_url(
authorobjs[1].text,
"|" .. author_fullname,
authorlink,
"|" .. authorlink_fullname
)
authorobjs[1].link = make_author_with_url(
authorobjs[1].link,
"|" .. author_fullname,
authorlink,
"|" .. authorlink_fullname
)
end
if authorobjs[1].gloss and trans_authorlink then
authorobjs[1].gloss = make_author_with_url(
authorobjs[1].gloss,
("<t:...> in |%s"):format(author_fullname),
trans_authorlink,
"|" .. trans_author_fullname
)
end
add(format_multivalued_annotated_text(authorobjs))
elseif trans_authorlink then
error(
(
"Can't specify |%s= along with multiple semicolon-separated entities in |%s=; use the "
.. "<t:...> inline modifier attached to the individual entities and put the link directly "
.. "in the value of the inline modifier"
):format(trans_authorlink_fullname, author_fullname)
)
else
-- Allow an authorlink with multiple authors, e.g. for use with |author=Max Mills; Harvey Mills
-- with |authorlink=Max and Harvey. For this we have to generate the entire text and link it
-- all.
local formatted_text = format_multivalued_annotated_text(authorobjs)
if authorlink then
formatted_text = make_author_with_url(
formatted_text,
"|" .. author_fullname,
authorlink,
"|" .. authorlink_fullname
)
end
add(formatted_text)
end
else
num_authorobjs = 1
-- Author separated into first name + last name. We don't currently support non-Latin-script
-- authors separated this way and probably never will.
if first then
if last_first then
author = last .. ", " .. first
else
author = first .. " " .. last
end
else
author = last
end
if authorlink then
local authorparam = first and ("|%s |%s"):format(first_fullname, last_fullname) or "|" .. last_fullname
author = make_author_with_url(author, authorparam, authorlink, authorlink_fullname)
end
local trans_author
if trans_last then
if trans_first then
trans_author = trans_first .. " " .. trans_last
else
trans_author = trans_last
end
if trans_authorlink then
local trans_authorparam = trans_first
and ("|%s |%s"):format(trans_first_fullname, trans_last_fullname)
or "|" .. trans_last_fullname
trans_author = make_author_with_url(
trans_author,
trans_authorparam,
trans_authorlink,
trans_authorlink_fullname
)
end
end
add(author)
if trans_author then
add(SPACE_LBRAC)
add(trans_author)
add(RBRAC)
end
end
author_outputted = true
return num_authorobjs
end
local function add_authorlike(
param,
prefix_with_preceding_authors,
suffix_without_preceding_authors,
suffix_if_multiple,
anonymous_suffix
)
local delimiter = author_outputted and "and" or ", "
local entities, _, num_entities = parse_and_format_multivalued_annotated_text_with_name(param, delimiter)
if not entities then
return
end
if is_anonymous(entities) then
-- If tlr=anonymous or similar given, display as "anonymous translator" or similar. If a specific
-- anonymous suffix not given, try to derive the anonymous suffix from the non-preceding-author suffix.
if not anonymous_suffix then
local cleaned_suffix = suffix_without_preceding_authors
:gsub(" ", " ")
:gsub(" ", " ")
:gsub(" ", " ")
:gsub("[", "[")
:gsub("]", "]")
cleaned_suffix = mw.text.trim(cleaned_suffix)
if not anonymous_suffix then
anonymous_suffix = " " .. cleaned_suffix:match("^, (.*)$")
end
if not anonymous_suffix then
anonymous_suffix = " " .. cleaned_suffix:match("^%((.*)%)$")
end
if not anonymous_suffix then
anonymous_suffix = " " .. cleaned_suffix:match("^%[(.*)%]$")
end
if not anonymous_suffix then
anonymous_suffix = suffix_without_preceding_authors
end
end
add_with_sep("anonymous" .. anonymous_suffix)
elseif prefix_with_preceding_authors and (author_outputted or not suffix_without_preceding_authors) then
add_with_sep(prefix_with_preceding_authors .. entities)
elseif suffix_if_multiple and num_entities > 1 then
add_with_sep(entities .. suffix_if_multiple)
else
add_with_sep(entities .. suffix_without_preceding_authors)
end
author_outputted = true
end
local function add_authorlabel()
local default_authorlabel = a("default-authorlabel")
if default_authorlabel and yesno(a("authorlabel"), true) then
sep = nil
add_with_sep(" " .. default_authorlabel)
end
end
local function has_new_title_or_author()
return args["2ndauthor"]
or args["2ndlast"]
or args.chapter2
or args.title2
or args.tlr2
or args.mainauthor2
or args.editor2
or args.editors2
or args.compiler2
or args.compilers2
or args.director2
or args.directors2
end
local function has_newversion()
return args.newversion or args.location2 or has_new_title_or_author()
end
-- Handle chapter=, section=, etc. `param` is the base name of the parameter in question, e.g. "chapter" or
-- "section". If numeric (either Arabic or Roman), add `numeric_prefix`; otherwise, parse as textual (allowing for
-- language prefixes, inline modifiers, etc.), prefix with `textual_prefix` (if given) and suffix with
-- `textual_suffix` (if given). Also checks for and handles the following (assuming param == "chapter"):
-- * chapterurl=: URL of the chapter.
-- * trans-chapter=: Chapter translation (can be given using an inline modifier <t:...>).
-- * chapter_number=: Chapter number, when chapter= is also given (otherwise put the chapter number in chapter=).
-- * chapter_plain=: Plain version of the chapter number; the "chapter " prefix isn't added.
-- * chapter_series=: Series that the chapter is within (used e.g. for journal articles part of a series).
-- * chapter_seriesvolume=: Volume of the series (compare seriesvolume=).
--
-- Returns nil if no value specified for the main parameter, otherwise the formatted value.
local function format_chapterlike(param, numeric_prefix, textual_prefix, textual_suffix)
local chap, chap_fullname = a_with_name(param)
local chap_num, chap_num_fullname = a_with_name(param .. "_number")
local chap_plain, chap_plain_fullname = parse_and_format_annotated_text_with_name(param .. "_plain")
if chap_num and chap_plain then
error(("Specify only one of |%s= or %s="):format(chap_num_fullname, chap_plain_fullname))
end
local chap_series, chap_series_fullname =
parse_and_format_annotated_text_with_name(param .. "_series", tag_with_cite, tag_with_cite)
local chap_seriesvolume, chap_seriesvolume_fullname =
parse_and_format_annotated_text_with_name(param .. "_seriesvolume")
if chap_series then
chap_series = ", " .. chap_series
end
if chap_seriesvolume then
if not chap_series then
error(("Cannot specify |%s= without %s="):format(chap_series_fullname, chap_seriesvolume_fullname))
end
chap_series = chap_series .. " (" .. chap_seriesvolume .. ")"
end
local chapterurl
local function make_chapter_with_url(chap)
if chapterurl then
return "[" .. chapterurl .. " " .. chap .. "]"
else
return chap
end
end
if not chap then
if chap_num then
error(
("Cannot specify |%s= without |%s=; put the numeric value in |%s= directly"):format(
chap_num_fullname,
chap_fullname,
chap_fullname
)
)
end
if chap_plain then
chapterurl = aurl(param .. "url")
return make_chapter_with_url(chap_plain .. (chap_series or ""))
end
return nil
end
local cleaned_chap = chap:gsub("<sup>[^<>]*</sup>", ""):gsub("[*+#]", "")
chapterurl = aurl(param .. "url")
local formatted
if numeric_prefix and get_number(cleaned_chap) then
-- Arabic chapter number
formatted = numeric_prefix .. make_chapter_with_url(chap)
elseif
numeric_prefix
and cleaned_chap:match("^[mdclxviMDCLXVI]+$")
and roman_to_arabic(cleaned_chap, true)
and (not overrides[param] or not overrides[param].noroman)
then
-- Roman chapter number
formatted = numeric_prefix .. make_chapter_with_url(upper(chap))
else
-- strip leading ! (used to force text-mode for titles that look like Roman numerals like "mil" and "dill")
if chap:find("^!") then
chap = chap:gsub("^!", "")
end
-- Must be a chapter name
local chapterobj = parse_annotated_text(chap, chap_fullname, a("trans-" .. param))
chapterobj.text = make_chapter_with_url(chapterobj.text)
chapterobj.link = make_chapter_with_url(chapterobj.link)
formatted = (textual_prefix or "") .. format_annotated_text(chapterobj) .. (textual_suffix or "")
end
if chap_num or chap_plain then
-- NOTE: Up above we throw an error if both chap_num and chap_plain are specified.
formatted = formatted .. " (" .. (chap_plain or numeric_prefix .. chap_num) .. ")"
end
if chap_series then
formatted = formatted .. chap_series
end
return formatted
end
-- This handles everything after displaying the author, starting with the chapter and ending with page, column,
-- line and then other=. It is currently called twice: Once to handle the main portion of the citation, and once to
-- handle a "newversion" citation. `ind` is either "" for the main portion or a number (currently only 2) for a
-- "newversion" citation. In a few places we conditionalize on `ind` to take actions depending on its value.
local function postauthor(ind, num_authors, format_as_cite)
get_full_paramname = make_get_full_paramname(ind)
if author_outputted then
add_authorlabel()
end
local coauthors = parse_and_format_multivalued_annotated_text("coauthors", "and")
if coauthors then
local with_prefix = ""
if author_outputted then
with_prefix = "dengan "
if num_authors == 1 then
sep = " "
end
end
add_with_sep(with_prefix .. coauthors)
author_outputted = true
end
add_authorlike("quotee", "memetik ", ", pemetik", ", pemetik")
if format_as_cite and author_outputted and not date_outputted then
add_date()
sep = ", "
end
add_authorlike("chapter_tlr", "diterjemah oleh ", ", penterjemah", nil, " penterjemah")
local function add_sg_and_pl_authorlike(noun, verbed)
local sgparam = noun
local plparam = noun .. "s"
local sgval, sgval_fullname = a_with_name(sgparam)
local plval, plval_fullname = a_with_name(plparam)
if sgval and plval then
error(("Can't specify both |%s= and |%s="):format(sgval_fullname, plval_fullname))
end
if sgval or plval then
local verbed_by = verbed .. " oleh "
local comma_sgnoun = ", " .. noun
local comma_plnoun = ", " .. noun .. "s"
add_authorlike(sgparam, verbed_by, comma_sgnoun, comma_plnoun)
add_authorlike(plparam, verbed_by, comma_plnoun)
end
end
local formatted_entry = format_chapterlike("entry", nil, "“", "”")
local formatted_chapter = format_chapterlike("chapter", "chapter ", "“", "”")
local function add_entry()
if formatted_entry then
add_with_sep(formatted_entry)
if not a("notitle") then
add("dalam ")
author_outputted = false
else
author_outputted = true
end
formatted_entry = nil
end
end
local function add_chapter()
add_entry()
if formatted_chapter then
add_with_sep(formatted_chapter)
if not a("notitle") then
add("dalam ")
author_outputted = false
end
formatted_chapter = nil
end
end
local function add_actor_role(format_as_cite)
local role = parse_and_format_multivalued_annotated_text("role", "and")
local actor_val, actor_fullname = a_with_name("actor")
local actor_objs = parse_multivalued_annotated_text(actor_val, actor_fullname)
local actor = format_multivalued_annotated_text(actor_objs, "and")
if format_as_cite then
if role then
if actor then
add_with_sep(actor .. " sebagai ")
end
sep = nil
add_with_sep(role)
elseif actor then
add_with_sep(actor .. " (" .. (#actor_objs > 1 and "actors" or "actor") .. ")")
end
else
if role then
add_with_sep("diucapkan oleh " .. role)
if actor then
sep = nil
add_with_sep(" (" .. actor .. ")")
end
elseif actor then
add_with_sep(actor .. " (" .. (#actor_objs > 1 and "actors" or "actor") .. ")")
end
end
end
if format_as_cite then
if date_outputted then
add_chapter()
end
local output_len = #output
local mainauthor = parse_and_format_multivalued_annotated_text("mainauthor")
if mainauthor then
add_with_sep(mainauthor)
end
-- quote-* templates display "jobbed by name" after the author, controlled by the author_outputted flag
author_outputted = false
add_authorlike("tlr", "diterjemah oleh ", ", penterjemah", nil, " penterjemah")
author_outputted = false
add_sg_and_pl_authorlike("editor", "edited")
add_sg_and_pl_authorlike("compiler", "compiled")
add_sg_and_pl_authorlike("director", "directed")
add_authorlike("lyricist", nil, " (lirik)", nil, " penulis lirik")
add_authorlike("lyrics-translator", nil, " (terjemahan)", nil, " penterjemah lirik")
add_authorlike("composer", nil, " (muzik)", nil, " penggubal")
add_actor_role("format_as_cite")
-- if the output length has changed, a credit name has been printed
-- and we can print the date
if output_len ~= #output then
author_outputted = true
add_date()
end
add_chapter()
else
add_chapter()
local mainauthor = parse_and_format_multivalued_annotated_text("mainauthor")
if mainauthor then
add_with_sep(mainauthor)
author_outputted = true
end
add_authorlike("tlr", "diterjemah oleh ", ", penterjemah", nil, " penterjemah")
add_sg_and_pl_authorlike("editor", "edited")
add_sg_and_pl_authorlike("compiler", "compiled")
add_sg_and_pl_authorlike("director", "directed")
add_authorlike("lyricist", nil, " (lirik)", nil, " penulis lirik")
add_authorlike("lyrics-translator", nil, " (terjemahan)", nil, " penterjemah lirik")
add_authorlike("composer", nil, " (muzik)", nil, " penggubal")
end
local title, title_fullname = a_with_name("title")
local need_comma = false
if title then
local titleobj = parse_annotated_text(title, title_fullname, a("trans-title"))
add(format_annotated_text(titleobj, tag_with_cite, tag_with_cite))
local series = parse_and_format_annotated_text("series")
if series then
add(" (" .. series)
local seriesvolume = parse_and_format_annotated_text("seriesvolume")
if seriesvolume then
add(SEMICOLON_SPACE .. seriesvolume)
end
add(")")
end
need_comma = true
elseif ind == "" then
if not a("notitle") then
add(maintenance_line("Please provide the book title or journal name"))
need_comma = true
end
end
local archiveurl, archiveurl_fullname = aurl_with_name("archiveurl")
local url, url_fullname = aurl_with_name("url")
local urls, urls_fullname = aurl_with_name("urls")
if url and urls then
error(("Supply only one of |%s= and |%s="):format(url_fullname, urls_fullname))
end
local function verify_title_supplied(url_name)
-- There are too many cases of this to throw an error at this time.
-- if not title then
-- error(("If |%s= is given, |%s= must also be supplied"):format(url_name, title_fullname))
-- end
end
if archiveurl or url then
verify_title_supplied(archiveurl and archiveurl_fullname or url_fullname)
sep = nil
add("‎<sup>[" .. (archiveurl or url) .. "]</sup>")
elseif urls then
verify_title_supplied(urls_fullname)
sep = nil
add("‎<sup>" .. urls .. "</sup>")
end
-- display (in Language) if language is provided and is not English and not overriden by termlang or worklang
if format_as_cite and ind == "" and not (args.termlang or args.worklang) and (args[1] or args.lang) then
local lang = get_first_lang(args[1] or args.lang)
if lang then
local langcode = lang:getCode()
if not (langcode == "und" or langcode == "ms") then
local langs = format_langs(args[1] or args.lang)
if langs then
add(" (dalam bahasa " .. langs .. ")")
end
end
end
end
if need_comma then
sep = ", "
end
local edition, edition_fullname = parse_and_format_annotated_text_with_name("edition")
local edition_plain, edition_plain_fullname = parse_and_format_annotated_text_with_name("edition_plain")
if edition and edition_plain then
error(("Supply only one of |%s= and |%s="):format(edition_fullname, edition_plain_fullname))
end
if edition then
add_with_sep("edisi " .. edition)
end
if edition_plain then
add_with_sep(edition_plain)
end
-- Display a numeric param such as page=, volume=, column=. For each `paramname`, four params are actually
-- recognized, e.g. for paramname == "page", the params page=, pages=, page_plain= and pageurl= are recognized
-- and checked (or the same with an index, e.g. page2=, pages2=, page_plain2= and pageurl2= respectively if
-- ind == "2"). Only one of the first three can be specified; an error results if more than one are given.
-- If none are given, the return value is nil; otherwise it is a string. The numeric spec is taken directly
-- from e.g. page_plain= if given; otherwise if e.g. pages= is given, or if page= is given and looks like a
-- combination of numbers (i.e. it has a hyphen or dash in it, a comma, or the word " and "), it is prefixed
-- by `singular_desc` + "s" (e.g. "pages "), otherwise it is prefixed by just `singular_desc` (e.g. "page ").
-- (As a special case, if either e.g. page=unnumbered or pages=unnumbered is given, the numeric spec is
-- "unnumbered page".) The resulting spec is returned directly unless e.g. pageurl= is given, in which case
-- it is linked to the specified URL. Note that any of the specs can be foreign text, e.g. foreign numbers
-- (including with optional inline modifiers), and such text is handled appropriately.
local function format_numeric_param(paramname, singular_desc)
local sgval = a_with_name(paramname)
local sgobj = parse_annotated_text(sgval, paramname)
local plparamname = paramname .. "s"
local plval = a_with_name(plparamname)
local plobj = parse_annotated_text(plval, plparamname)
local plainval, plain_fullname = parse_and_format_annotated_text_with_name(paramname .. "_plain")
local numspec
if not sgval and not plval and not plainval then
return
elseif plainval and (sgval or plval) then
error(("Can't specify " .. plain_fullname .. " with " .. paramname .. " or " .. plparamname))
elseif sgval and plval then
-- if both singular and plural, display "page 1 of 1-10"
numspec = singular_desc .. " " .. sgval .. " daripada " .. plval
else
-- Merge page= and pages= and treat alike because people often mix them up in both directions.
if plainval then
numspec = plainval
else
local val = sgobj and sgobj.text or plobj.text
if val == "unnumbered" then
numspec = "unnumbered " .. singular_desc
else
local desc
if val:find("^!") then
val = val:gsub("^!", "")
desc = sgval and singular_desc or pluralize(singular_desc)
else
local check_val = val
if check_val:find("%[") then
check_val = remove_links(check_val)
-- convert URL's of the form [URL DISPLAY] to the displayed value
check_val = check_val:gsub("%[[^ %[%]]* ([^%[%]]*)%]", "%1")
end
-- in case of negative page numbers (do they exist?), don't treat as multiple pages
check_val = check_val:gsub("^%-", "")
-- replace HTML entity en-dashes and em-dashes with their literal codes
check_val = check_val:gsub("–", "–")
check_val = check_val:gsub("–", "–")
check_val = check_val:gsub("—", "—")
check_val = check_val:gsub("—", "—")
-- Check for en-dash or em-dash, or two numbers (possibly with stuff after like 12a-15b)
-- separated by a hyphen or by comma a followed by a space (to avoid firing on thousands separators).
if
umatch(check_val, "[–—]")
or check_val:find(" and ")
or check_val:match("%d+[^ ]* *%- *%d+")
or check_val:match("%d+[^ ]* *, +%d+")
then
desc = pluralize(singular_desc)
else
desc = singular_desc
end
end
local obj = sgobj or plobj
obj.text = val
if obj.link:find("^!") then
obj.link = obj.link:gsub("^!", "")
end
val = format_annotated_text(obj)
numspec = desc .. " " .. val
end
end
end
local url = a(paramname .. "url")
if url then
return "[" .. url .. " " .. numspec .. "]"
else
return numspec
end
end
local volume = format_numeric_param("volume", a("volume_prefix") or "volume")
if volume then
add_with_sep(volume)
end
local issue = format_numeric_param("issue", a("issue_prefix") or "number")
if issue then
add_with_sep(issue)
end
-- number= is an alias for issue= (except in {{quote-av}}, where it is the episode number)
local number = format_numeric_param("number", a("number_prefix") or "number")
if number then
add_with_sep(number)
end
local annotations = {}
local genre = a("genre")
if genre then
insert(annotations, genre)
end
local format = a("format")
if format then
insert(annotations, format)
end
local medium = a("medium")
if medium then
insert(annotations, medium)
end
-- Now handle the display of language annotations like "(in French)" or
-- "(quotation in Nauruan; overall work in German)".
local quotelang = args[1] or args.lang
if not quotelang then
if ind == "" then
-- This can only happen for certain non-mainspace pages, e.g. Talk pages; otherwise an error is thrown
-- above.
insert(annotations, maintenance_line("Please specify the language of the quote using |1="))
else
-- do nothing in newversion= portion
end
elseif ind == "" then
local worklang = a("worklang")
local termlang = a("termlang")
worklang = worklang or quotelang
termlang = termlang or quotelang
if worklang == quotelang then
if worklang == termlang then
-- do nothing
else
insert(annotations, "dalam " .. format_langs(quotelang))
end
else
if quotelang ~= termlang then
insert(annotations, "petikan dalam bahasa " ..format_langs(quotelang))
end
insert(annotations, "karya secara keseluruhannya dalam bahasa " .. format_langs(worklang))
end
else
local lang2 = a("lang2")
if lang2 then
insert(annotations, "dalam " .. format_langs(lang2))
end
end
if #annotations > 0 then
sep = nil
add_with_sep(" (" .. concat(annotations, SEMICOLON_SPACE) .. ")")
end
local artist = parse_and_format_multivalued_annotated_text("artist", "and")
if artist then
add_with_sep("performed by " .. artist)
end
local feat = parse_and_format_multivalued_annotated_text("feat", "and")
if feat then
sep = " "
add_with_sep("ft. " .. feat)
end
if not format_as_cite then
add_actor_role()
end
local others = parse_and_format_annotated_text("others")
if others then
add_with_sep(others)
end
local quoted_in = parse_and_format_annotated_text("quoted_in", tag_with_cite, tag_with_cite)
if quoted_in then
add_with_sep("dipetik dalam " .. quoted_in)
insert(tracking_categories, "Quotations using quoted-in parameter")
end
local location = parse_and_format_multivalued_annotated_text("location", "; ")
local publisher = parse_and_format_multivalued_annotated_text("publisher", "; ")
if publisher then
if location then
add_with_sep(location) -- colon
sep = ": " -- colon
end
add_with_sep(publisher)
elseif location then
add_with_sep(location)
end
if not date_outputted then
add_date("no_paren")
end
local source = parse_and_format_multivalued_annotated_text("source", "and")
if source then
add_with_sep("sourced from " .. source)
end
local original = parse_and_format_annotated_text("original", tag_with_cite, tag_with_cite)
local by = parse_and_format_multivalued_annotated_text("by", "and")
local origtype = a("deriv") or "translation"
if original or by then
add_with_sep(origtype .. " of " .. (original or "original") .. (by and " by " .. by or ""))
end
-- Handle origlang=, origworklang=. How we handle them depends on whether the original title or author are explicitly
-- given.
local origlang = a("origlang")
local origworklang = a("origworklang")
local origlangtext, origworklangtext
if origlang then
origlangtext = "dalam " .. format_langs(origlang)
end
if origworklang then
origworklangtext = "overall work in " .. format_langs(origworklang)
end
if origlang or origworklang then
if original or by then
local orig_annotations = {}
if origlangtext then
insert(orig_annotations, origlangtext)
end
if origworklangtext then
insert(orig_annotations, origworklangtext)
end
sep = nil
add_with_sep(" (" .. concat(orig_annotations, SEMICOLON_SPACE) .. ")")
else
add_with_sep(origtype .. " of original" .. (origlangtext and " " .. origlangtext or ""))
if origworklangtext then
sep = nil
add_with_sep(" (" .. origworklangtext .. ")")
end
end
end
if ind ~= "" and has_newversion() then
local formatted_new_date = format_date_args(a, get_full_paramname, alias_map, "", "", nil, "Please provide a date or year", true)
if formatted_new_date then
add_with_sep(formatted_new_date)
end
end
-- Fetch date_published=/year_published=/month_published= and format appropriately.
local formatted_date_published = format_date_args(a, get_full_paramname, alias_map, "", "_published", nil, nil, true)
local platform = parse_and_format_multivalued_annotated_text("platform", "and")
if formatted_date_published then
add_with_sep("published " .. formatted_date_published .. (platform and " via " .. platform or ""))
elseif platform then
add_with_sep("via " .. platform)
end
-- From here on out, there should always be a preceding item, so we
-- can dispense with add_with_sep() and always insert the comma.
sep = nil
local function small(txt)
add(", <small>")
add(txt)
add("</small>")
end
-- Add an identifier to a book or article database such as DOI, ISBN, JSTOR, etc. `param_or_params`
-- is a string identifying the base param, or a list of such strings to check in turn. If found, the value
-- of the parameter is processed using `process` (a function of one argument, defaulting to mw.uri.encode()),
-- and then the actual URL to insert is generated by preceding with `pretext`, following with `posttext`,
-- and running the resulting string through small(), which first adds a comma and then the URL in small font.
local function add_identifier(param_or_params, pretext, posttext, process)
local val = a(param_or_params)
if val then
val = (process or mw.uri.encode)(val)
small(pretext .. val .. posttext)
end
end
add_identifier("bibcode", "[https://adsabs.harvard.edu/abs/", " →Bibcode]")
add_identifier("doi", '<span class="neverexpand">[https://doi.org/', " →DOI]</span>")
add_identifier("isbn", "", "", isbn)
add_identifier("issn", "", "", issn)
add_identifier("jstor", "[https://www.jstor.org/stable/", " →JSTOR]")
add_identifier("lccn", "", "", lccn)
add_identifier("oclc", "[https://search.worldcat.org/title/", " →OCLC]")
add_identifier("ol", "[https://openlibrary.org/works/OL", "/ →OL]")
add_identifier("pmid", "[https://www.ncbi.nlm.nih.gov/pubmed/", " →PMID]")
add_identifier("pmcid", "[https://www.ncbi.nlm.nih.gov/pmc/articles/", "/ →PMCID]")
add_identifier("ssrn", "[https://ssrn.com/abstract=", " →SSRN]")
-- add_identifier("urn", "", "", urn)
local id = a("id")
if id then
small(id)
end
archiveurl, archiveurl_fullname = aurl_with_name("archiveurl")
if archiveurl then
add(", diarkibkan daripada ")
local url, url_fullname = aurl_with_name("url")
if not url then
-- attempt to infer original URL from archive URL; this works at
-- least for Wayback Machine (web.archive.org) URL's
url = archiveurl:match("/(https?:.*)$")
if not url then
error(
("When |%s= is specified, |%s= must also be included"):format(archiveurl_fullname, url_fullname)
)
end
end
add("[" .. url .. " yang asal] pada ")
local archivedate, archivedate_fullname = a_with_name("archivedate")
if archivedate then
add(format_date(archivedate))
elseif string.sub(archiveurl, 1, 28) == "https://web.archive.org/web/" then
-- If the archive is from the Wayback Machine, then it already contains the date
-- Get the date and format it
local wayback_date = string.sub(archiveurl, 29, 29 + 7)
wayback_date = string.sub(wayback_date, 1, 4)
.. "-"
.. string.sub(wayback_date, 5, 6)
.. "-"
.. string.sub(wayback_date, 7, 8)
add(format_date(wayback_date))
else
error(
("When |%s= is specified, |%s= must also be included"):format(
archiveurl_fullname,
archivedate_fullname
)
)
end
end
if a("accessdate") then
--Otherwise do not display here, as already used as a fallback for missing date= or year= earlier.
if (a("date") or a("nodate") or a("year")) and not a("archivedate") then
add(", retrieved " .. format_date(a("accessdate")))
end
end
local formatted_section = format_chapterlike("section", "section ")
if formatted_section then
add(", ")
add(formatted_section)
end
-- video game stuff
local system = parse_and_format_annotated_text("system")
if system then
add(", " .. system)
end
local scene = parse_and_format_annotated_text("scene")
if scene then
add(", scene: " .. scene)
end
local level = parse_and_format_annotated_text("level")
if level then
add(", level/area: " .. level)
end
local note = parse_and_format_annotated_text("note")
if note then
add(", " .. note)
end
local note_plain = parse_and_format_annotated_text("note_plain")
if note_plain then
add(" " .. note_plain)
end
-- Wrapper around format_numeric_param that inserts the formatted text with optional preceding text.
local function handle_numeric_param(paramname, singular_desc, pretext)
local numspec = format_numeric_param(paramname, singular_desc)
if numspec then
add((pretext or "") .. numspec)
end
end
handle_numeric_param("page", a("page_prefix") or "page", ", ")
handle_numeric_param("column", a("column_prefix") or "column", ", ")
handle_numeric_param("line", a("line_prefix") or "line", ", ")
-- FIXME: Does this make sense? What is other=?
local other = parse_and_format_annotated_text("other")
if other then
add(", " .. other)
end
end
local function add_authors(args, last_first)
-- Find maximum indexed author or last name.
local maxind = math.max(args.author.maxindex, args.last.maxindex)
-- Include max index of ancillary params so we get an error message about their use without the primary params.
local ancillary_params =
{"trans-author", "authorlink", "trans-authorlink", "first", "trans-first", "trans-last"}
for _, ancillary in ipairs(ancillary_params) do
maxind = math.max(maxind, args[ancillary].maxindex)
end
local num_authors = 0
for i = 1, maxind do
local ind = i == 1 and "" or i
local author, last = args.author[i], args.last[i]
if author or last then
local this_num_authors = add_author(
author,
"author" .. ind,
args["trans-author"][i],
"trans-author" .. ind,
args.authorlink[i],
"authorlink" .. ind,
args["trans-authorlink"][i],
"trans-authorlink" .. ind,
args.first[i],
"first" .. ind,
args["trans-first"][i],
"trans-first" .. ind,
last,
"last" .. ind,
args["trans-last"][i],
"trans-last" .. ind,
last_first
)
num_authors = num_authors + this_num_authors
sep = last_first and "; " or ", "
else
for _, cant_have in ipairs(ancillary_params) do
if args[cant_have][i] then
error(("Can't have |%s%s= without |author%s= or |last%s="):format(cant_have, ind, ind, ind))
end
end
end
end
return num_authors
end
local function add_newversion()
-- If there's a "newversion" section, add the new-version text.
if has_newversion() then
sep = nil
--Test for new version of work.
add(SEMICOLON_SPACE)
if args.newversion then -- newversion= is intended for English text, e.g. "quoted in" or "republished as".
add(args.newversion)
elseif not args.edition2 then
if has_new_title_or_author() then
add("diterbitkan semula sebagai")
else
add("diterbitkan semula")
end
end
add(" ")
return ""
else
return ", "
end
end
------------------- Now we start outputting text ----------------------
local need_comma = false
-- Set this now so a() works just below.
get_full_paramname = make_get_full_paramname("")
if args.brackets then
add("[")
end
bold_year = not format_as_cite
year_last = format_as_cite
formatted_date, need_date = format_date_args(
a,
get_full_paramname,
alias_map,
nil,
nil,
bold_year,
"Can we [[:Category:Requests for date|date]] this quote?",
year_last
)
-- Fetch origdate=/origyear=/origmonth= and format appropriately.
formatted_origdate = format_date_args(a, get_full_paramname, alias_map, "orig", nil, nil, nil, year_last)
local num_authors, need_date
if format_as_cite then
num_authors = add_authors(args, "last_first")
if author_outputted then
sep = " "
end
-- Display all the text that comes after the author, for the main portion.
postauthor("", num_authors, "format_as_cite")
author_outputted = false
sep = add_newversion()
-- Add the newversion author(s).
if args["2ndauthor"] or args["2ndlast"] then
num_authors = add_author(
args["2ndauthor"],
"2ndauthor",
nil,
nil,
args["2ndauthorlink"],
"2ndauthorlink",
nil,
nil,
args["2ndfirst"],
"2ndfirst",
nil,
nil,
args["2ndlast"],
"2ndlast",
nil,
nil,
"last_first"
)
sep = ", "
else
for _, cant_have in ipairs{"2ndauthorlink", "2ndfirst"} do
if args[cant_have] then
error(("Can't have |%s= without |2ndauthor= or |2ndlast="):format(cant_have))
end
end
end
-- Display all the text that comes after the author, for the "newversion" section.
postauthor(2, num_authors, "format_as_cite")
else
if formatted_date then
need_comma = true
add(formatted_date)
end
if formatted_origdate then
need_comma = true
add(SPACE_LBRAC .. formatted_origdate .. RBRAC)
end
if need_comma then
sep = ", "
end
date_outputted = true
num_authors = add_authors(args)
-- Display all the text that comes after the author, for the main portion.
postauthor("", num_authors)
author_outputted = false
sep = add_newversion()
-- Add the newversion author(s).
if args["2ndauthor"] or args["2ndlast"] then
num_authors = add_author(
args["2ndauthor"],
"2ndauthor",
nil,
nil,
args["2ndauthorlink"],
"2ndauthorlink",
nil,
nil,
args["2ndfirst"],
"2ndfirst",
nil,
nil,
args["2ndlast"],
"2ndlast",
nil,
nil
)
sep = ", "
else
for _, cant_have in ipairs{"2ndauthorlink", "2ndfirst"} do
if args[cant_have] then
error(("Can't have |%s= without |2ndauthor= or |2ndlast="):format(cant_have))
end
end
end
-- Display all the text that comes after the author, for the "newversion" section.
postauthor(2, num_authors)
end
if not args.nocolon then
sep = nil
add(":")
elseif args.usenodot and not args.nodot then
add(".")
end
-- Concatenate output portions to form output text.
local output_text = concat(output)
-- Remainder of code handles adding categories. We add one or more of the following categories:
--
-- 1. [[Category:LANG terms with quotations]], based on the first language code in termlang= or 1=. Added to
-- mainspace, Reconstruction: and Appendix: pages as well as Citations: pages if the corresponding mainspace
-- page exists. Not added if nocat= is given. Note that [[Module:usex]] adds the same category using the same
-- logic, but we do it here too because we may not have a quotation to format. (We add in those circumstances
-- because typically when there's no quotation to format, it's because it's formatted manually underneath the
-- citation, or using {{ja-x}}, {{th-x}} or similar.)
-- 2. [[Category:Requests for date in LANG entries]], based on the first language code in 1=. Added to mainspace,
-- Reconstruction:, Appendix: and Citations: pages unless nocat= is given.
-- 3. [[Category:Quotations using nocat parameter]], if nocat= is given. Added to mainspace, Reconstruction:,
-- Appendix: and Citations: pages.
local categories = {}
local termlang = get_first_lang(args.termlang or argslang)
if args.nocat then
if format_as_cite then
insert(tracking_categories, "Citations using nocat parameter")
else
insert(tracking_categories, "Quotations using nocat parameter")
end
else
-- don't create language tracking categories for citations
if not format_as_cite then
local title
if args.pagename then -- for testing, doc pages, etc.
title = mw.title.new(args.pagename)
if not title then
error(("Bad value for `args.pagename`: '%s'"):format(args.pagename))
end
else
title = mw.title.getCurrentTitle()
end
-- Only add [[Citations:foo]] to [[:Category:LANG terms with quotations]] if [[foo]] exists.
local ok_to_add_cat
if title.nsText ~= "Citations" then
ok_to_add_cat = true
else
local mainspace_title = mw.title.new(title.text)
if mainspace_title and mainspace_title.exists then
ok_to_add_cat = true
end
end
if ok_to_add_cat then
insert(categories, termlang:getFullName() .. " terms with quotations")
end
end
if need_date then
local argslangobj = get_first_lang(argslang)
insert(categories, "Requests for date in " .. argslangobj:getCanonicalName() .. " entries")
end
end
return output_text .. (
not lang and "" or
(#categories > 0 and format_categories(categories, lang, args.sort) or "") .. (
#tracking_categories > 0
and format_categories(
tracking_categories,
lang,
args.sort,
nil,
not page_should_be_ignored(mw.title.getCurrentTitle().fullText)
)
or ""
)
)
end
-- Alias specs for type= and type2=. Each spec is `{canon, aliases, with_newversion}` where `canon` is the canonical
-- parameter (with "2" added if type2= is being handled), `aliases` is a comma-separated string of aliases (with "2"
-- added if type2= is being handled, except for numeric params), and `with_newversion` indicates whether we should
-- process this spec if type2= is being handled.
local type_alias_specs = {
av = {
{"author", "writer,writers", true},
{"chapter", "episode", true},
{"chapterurl", "episodeurl", true},
{"trans-chapter", "trans-episode", true},
{"chapter_tlr", "episode_tlr,episode_tlrs,episode_translator,episode_translators", true},
{"chapter_series", "episode_series", true},
{"chapter_seriesvolume", "episode_seriesvolume", true},
{"chapter_number", "episode_number", true},
{"chapter_plain", "episode_plain", true},
{"volume", "season", true},
{"volumes", "seasons", true},
{"volume_plain", "season_plain", true},
{"volumeurl", "seasonurl", true},
{"platform", "network", true},
},
book = {
{"author", "3"},
{"title", "4"},
{"url", "5"},
{"year", "2"},
{"page", "6"},
{"text", "7"},
{"t", "8"},
},
journal = {
{"year", "2"},
{"author", "3"},
{"chapter", "title,article,4", true},
{"chapterurl", "titleurl,articleurl", true},
{"trans-chapter", "trans-title,trans-article", true},
{"chapter_tlr", "title_tlr,title_tlrs,title_translator,title_translators,article_tlr,article_tlrs,article_translator,article_translators", true},
{"chapter_series", "title_series,article_series", true},
{"chapter_seriesvolume", "title_seriesvolume,article_seriesvolume", true},
{"chapter_number", "title_number,article_number", true},
{"chapter_plain", "title_plain,article_plain", true},
{"title", "journal,magazine,newspaper,work,5", true},
{"trans-title", "trans-journal,trans-magazine,trans-newspaper,trans-work", true},
{"tlr", "journal_tlr,journal_tlrs,journal_translator,journal_translators,magazine_tlr,magazine_tlrs,magazine_translator,magazine_translators,newspaper_tlr,newspaper_tlrs,newspaper_translator,newspaper_translators,work_tlr,work_tlrs,work_translator,work_translators", true},
{"url", "6,journalurl,magazineurl,newspaperurl,workurl", true},
{"page", "7"},
{"source", "newsagency,news_agency", true},
{"text", "8"},
{"t", "9"},
},
}
-- Process interally-handled aliases related to type= or type2=. `args` is a table of arguments; `typ` is the value of
-- type= or type2=; newversion=true if we're dealing with type2=; alias_map is used to keep track of alias mappings
-- seen.
local function process_type_aliases(args, typ, newversion, alias_map)
local ind = newversion and "2" or ""
local deprecated = ine(args.lang)
if not type_alias_specs[typ] then
local possible_values = {}
for possible, _ in pairs(type_alias_specs) do
insert(possible_values, possible)
end
sort(possible_values)
error(
("Unrecognized value '%s' for type%s=; possible values are %s"):format(
typ,
ind,
concat(possible_values, ",")
)
)
end
for _, alias_spec in ipairs(type_alias_specs[typ]) do
local canon, aliases, with_newversion = unpack(alias_spec)
if with_newversion or not newversion then
canon = canon .. ind
aliases = split(aliases, ",", true)
local saw_alias = nil
for _, alias in ipairs(aliases) do
if alias:match("^%d+$") then
alias = tonumber(alias)
if deprecated then
alias = alias - 1
end
else
alias = alias .. ind
end
if args[alias] then
if saw_alias == nil then
saw_alias = alias
else
error(("|%s= and |%s= are aliases; cannot specify a value for both"):format(saw_alias, alias))
end
end
end
if saw_alias and (not newversion or type(saw_alias) == "string") then
if args[canon] then
error(("|%s= is an alias of |%s=; cannot specify a value for both"):format(saw_alias, canon))
end
args[canon] = args[saw_alias]
-- Wipe out the original after copying. This is important in case of a param that has general significance
-- but has been redefined (e.g. {{quote-av}} redefines number= for the episode number, and
-- {{quote-journal}} redefines title= for the chapter= (article). It's also important due to unhandled
-- parameter checking.
args[saw_alias] = nil
alias_map[canon] = saw_alias
end
end
end
end
-- Clone and combine frame's and parent's args while also assigning nil to empty strings. Handle aliases and ignores.
local function clone_args(direct_args, parent_args)
local args = {}
-- Processing parent args must come first so that direct args override parent args. Note that if a direct arg is
-- specified but is blank, it will still override the parent arg (with nil).
for pname, param in pairs(parent_args) do
-- [[Special:WhatLinksHere/Wiktionary:Tracking/quote/param/PARAM]]
track("param/" .. pname)
args[pname] = ine(param)
end
-- Process ignores. The value of `ignore` is a comma-separated list of parameter names to ignore (erase). We need to
-- do this before aliases due to {{quote-song}}, which sets chapter= to the value of title= in the direct params and
-- sets title= to the value of album= using an alias. If we do the ignores after aliases, we get an error during alias
-- processing, saying that title= and its alias album= are both present.
local ignores = ine(direct_args.ignore)
if ignores then
for ignore in gsplit(ignores, "%s*,%s*") do
args[ignore] = nil
end
end
local alias_map = {}
local other_controls = {}
-- noroman
local noroman = ine(direct_args.noroman)
if noroman then
other_controls.overrides = other_controls.overrides or {}
for param in gsplit(noroman, "%s*,%s*") do
other_controls.overrides[param] = (other_controls.overrides[param] or {})
other_controls.overrides[param].noroman = true
end
end
-- Process internally-specified aliases using type= or type2=.
local typ = args.type or direct_args.type
if typ then
process_type_aliases(args, typ, false, alias_map)
end
local typ2 = args.type2 or direct_args.type2
if typ2 then
process_type_aliases(args, typ2, true, alias_map)
end
-- Process externally-specified aliases. The value of `alias` is a list of semicolon-separated specs, each of which
-- is of the form DEST:SOURCE,SOURCE,... where DEST is the canonical name of a parameter and SOURCE refers to an
-- alias. Whitespace is allowed between all delimiters. The order of aliases may be important. For example, for
-- {{quote-journal}}, title= contains the article name and is an alias of underlying chapter=, while journal= or
-- work= contains the journal name and is an alias of underlying title=. As a result, the title -> chapter alias
-- must be specified before the journal/work -> title alias.
--
-- Whenever we copy a value from argument SOURCE to argument DEST, we record an entry for the pair in alias_map, so
-- that when we would display an error message about DEST, we display SOURCE instead.
--
-- Do alias processing (and ignore and error_if processing) before processing direct_args so that e.g. we can set up
-- an alias of title -> chapter and then set title= to something else in the direct args ({{quote-hansard}} does
-- this).
--
-- FIXME: Delete this once we've converted all alias processing to internal.
local aliases = ine(direct_args.alias)
if aliases then
-- Allow and discard a trailing semicolon, to make managing multiple aliases easier.
aliases = ugsub(aliases, "%s*;$", "")
for alias_spec in gsplit(aliases, "%s*;%s*") do
local alias_spec_parts = split(alias_spec, "%s*:%s*")
if #alias_spec_parts ~= 2 then
error(("Alias spec '%s' should have one colon in it"):format(alias_spec))
end
local dest, sources = unpack(alias_spec_parts)
sources = split(sources, "%s*,%s*")
local saw_source = nil
for _, source in ipairs(sources) do
if source:match("^%d+$") then
source = tonumber(source)
end
if args[source] then
if saw_source == nil then
saw_source = source
else
error(("|%s= and |%s= are aliases; cannot specify a value for both"):format(saw_source, source))
end
end
end
if saw_source then
if args[dest] then
error(("|%s= is an alias of |%s=; cannot specify a value for both"):format(saw_source, dest))
end
args[dest] = args[saw_source]
-- Wipe out the original after copying. This important in case of a param that has general significance
-- but has been redefined (e.g. {{quote-av}} redefines number= for the episode number, and
-- {{quote-journal}} redefines title= for the chapter= (article). It's also important due to unhandled
-- parameter checking.
args[saw_source] = nil
alias_map[dest] = saw_source
end
end
end
-- Process error_if. The value of `error_if` is a comma-separated list of parameter names to throw an error if seen
-- in parent_args (they are params we overwrite in the direct args).
local error_ifs = ine(direct_args.error_if)
if error_ifs then
for error_if in gsplit(error_ifs, "%s*,%s*") do
if ine(parent_args[error_if]) then
error(
("Cannot specify a value |%s=%s as it would be overwritten or ignored"):format(
error_if,
ine(parent_args[error_if])
)
)
end
end
end
for pname, param in pairs(direct_args) do
-- ignore control params
if pname ~= "ignore" and pname ~= "alias" and pname ~= "error_if" and pname ~= "noroman" then
args[pname] = ine(param)
end
end
return args, alias_map, other_controls
end
local function get_args(frame_args, parent_args, require_lang)
-- FIXME: We are processing arguments twice, once in clone_args() and then again in [[Module:parameters]]. This is
-- wasteful of memory.
local cloned_args, alias_map, other_controls = clone_args(frame_args, parent_args)
local deprecated = ine(parent_args.lang)
local alias_of_t = {alias_of = "t"}
local boolean = {type = "boolean"}
local language_sublist = {type = "language", sublist = true}
local list_allow_holes = {list = true, allow_holes = true}
local script = {type = "script"}
-- First, the "single" params that don't have FOO2 or FOOn versions.
local params = {
[deprecated and "lang" or 1] = {required = require_lang, type = "language", sublist = true, default = "und"},
["lang2"] = language_sublist,
["newversion"] = true,
["author"] = list_allow_holes,
["2ndauthor"] = true,
["trans-author"] = list_allow_holes,
["authorlink"] = list_allow_holes,
["2ndauthorlink"] = true,
["trans-authorlink"] = list_allow_holes,
["first"] = list_allow_holes,
["2ndfirst"] = true,
["trans-first"] = list_allow_holes,
["last"] = list_allow_holes,
["2ndlast"] = true,
["trans-last"] = list_allow_holes,
["nocat"] = boolean,
["nocolon"] = boolean,
-- quote params
text = true,
passage = {alias_of = "text"},
tr = true,
transliteration = {alias_of = "tr"},
ts = true,
transcription = {alias_of = "ts"},
norm = true,
normalization = {alias_of = "norm"},
sc = script,
normsc = script,
sort = true,
subst = true,
footer = true,
lit = true,
t = true,
translation = alias_of_t,
gloss = alias_of_t,
lb = true,
brackets = boolean,
-- original quote params
origtext = true,
origtr = true,
origts = true,
orignorm = true,
origsc = script,
orignormsc = script,
origsubst = true,
origlb = true,
usenodot = boolean,
nodot = boolean,
inline = boolean,
asquote = boolean,
ascite = boolean,
}
-- Most params have PARAM2 variants for use with `newversion`. FIXME: We should generalize this to
-- allow PARAMN variants for any N.
local function add_with_2(param, value)
params[param] = value
params[param .. "2"] = value
end
local function alias_with_2(alias, canon)
params[alias] = {alias_of = canon}
params[alias .. "2"] = {alias_of = canon .. "2"}
end
for _, param12 in ipairs{
"worklang",
"termlang",
"origlang",
"origworklang"
} do
add_with_2(param12, language_sublist)
end
-- Then the newversion params (which have FOO2 versions).
for _, param12 in ipairs{
-- author-like params; author params themselves are either list params (author=, last=, etc.) or single params
-- (2ndauthor=, 2ndlast=, etc.)
"coauthors",
"quotee",
"tlr",
"editor",
"editors",
"mainauthor",
"compiler",
"compilers",
"director",
"directors",
"lyricist",
"lyrics-translator",
"composer",
"role",
"actor",
"artist",
"feat",
-- author control params
"default-authorlabel",
"authorlabel",
-- title
"title",
"trans-title",
"series",
"seriesvolume",
"notitle",
-- entry
"entry",
"entryurl",
"trans-entry",
-- chapter
"chapter",
"chapterurl",
"chapter_number",
"chapter_plain",
"chapter_series",
"chapter_seriesvolume",
"trans-chapter",
"chapter_tlr",
-- section
"section",
"sectionurl",
"section_number",
"section_plain",
"section_series",
"section_seriesvolume",
"trans-section",
-- other video-game params
"system",
"scene",
"level",
-- URL
"url",
"urls",
"archiveurl",
-- edition
"edition",
"edition_plain",
-- ID params
"bibcode",
"doi",
"isbn",
"issn",
"jstor",
"lccn",
"oclc",
"ol",
"pmid",
"pmcid",
"ssrn",
"urn",
"id",
-- misc date params; most date params handled below
"archivedate",
"accessdate",
"nodate",
-- numeric params handled below
-- other params
"type",
"genre",
"format",
"medium",
"others",
"quoted_in",
"location",
"publisher",
"original",
"by",
"deriv",
"note",
"note_plain",
"other",
"source",
"platform",
} do
add_with_2(param12, true)
end
-- Then the aliases of newversion params (which have FOO2 versions).
for _, param12_aliased in ipairs{
{"role", "roles"},
{"role", "speaker"},
{"tlr", "tlrs"},
{"tlr", "translator"},
{"tlr", "translators"},
{"chapter_tlr", "chapter_tlrs"},
{"chapter_tlr", "chapter_translator"},
{"chapter_tlr", "chapter_translators"},
{"doi", "DOI"},
{"isbn", "ISBN"},
{"issn", "ISSN"},
{"jstor", "JSTOR"},
{"lccn", "LCCN"},
{"oclc", "OCLC"},
{"ol", "OL"},
{"pmid", "PMID"},
{"pmcid", "PMCID"},
{"ssrn", "SSRN"},
{"urn", "URN"},
} do
local canon, alias = unpack(param12_aliased)
alias_with_2(alias, canon)
end
-- Then the date params.
for _, datelike in ipairs{{"", ""}, {"orig", ""}, {"", "_published"}} do
local pref, suf = unpack(datelike)
for _, arg in ipairs{"date", "year", "month", "start_date", "start_year", "start_month"} do
add_with_2(pref .. arg .. suf, true)
end
end
local numeric_param_suffixes = {"", "s", "_plain", "url", "_prefix"}
-- Then the numeric params.
for _, numeric in ipairs{"volume", "issue", "number", "line", "page", "column"} do
for _, suf in ipairs(numeric_param_suffixes) do
add_with_2(numeric .. suf, true)
end
end
-- And the aliases of numeric params.
for _, numeric_aliased in ipairs{{"volume", "vol"}} do
local canon, alias = unpack(numeric_aliased)
for _, suf in ipairs(numeric_param_suffixes) do
alias_with_2(alias .. suf, canon .. suf)
end
end
return process_params(cloned_args, params), alias_map, other_controls
end
local function get_origtext_params(args)
local origtext, origtextlang, origsc, orignormsc
if args.origtext then
-- Wiktionary language codes have at least two lowercase letters followed possibly by lowercase letters and/or
-- hyphens (there are more restrictions but this is close enough). Also check for nonstandard Latin etymology
-- language codes (e.g. VL. or LL.). (There used to be more nonstandard codes but they have all been
-- eliminated.)
origtextlang, origtext = args.origtext:match("^(%l%l[%l-]*):([^ ].*)$")
if not origtextlang then
-- Special hack for Latin variants, which can have nonstandard etym codes, e.g. VL., LL.
origtextlang, origtext = args.origtext:match("^(%uL%.):([^ ].*)$")
end
if not origtextlang then
error("origtext= should begin with a language code prefix")
end
origtextlang = get_lang(origtextlang, nil, "allow etym") or
error("origtext= should begin with a language code prefix")
origsc = args.origsc
orignormsc = args.orignormsc
else
for _, noparam in ipairs{"origtr", "origts", "origsc", "orignorm", "orignormsc", "origsubst", "origlb"} do
if args[noparam] then
error(("Cannot specify %s= without origtext="):format(noparam))
end
end
end
return origtext, origtextlang, origsc, orignormsc
end
local function get_quote(args, is_cite)
local text = args.text
local gloss = args.t
local tr = args.tr
local ts = args.ts
local norm = args.norm
local sc = args.sc
local normsc = args.normsc
-- Fetch original-text parameters.
local origtext, origtextlang, origsc, orignormsc = get_origtext_params(args)
-- If any quote-related args are present, display the actual quote; otherwise, display nothing.
if text or gloss or tr or ts or norm or args.origtext then
-- Pass "und" here rather than cause an error; there will be an error on mainspace, Citations, etc. pages
-- in any case in source() if the language is omitted.
local lang = get_first_lang(args[1] or args.lang)
local termlang = args.termlang and get_first_lang(args.termlang) or lang
local usex_data = {
lang = lang,
termlang = termlang,
usex = text,
sc = sc,
translation = gloss,
normalization = norm,
normsc = normsc,
transliteration = tr,
transcription = ts,
brackets = args.brackets,
subst = args.subst,
lit = args.lit,
footer = args.footer,
qq = parse_and_format_labels(args.lb, lang),
quote = "quote-meta",
orig = origtext,
origlang = origtextlang,
origsc = origsc,
orignorm = args.orignorm,
orignormsc = orignormsc,
origtr = args.origtr,
origts = args.origts,
origsubst = args.origsubst,
origqq = parse_and_format_labels(args.origlb, lang),
noreq = args.noreq,
nocat = is_cite or args.nocat,
}
if args.inline then
-- don't let usex format the footer, otherwise it gets inlined with the rest of the quoted text
usex_data.footer = nil
usex_data.inline = 1
text = format_usex(usex_data)
if text then
text = " “" .. text .. "”"
else
text = ""
end
if args.footer then
text = text .. "<dl><dd>" .. args.footer .. "</dd></dl>"
end
else
text = "<dl><dd>" .. format_usex(usex_data) .. "</dd></dl>"
end
elseif args.footer then
text = "<dl><dd>" .. args.footer .. "</dd></dl>"
end
return text
end
-- External interface, meant to be called from a template. Replaces {{quote-meta}} and meant to be the primary
-- interface for {{quote-*}} templates.
function export.quote_t(frame)
local args, alias_map, other_controls = get_args(frame.args, frame:getParent().args, "require_lang")
if args.ascite then
return export.cite_t(frame)
end
return show_quote(args, alias_map, other_controls)
end
function show_quote(args, alias_map, other_controls)
local parts = {}
insert(parts, '<div class="citation-whole"><span class="cited-source">')
insert(parts, export.source(args, alias_map, nil, other_controls))
insert(parts, "</span>")
insert(parts, get_quote(args))
insert(parts, "</div>")
local retval = concat(parts)
return deprecated and frame:expandTemplate({
title = "check deprecated lang param usage",
args = {retval, lang = args.lang},
}) or retval
end
-- External interface, meant to be called from a template. Replaces {{cite-meta}} and meant to be the primary
-- interface for {{cite-*}} templates.
function export.cite_t(frame)
local parent_args = {}
for k, v in pairs(frame:getParent().args) do
parent_args[k] = v
end
-- use "und" as lang if none provided
if parent_args[1] == nil then
parent_args[1] = "und"
end
local args, alias_map, other_controls = get_args(frame.args, parent_args)
if args.asquote then
return show_quote(args, alias_map, other_controls)
end
return show_cite(args, alias_map, other_controls)
end
function show_cite(args, alias_map, other_controls)
local parts = {}
-- don't nag for translations
if args.text and not args.t then
args.noreq = 1
end
local len_visible = args.text and ulen((args.text:gsub("<[^<>]+>", ""))) or 0
if len_visible == 0 then
if not args.t or args.t == "-" then
args.nocolon = true
end
elseif args.inline == nil then
args.inline = not (
args.block_text
or len_visible > 300
or (args.t and not args.t == "-" and len_visible < 80)
or string.match(args.text, "<br>")
)
end
insert(parts, '<span class="citation-whole"><span class="cited-source">')
insert(parts, export.source(args, alias_map, "format_as_cite", other_controls))
insert(parts, "</span>")
insert(parts, get_quote(args, "is_cite"))
insert(parts, "</span>")
local retval = concat(parts)
return deprecated
and frame:expandTemplate({
title = "check deprecated lang param usage",
args = {retval, lang = args.lang},
})
or retval
end
-- External interface, meant to be called from a template.
function export.call_quote_template(frame)
return export.call_template(frame)
end
-- External interface, for calling a template with overloaded parameters.
function export.overload(frame)
local iargs, overloaded_args = process_params(frame.args, {
[1] = {required = true}
}, true)
local parent_args = frame:getParent().args
for k, v in pairs(parent_args) do
overloaded_args[k] = v
end
return frame:expandTemplate{title = iargs[1], args = overloaded_args}
end
-- External interface, meant to be called from a template.
function export.call_template(frame)
local parameter_sublist = {type = "parameter", sublist = true}
local iargs, other_direct_args = process_params(frame.args, {
["template"] = true,
["textparam"] = parameter_sublist,
["pageparam"] = parameter_sublist,
["propagateparams"] = parameter_sublist,
["allowparams"] = {sublist = true}, -- Doesn't use type = "parameter", because any that end in :list get processed differently.
}, true)
local function fetch_param(source, params)
for _, param in ipairs(params) do
if source[param] then
return source[param]
end
end
return nil
end
local params = {
["text"] = true,
["passage"] = true,
["footer"] = true, -- always propagates, but gets special formatting
}
-- paramaters that can be used during template declaration or included in template calls
-- if the template is called with param=- it will set any declared value to nil
local always_propagate = {
["brackets"] = true,
["usenodot"] = true,
["nodot"] = true,
["ascite"] = true,
["asquote"] = true,
}
for k,v in pairs(always_propagate) do
params[k] = v
end
local textparam = iargs.textparam or {}
for _, param in ipairs(textparam) do
params[param] = true
end
local pageparam = iargs.pageparam or {}
local pageparam1 = pageparam[1]
if pageparam1 ~= nil then
params["page"], params["pages"] = true, true
for _, param in ipairs(pageparam) do
params[param] = true
end
end
local allowparams, allow_all, list = iargs.allowparams, false
if allowparams ~= nil then
for _, allow in ipairs(allowparams) do
local param = allow:match("^(.*):list$")
if param then
if list == nil then
list = {list = true}
end
params[scribunto_parameter_key(param)] = list
elseif allow == "*" then
track("no parameter checking")
allow_all = true
else
params[scribunto_parameter_key(allow)] = true
end
end
else
local parent = frame:getParent()
local template_name = parent:getTitle()
local template_title = new_title(template_name)
local content = template_title:getContent()
local allowed_params, seen = {}, {}
-- Detect all params used by the parent template. param:get_name() takes the
-- parent frame arg table as an argument so that preprocessing will take
-- them into account, since it will matter if the name contains another
-- parameter (e.g. the outer param in "{{{foo{{{bar}}}baz}}}" will change
-- depending on the value for bar=). `seen` memoizes results based on the
-- raw parameter text (which is stored as a string in the parameter object),
-- which avoids unnecessary param:get_name() calls, which are non-trivial.
for param in find_parameters(content) do
local raw = param.raw
if not seen[raw] then
allow = param:get_name(template_args)
params[scribunto_parameter_key(allow)] = true
seen[raw] = true
end
end
end
local propagateparams = iargs.propagateparams or {}
for _, param in ipairs(propagateparams) do
params[param] = true
end
local parent_args = frame:getParent().args
local args = process_params(parent_args, params, allow_all)
parent_args = shallow_copy(parent_args)
if textparam[1] ~= "-" then
other_direct_args.passage = args.text or args.passage or fetch_param(args, textparam)
end
if not (pageparam1 == nil or pageparam1 == "-") then
other_direct_args.page = fetch_param(args, pageparam) or args.page or other_direct_args.page or nil
other_direct_args.pages = args.pages or other_direct_args.pages or nil
if other_direct_args.page == "-" then
other_direct_args.page = nil
end
if other_direct_args.pages == "-" then
other_direct_args.pages = nil
end
end
if args.footer then
other_direct_args.footer = frame:expandTemplate{title = "small", args = {args.footer}}
end
for param, _ in pairs(always_propagate) do
if args[param] == "-" then
other_direct_args[param] = nil
else
other_direct_args[param] = args[param] or other_direct_args[param] or nil
end
end
-- authorlink=- can be used to prevent copying of author= to authorlink= but we don't want to propagate this to
-- the actual {{quote-*}} code.
if other_direct_args.authorlink == "-" then
other_direct_args.authorlink = nil
end
for _, param in ipairs(propagateparams) do
if args[param] then
other_direct_args[param] = args[param]
end
end
return frame:expandTemplate{title = iargs.template or "quote-book", args = other_direct_args}
end
local paramdoc_param_replacements = {
passage = {
param_with_synonym = "<<synonym>>, {{para|text}}, or {{para|passage}}",
param_no_synonym = "{{para|text}} or {{para|passage}}",
text = [=[
* <<params>> – the passage to be quoted.]=],
},
page = {
param_with_synonym = "<<synonym>> or {{para|page}}, or {{para|pages}}",
param_no_synonym = "{{para|page}} or {{para|pages}}",
text = [=[
* <<params>> – '''mandatory in some cases''': the page number(s) quoted from. When quoting a range of pages, note the following:
** Separate the first and last pages of the range with an [[en dash]], like this: {{para|pages|10–11}}.
** You must also use {{para|pageref}} to indicate the page to be linked to (usually the page on which the Wiktionary entry appears).
: This parameter must be specified to have the template link to the online version of the work.]=],
},
page_with_roman_preface = {
param_with_synonym = {"inherit", "page"},
param_no_synonym = {"inherit", "page"},
text = [=[
* <<params>> – '''mandatory in some cases''': the page number(s) quoted from. If quoting from the preface, specify the page number(s) in lowercase Roman numerals. When quoting a range of pages, note the following:
** Separate the first and last page number of the range with an [[en dash]], like this: {{para|pages|10–11}} or {{para|pages|iii–iv}}.
** You must also use {{para|pageref}} to indicate the page to be linked to (usually the page on which the Wiktionary entry appears).
: This parameter must be specified to have the template link to the online version of the work.]=],
},
chapter = {
param_with_synonym = "<<synonym>> or {{para|chapter}}",
param_no_synonym = "{{para|chapter}}",
text = [=[
* <<params>> – the name of the chapter quoted from.]=],
},
roman_chapter = {
param_with_synonym = {"inherit", "chapter"},
param_no_synonym = {"inherit", "chapter"},
text = [=[
* <<params>> – the chapter number quoted from in uppercase Roman numerals.]=],
},
arabic_chapter = {
param_with_synonym = {"inherit", "chapter"},
param_no_synonym = {"inherit", "chapter"},
text = [=[
* <<params>> – the chapter number quoted from in Arabic numerals.]=],
},
trailing_params = {
text = [=[
* {{para|footer}} – a comment on the passage quoted.
* {{para|brackets}} – use {{para|brackets|on}} to surround a quotation with [[bracket#Noun|brackets]]. This indicates that the quotation either contains a mere mention of a term (for example, “some people find the word '''''manoeuvre''''' hard to spell”) rather than an actual use of it (for example, “we need to '''manoeuvre''' carefully to avoid causing upset”), or does not provide an actual instance of a term but provides information about related terms.]=],
},
}
function export.paramdoc(frame)
local parargs = frame:getParent().args
local args = process_params(parargs, {
[1] = true,
})
local text = args[1]
local function do_param_with_optional_synonym(param, text_to_sub, paramtext_synonym, paramtext_no_synonym)
local function sub_param(synonym)
local subbed_paramtext
if synonym then
subbed_paramtext = paramtext_synonym:gsub("<<synonym>>", "{{para|" .. replacement_escape(synonym) .. "}}")
else
subbed_paramtext = paramtext_no_synonym
end
return frame:preprocess((text_to_sub:gsub("<<params>>", replacement_escape(subbed_paramtext))))
end
text = text:gsub("<<" .. pattern_escape(param) .. ">>", function()
return sub_param()
end)
text = text:gsub("<<" .. pattern_escape(param) .. ":(.-)>>", sub_param)
end
local function fetch_text(param_to_replace, key)
local spec = paramdoc_param_replacements[param_to_replace]
local val = spec[key]
if type(val) == "string" then
return val
end
if type(val) == "table" and val[1] == "inherit" then
return fetch_text(val[2], key)
end
error(
"Internal error: Unrecognized value for param '"
.. param_to_replace
.. "', key '"
.. key
.. "': "
.. mw.dumpObject(val)
)
end
for param_to_replace, spec in pairs(paramdoc_param_replacements) do
if not spec.param_no_synonym then
-- Text to substitute directly.
text = text:gsub("<<" .. pattern_escape(param_to_replace) .. ">>", function()
return frame:preprocess(fetch_text(param_to_replace, "text"))
end)
else
do_param_with_optional_synonym(
param_to_replace,
fetch_text(param_to_replace, "text"),
fetch_text(param_to_replace, "param_with_synonym"),
fetch_text(param_to_replace, "param_no_synonym")
)
end
end
-- Remove final newline so template code can add a newline after invocation
text = text:gsub("\n$", "")
return text
end
return export
pvj2bzazb12pq4jcw2763jsedii3ggz
Modul:language-like
828
13233
375849
373539
2026-09-25T09:16:06Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/92765629|92765629]])
375849
Scribunto
text/plain
local export = {}
local families_module = "Module:families"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
-- In a locally installed Docker container containing Wiktionary, Wikibase is typically not installed because it's
-- a real pain to get working properly. In such a case, `mw.wikibase` returns nil. We now have checks in the code
-- below so it does reasonable things in the absence of Wikibase.
local wikibase = mw.wikibase
local category_name_has_suffix -- defined as export.categoryNameHasSuffix below
local get_entity = wikibase and wikibase.getEntity or nil
local get_entity_id_for_title = wikibase and wikibase.getEntityIdForTitle or nil
local gsub = string.gsub
local ipairs = ipairs
local match = string.match
local select = select
local sitelink = wikibase and wikibase.sitelink or nil
local type = type
local umatch = mw.ustring.match
--[==[
Loaders for functions in other modules, which overwrite themselves with the target function when called. This ensures modules are only loaded when needed, retains the speed/convenience of locally-declared pre-loaded functions, and has no overhead after the first call, since the target functions are called directly in any subsequent calls.]==]
local function case_insensitive_pattern(...)
case_insensitive_pattern = require(string_utilities_module).case_insensitive_pattern
return case_insensitive_pattern(...)
end
local function table_flatten(...)
table_flatten = require(table_module).flatten
return table_flatten(...)
end
--[==[
Loaders for objects, which load data (or some other object) into some variable, which can then be accessed as "foo or get_foo()", where the function get_foo sets the object to "foo" and then returns it. This ensures they are only loaded when needed, and avoids the need to check for the existence of the object each time, since once "foo" has been set, "get_foo" will not be called again.]==]
local content_lang
local function get_content_lang()
content_lang, get_content_lang = mw.getContentLanguage(), nil
return content_lang
end
-- Implementation of getAliases() for languages, etymology languages,
-- families, scripts and writing systems.
function export.getAliases(self)
local aliases = self._aliases
if aliases == nil then
aliases = (self._data or self).aliases or {}
self._aliases = aliases
end
return aliases
end
-- Implementation of getVarieties() for languages, etymology languages,
-- families, scripts and writing systems. If `flatten` is passed in,
-- flatten down to a list of strings; otherwise, keep the structure.
function export.getVarieties(self, flatten)
local varieties = self._varieties
if varieties == nil then
varieties = (self._data or self).varieties or {}
self._varieties = varieties
end
if not flatten then
return varieties
end
local flattened_varieties = self._flattened_varieties
if flattened_varieties == nil then
flattened_varieties = table_flatten(varieties)
self._flattened_varieties = flattened_varieties
end
return flattened_varieties
end
-- Implementation of getOtherNames() for languages, etymology languages,
-- families, scripts and writing systems.
function export.getOtherNames(self)
local other_names = self._other_names
if other_names == nil then
other_names = (self._data or self).other_names or {}
self._other_names = other_names
end
return other_names
end
-- Implementation of getAllNames() for languages, etymology languages,
-- families, scripts and writing systems.
function export.getAllNames(self)
local all_names = self._allNames
if all_names == nil then
all_names = table_flatten{
self.getCanonicalNames and self:getCanonicalNames() or self:getCanonicalName(),
self:getAliases(),
self:getVarieties(),
self:getOtherNames(),
}
self._allNames = all_names
end
return all_names
end
function export.hasType(self, ...)
local n = select("#", ...)
if n == 0 then
error("Must specify at least one type.")
end
local types = self:getTypes()
if not types[...] then
return false
elseif n == 1 then
return true
end
local args = {...}
for i = 2, n do
if not types[args[i]] then
return false
end
end
return true
end
-- Implementation of template-callable getByCode() function for languages,
-- etymology languages, families and scripts. `item` is the language,
-- family or script in question; `args` is the arguments passed in by the
-- module invocation; `extra_processing`, if specified, is a function of
-- one argument (the requested property) and should return the value to
-- be returned to the caller, or nil if the property isn't recognized.
-- `extra_processing` is called after special-cased properties are handled
-- and before general-purpose processing code that works for all string
-- properties.
function export.templateGetByCode(args, extra_processing)
-- The item that the caller wanted to look up.
local item, itemname, list = args[1], args[2]
if itemname == "getAllNames" then
list = item:getAllNames()
elseif itemname == "getOtherNames" then
list = item:getOtherNames()
elseif itemname == "getAliases" then
list = item:getAliases()
elseif itemname == "getVarieties" then
list = item:getVarieties(true)
end
if list then
local index = args[3]; if index == "" then index = nil end
index = tonumber(index or error("Numeric index of the desired item in the list (parameter 3) has not been specified."))
return list[index] or ""
end
if itemname == "getFamily" and item.getFamily then
return item:getFamily():getCode()
end
if extra_processing then
local retval = extra_processing(itemname)
if retval then
return retval
end
end
if item[itemname] then
local ret = item[itemname](item)
if type(ret) == "string" then
return ret
end
error("The function \"" .. itemname .. "\" did not return a string value.")
end
error("Requested invalid item name \"" .. itemname .. "\".")
end
-- Implementation of getCommonsCategory() for languages, etymology languages,
-- families, scripts and writing systems.
function export.getWikidataItem(self)
local item = self._WikidataItem
if item == nil then
item = (self._data or self)[2]
-- If the value is nil, it's cached as false.
item = item ~= nil and (type(item) == "number" and "Q" .. item or item) or false
self._WikidataItem = item
end
return item or nil
end
do
local function get_wiki_article(self, project)
local article
-- If the project is mswiki, check the language data.
if project == "mswiki" then
article = (self._data or self).wikipedia_article
if article then
return article
end
end
-- Otherwise, check the Wikidata item for a sitelink.
local item = self:getWikidataItem()
article = item and sitelink and sitelink(item, project) or false
if article then
return article
end
-- If there's still no article, try the parent (if any).
local get_parent = self.getParent
if get_parent then
local parent = get_parent(self)
if parent then
return get_wiki_article(parent, project)
end
end
return false
end
-- Implementation of getWikipediaArticle() for languages, etymology languages,
-- families, scripts and writing systems.
function export.getWikipediaArticle(self, noCategoryFallback, project)
if project == nil then
project = "mswiki"
end
local article
if project == "mswiki" then
article = self._wikipedia_article
if article == nil then
article = get_wiki_article(self, project)
self._wikipedia_article = article
end
else
-- If the project isn't mswiki, default to no category fallback, but
-- this can be overridden by specifying the value `false`.
if noCategoryFallback == nil then
noCategoryFallback = true
end
local non_en_wikipedia_articles = self._non_en_wikipedia_articles
if non_en_wikipedia_articles == nil then
non_en_wikipedia_articles = {}
self._non_en_wikipedia_articles = non_en_wikipedia_articles
else
article = non_en_wikipedia_articles[project]
end
if article == nil then
article = get_wiki_article(self, project)
non_en_wikipedia_articles[project] = article
end
end
if article or noCategoryFallback then
return article or nil
end
return (gsub(self:getCategoryName(), "Bahasa kreol", "Kreol"))
end
end
do
local function get_commons_cat_claim(item)
if item then
local entity = get_entity and get_entity(item) or nil
if entity then
-- P373 is the "Commons category" property.
local claim = entity:getBestStatements("P373")[1]
return claim and ("Kategori:" .. claim.mainsnak.datavalue.value) or nil
end
end
end
local function get_commons_cat_sitelink(item)
if item then
local commons_sitelink = sitelink and sitelink(item, "commonswiki") or nil
-- Reject any sitelinks that aren't categories.
return commons_sitelink and match(commons_sitelink, "^Kategori:") and commons_sitelink or nil
end
end
local function get_commons_cat(self)
-- Checks are in decreasing order of likelihood for a useful match.
-- Get the Commons Category claim from the object's item.
local lang_item = self:getWikidataItem()
local category = get_commons_cat_claim(lang_item)
if category then
return category
end
-- Otherwise, try the object's category's item.
local langcat_item = get_entity_id_for_title and get_entity_id_for_title("Kategori:" .. self:getCategoryName()) or nil
category = get_commons_cat_claim(langcat_item)
if category then
return category
end
-- If there's no P373 claim, there might be a sitelink on the
-- object's category's item.
category = get_commons_cat_sitelink(langcat_item)
if category then
return category
end
-- Otherwise, try for a sitelink on the object's own item.
category = get_commons_cat_sitelink(lang_item)
if category then
return category
end
-- If there's still no category, try the parent (if any).
local get_parent = self.getParent
if get_parent then
local parent = get_parent(self)
if parent then
return get_commons_cat(parent)
end
end
return false
end
-- Implementation of getCommonsCategory() for languages, etymology
-- languages, families, scripts and writing systems.
function export.getCommonsCategory(self)
local category
category = self._commons_category
-- Nil values cached as false.
if category ~= nil then
return category or nil
end
category = get_commons_cat(self)
self._commons_category = category
return category or nil
end
end
function export.categoryNameHasSuffix(name, suffixes)
for _, suffix in ipairs(suffixes) do
if umatch(name, "%f[%w]" .. case_insensitive_pattern(suffix, "^.") .. "$") then
return false
end
end
return true
end
category_name_has_suffix = export.categoryNameHasSuffix
function export.categoryNameToCode(name, suffix, data, suffixes)
local truncated = match(name, "^" .. suffix .. "(.*)")
if truncated and category_name_has_suffix(truncated, suffixes) then
local code = data[truncated]
if code ~= nil then
return code, truncated
end
truncated = (content_lang or get_content_lang()):lcfirst(truncated)
code = data[truncated]
if code ~= nil then
return code, truncated
end
end
if not category_name_has_suffix(name, suffixes) then
local code = data[name]
if code ~= nil then
return code, name
end
name = (content_lang or get_content_lang()):lcfirst(name)
code = data[name]
if code ~= nil then
return code, name
end
end
return nil, nil
end
-- If a language has no specified pseudo-family but its or its family's code begins with one or these prefixes,
-- assign it to the specified pseudo-family. Note that we don't do this across-the-board because there are (currently)
-- some families whose code begins with `nai-` but are considered in the `cai` pseudo-family.
local pseudo_family_prefixes = {
cai = true,
nai = true,
paa = true,
sai = true,
sgn = true,
}
-- Implementation of getPseudoFamilies() for languages, etymology languages
-- and families.
function export.getPseudoFamilies(self)
local pseudo_families = self._pseudo_families
local function insert_pseudo_family(code)
local fam = require(families_module).getByCode(code)
if not fam then
mw.log(("WARNING: Unrecognized pseudo-family code '%s'"):format(code))
else
table.insert(pseudo_families, fam)
end
end
if pseudo_families == nil then
pseudo_families = {}
local pseudo_family_string = (self._data or self).pseudo_families
if pseudo_family_string then
local pseudo_family_codes = require(string_utilities_module).split(pseudo_family_string, "%s*,%s*")
for _, code in ipairs(pseudo_family_codes) do
insert_pseudo_family(code)
end
end
if not pseudo_families[1] then
-- Check the parent. If it's a full language, we will have to fetch the extra data to get the
-- list of pseudo-families.
local get_parent = self.getParent
if get_parent then
local parent = get_parent(self)
if parent then
local parent_code = parent:getCode()
if pseudo_family_prefixes[parent_code] then
insert_pseudo_family(parent_code)
else
pseudo_families = parent:getPseudoFamilies()
end
end
end
end
if not pseudo_families[1] then
-- Check the family to see if it's in any pseudo-families.
local get_family = self.getFamily
if get_family then
local family = get_family(self)
if family then
local family_code = family:getCode()
if pseudo_family_prefixes[family_code] then
insert_pseudo_family(family_code)
elseif family_code ~= self:getCode() then -- qfa-not has itself as a family.
pseudo_families = family:getPseudoFamilies()
end
end
end
end
if not pseudo_families[1] then
local prefix = self:getCode():match("^([a-z][a-z][a-z])%-")
if prefix and pseudo_family_prefixes[prefix] then
insert_pseudo_family(prefix)
end
end
self._pseudo_families = pseudo_families
end
return pseudo_families
end
return export
cwttp4kr8xzjm01ppnl2xcyokojqxn3
Modul:it-headword
828
13932
375795
281465
2026-09-24T13:22:13Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/92714442|92714442]])
375795
Scribunto
text/plain
-- This module contains code for Italian headword templates.
-- Templates covered are:
-- * {{it-noun}}, {{it-proper noun}};
-- * {{it-verb}};
-- * {{it-adj}}, {{it-adj-comp}}, {{it-adj-sup}};
-- * {{it-det}};
-- * {{it-art}};
-- * {{it-pron-adj}};
-- * {{it-pp}};
-- * {{it-presp}};
-- * {{it-card-noun}}, {{it-card-adj}}, {{it-card-inv}};
-- * {{it-adv}};
-- * {{it-pos}};
-- * {{it-suffix form}}.
-- See [[Module:it-verb]] for Italian conjugation templates.
local export = {}
local pos_functions = {}
local force_cat = false -- for testing; if true, categories appear in non-mainspace pages
local m_strutils = require("Module:string utilities")
local usub = m_strutils.sub
local require_when_needed = require("Module:utilities/require when needed")
local insert = table.insert
local remove = table.remove
local m_table = require("Module:table")
local com = require("Module:it-common")
local en_utilities_module = "Module:en-utilities"
local headword_module = "Module:headword"
local headword_utilities_module = "Module:headword utilities"
local inflection_utilities_module = "Module:inflection utilities"
local it_verb_module = "Module:it-verb"
local parse_interface_module = "Module:parse interface"
local romut_module = "Module:romance utilities"
local lang = require("Module:languages").getByCode("it")
local langname = lang:getCanonicalName()
local m_en_utilities = require_when_needed(en_utilities_module)
local m_headword_utilities = require_when_needed(headword_utilities_module)
local glossary_link = require_when_needed(headword_utilities_module, "glossary_link")
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local no_split_apostrophe_words = {
["c'è"] = true,
["c'era"] = true,
["c'erano"] = true,
}
-----------------------------------------------------------------------------------------
-- Utility functions --
-----------------------------------------------------------------------------------------
local function track(page)
require("Module:debug/track")("it-headword/" .. page)
return true
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;
-- `accel` is the accelerator form, or nil.
local function parse_and_insert_inflection(data, args, field, label, accel, frob)
m_headword_utilities.parse_and_insert_inflection {
headdata = data,
forms = args[field],
paramname = field,
splitchar = ",",
label = label,
accel = accel and {form = accel} or nil,
frob = frob,
}
end
local function replace_hash_with_lemma(term, lemma)
-- If there is a % sign in the lemma, we have to replace it with %% so it doesn't get interpreted as a capture
-- replace expression.
lemma = lemma:gsub("%%", "%%%%")
return (term:gsub("#", lemma))
end
local list_param = {list = true, disallow_holes = true}
local boolean_param = {type = "boolean"}
-----------------------------------------------------------------------------------------
-- Main entry point --
-----------------------------------------------------------------------------------------
function export.show(frame)
local poscat = frame.args[1]
or error("Part of speech has not been specified. Please pass parameter 1 to the module invocation.")
local parargs = frame:getParent().args
local params = {
["head"] = list_param,
["id"] = true,
["sort"] = true,
["apoc"] = boolean_param,
["splithyph"] = boolean_param,
["nolinkhead"] = boolean_param,
["nolink"] = {type = "boolean", alias_of = "nolinkhead"},
["json"] = boolean_param,
["pagename"] = true, -- for testing
}
if pos_functions[poscat] then
for key, val in pairs(pos_functions[poscat].params) do
params[key] = val
end
end
local args = require("Module:parameters").process(parargs, params)
local pagename = args.pagename or mw.loadData("Module:headword/data").pagename
local user_specified_heads = args.head
local heads = user_specified_heads
if args.nolinkhead then
if #heads == 0 then
heads = {pagename}
end
else
local romut = require(romut_module)
local auto_linked_head = romut.add_links_to_multiword_term(pagename, args.splithyph,
no_split_apostrophe_words)
if #heads == 0 then
heads = {auto_linked_head}
else
for i, head in ipairs(heads) do
if head:find("^~") then
head = romut.apply_link_modifiers(auto_linked_head, usub(head, 2))
heads[i] = head
end
if head == auto_linked_head then
track("redundant-head")
end
end
end
end
local data = {
lang = lang,
pos_category = pos_functions[poscat] and pos_functions[poscat].pos_category or poscat,
categories = {},
heads = heads,
user_specified_heads = user_specified_heads,
no_redundant_head_cat = #user_specified_heads == 0,
genders = {},
inflections = {},
pagename = pagename,
id = args.id,
sort_key = args.sort,
force_cat_output = force_cat,
checkredlinks = pos_functions[poscat] and pos_functions[poscat].redlink_pos or true,
}
if pagename:find("^%-") and poscat ~= "bentuk akhiran" then
data.is_suffix = true
data.pos_category = "akhiran"
data.checkredlinks = true
local singular_poscat = m_en_utilities.singularize(poscat)
insert(data.categories, "Akhiran membentuk " .. singular_poscat .. " bahasa " .. langname)
insert(data.inflections, {label = "Akhiran membentuk " .. singular_poscat})
end
if pos_functions[poscat] then
pos_functions[poscat].func(args, data)
end
if args.apoc then
-- Apocopated form of a term; do this after calling pos_functions[], because the function might modify
-- data.pos_category.
local pos = data.pos_category
if not pos:find("Bentuk ") then
-- Apocopated forms are non-lemma forms.
local singular_poscat = m_en_utilities.singularize(pos)
data.pos_category = "Bentuk " .. singular_poscat
end
-- If this is a suffix, insert label 'apocopated' after 'FOO-forming suffix', otherwise insert at the beginning.
insert(data.inflections, data.is_suffix and 2 or 1, {label = glossary_link("apocopated")})
end
if args.json then
return require("Module:JSON").toJSON(data)
end
return require(headword_module).full_headword(data)
end
local deriv_params = {
{"dim", glossary_link("diminutif")},
{"dim_dim", "double " .. glossary_link("diminutif")},
{"aug_dim", glossary_link("agam") .. "-" .. glossary_link("diminutif")},
{"aug", glossary_link("agam")},
{"dim_aug", glossary_link("diminutif") .. "-" .. glossary_link("agam")},
{"aug_aug", "double " .. glossary_link("agam")},
{"pej", glossary_link("pejoratif")},
{"dim_pej", glossary_link("diminutif") .. "-" .. glossary_link("pejoratif")},
{"aug_pej", glossary_link("agam") .. "-" .. glossary_link("pejoratif")},
{"pej_pej", "double " .. glossary_link("pejoratif")},
{"end", glossary_link("endearing")},
{"dim_end", glossary_link("diminutif") .. "-" .. glossary_link("endearing")},
{"aug_end", glossary_link("agam") .. "-" .. glossary_link("endearing")},
{"derog", glossary_link("hinaan")},
{"dim_derog", glossary_link("diminutif") .. "-" .. glossary_link("hinaan")},
{"aug_derog", glossary_link("agam") .. "-" .. glossary_link("hinaan")},
{"end_derog", glossary_link("endearing") .. "-" .. glossary_link("hinaan")},
}
local function insert_deriv_params(params)
for _, deriv_param in ipairs(deriv_params) do
local param = unpack(deriv_param)
params[param] = list_param
end
end
local param_mods = {
t = {
-- We need to store the <t:...> inline modifier into the "gloss" key of the parsed part, because that is what
-- [[Module:links]] expects.
item_dest = "gloss",
},
gloss = {},
-- no 'tr' or 'ts', doesn't make sense for Italian
g = {
-- We need to store the <g:...> inline modifier into the "genders" key of the parsed part, because that is what
-- [[Module:links]] expects.
item_dest = "genders",
sublist = true,
},
id = {},
alt = {},
q = {type = "qualifier"},
qq = {type = "qualifier"},
lit = {},
pos = {},
-- no 'sc', doesn't make sense for Italian
}
local function parse_term_with_modifiers(paramname, val)
local function generate_obj(term)
local decomp = com.decompose(term)
local lemma = com.remove_non_final_accents(decomp)
if lemma ~= decomp then
term = com.compose("[[" .. lemma .. "|" .. decomp .. "]]")
end
return {term = term}
end
local retval = require(parse_interface_module).parse_inline_modifiers(val, {
paramname = paramname,
param_mods = param_mods,
generate_obj = generate_obj,
splitchar = "[/;,]",
preserve_splitchar = true,
})
for _, obj in ipairs(retval) do
if obj.delimiter == ";" then
obj.separator = "; "
elseif obj.delimiter == "/" then
obj.separator = "/"
-- default to nil for comma
end
end
return retval
end
local function insert_deriv_inflections(data, args)
for _, deriv_param in ipairs(deriv_params) do
local param, desc = unpack(deriv_param)
if #args[param] > 0 then
local inflection = {label = desc}
for _, term in ipairs(args[param]) do
local parsed_terms = parse_term_with_modifiers(param, term)
for _, parsed_term in ipairs(parsed_terms) do
insert(inflection, parsed_term)
end
end
insert(data.inflections, inflection)
end
end
end
-----------------------------------------------------------------------------------------
-- Nouns --
-----------------------------------------------------------------------------------------
local allowed_genders = m_table.listToSet(
{"m", "f", "mf", "mfbysense", "mfequiv", "gneut", "n", "m-p", "f-p", "mf-p", "mfbysense-p", "mfequiv-p", "gneut-p", "n-p", "?", "?-p"}
)
local function validate_genders(genders)
for _, g in ipairs(genders) do
if type(g) == "table" then
g = g.spec
end
if not allowed_genders[g] then
error("Unrecognized gender: " .. g)
end
end
end
local function do_noun(args, data, is_proper)
local is_plurale_tantum = false
local has_singular = false
local category_plpos = data.checkredlinks
if category_plpos == true then
category_plpos = data.pos_category
end
local category_pos = m_en_utilities.singularize(category_plpos)
validate_genders(args[1])
data.genders = args[1]
local saw_m = false
local saw_f = false
local gender_for_default_plural
-- Check for specific genders and pluralia tantum.
for _, g in ipairs(args[1]) do
if type(g) == "table" then
g = g.spec
end
if g:find("-p$") then
is_plurale_tantum = true
else
has_singular = true
if g == "m" or g == "mf" or g == "mfbysense" then
saw_m = true
end
if g == "f" or g == "mf" or g == "mfbysense" then
saw_f = true
end
end
end
if saw_m and saw_f then
gender_for_default_plural = "mf"
elseif saw_f then
gender_for_default_plural = "f"
else
gender_for_default_plural = "m"
end
local lemma = data.pagename
local function inscat(cat)
insert(data.categories, langname .. " " .. cat)
end
local function insert_noun_inflection(terms, label, accel, no_inv)
for _, term in ipairs(terms) do
if not no_inv and term.term == lemma then
term.term = nil
term.label = glossary_link("invariable")
end
end
m_headword_utilities.insert_inflection {
headdata = data,
terms = terms,
label = label,
accel = accel and {form = accel} or nil,
}
end
-- Plural
local plurals = {}
-- Fetch explicit masculine and feminine plurals here because we may change them below when processing plurals.
local mpls = m_headword_utilities.parse_term_list_with_modifiers {
paramname = "mpl",
forms = args.mpl,
splitchar = ",",
}
local fpls = m_headword_utilities.parse_term_list_with_modifiers {
paramname = "fpl",
forms = args.fpl,
splitchar = ",",
}
if is_plurale_tantum and not has_singular then
if args[2][1] then
error("Can't specify plurals of plurale tantum " .. category_pos)
end
insert(data.inflections, {label = glossary_link("hanya jamak")})
elseif args.apoc then
-- apocopated noun
if args[2][1] then
error("Can't specify plurals of apocopated " .. category_pos)
end
else
-- Fetch plurals and associated decorations (qualifiers and labels) and genders.
plurals = m_headword_utilities.parse_term_list_with_modifiers {
paramname = {2, "pl"},
forms = args[2],
splitchar = ",",
include_mods = {"g"},
}
-- Check for special plural signals
local mode = nil
local pl1 = plurals[1]
if pl1 and #pl1.term == 1 then
mode = pl1.term
if mode == "?" or mode == "!" or mode == "-" or mode == "~" then
pl1.term = nil
if next(pl1) then
error(("Can't specify inline modifiers with plural code '%s'"):format(mode))
end
remove(plurals, 1) -- Remove the mode parameter
elseif mode ~= "+" and mode ~= "#" then
error(("Unexpected plural code '%s'"):format(mode))
end
end
if is_plurale_tantum then
-- both singular and plural
insert(data.inflections, {label = "kadangkala " .. glossary_link("hanya jamak") .. ", dengan kelainan"})
end
if mode == "?" then
-- Plural is unknown
insert(data.categories, category_plpos .. " bahasa " .. langname .. " dengan bentuk jamak yang tidak dikenal pasti")
elseif mode == "!" then
-- Plural is not attested
insert(data.inflections, {label = "plural not attested"})
insert(data.categories, category_plpos .. " bahasa " .. langname .. " dengan bentuk jamak yang tidak ditentusahkan")
if plurals[1] then
error("Can't specify any plurals along with unattested plural code '!'")
end
elseif mode == "-" then
-- Uncountable noun; may occasionally have a plural
insert(data.categories, category_plpos .. " tak berbilang bahasa " .. langname)
-- If plural forms were given explicitly, then show "usually"
if plurals[1] then
insert(data.inflections, {label = "biasanya " .. glossary_link("tak berbilang")})
insert(data.categories, category_plpos .. " berbilang bahasa " .. langname)
else
insert(data.inflections, {label = glossary_link("uncountable")})
end
else
-- Countable or mixed countable/uncountable
-- If no plurals, use the default plural unless mpl= or fpl= explicitly given.
if not plurals[1] and not mpls[1] and not fpls[1] and not is_proper then
plurals[1] = {term = "+"}
end
if mode == "~" then
-- Mixed countable/uncountable noun, always has a plural
insert(data.inflections, {label = glossary_link("berbilang") .. " dan " .. glossary_link("tak berbilang")})
insert(data.categories, category_plpos .. " tak berbilang bahasa " .. langname)
insert(data.categories, category_plpos .. " berbilang bahasa " .. langname)
elseif plurals[1] then
-- Countable nouns
insert(data.categories, category_plpos .. " berbilang bahasa " .. langname)
else
-- Uncountable nouns
insert(data.categories, category_plpos .. " tak berbilang bahasa " .. langname)
end
end
-- Process plurals, handling requests for default plurals.
local has_default_or_hash = false
for _, pl in ipairs(plurals) do
if pl.term:find("^%+") or pl.term:find("#") or pl.term == "cap*" or pl.term == "cap*+" then
has_default_or_hash = true
break
end
end
if has_default_or_hash then
local newpls = {}
local function insert_pl(pl, defpl)
pl.term = defpl
insert(newpls, pl)
end
local function make_gendered_plural(pl, special)
if gender_for_default_plural == "mf" then
local default_mpl = com.make_plural(lemma, "m", special)
local default_fpl = com.make_plural(lemma, "f", special)
if default_mpl then
if default_mpl == default_fpl then
insert_pl(pl, default_mpl)
else
if args.mpl[1] or args.fpl[1] then
error("Can't specify gendered plural spec '" .. (special or "+") ..
"' along with gender=" .. gender_for_default_plural ..
" and also specify mpl= or fpl=")
end
mpls = {m_table.shallowCopy(pl)}
mpls[1].term = default_mpl
fpls = {pl}
fpls[1].term = default_fpl
end
end
else
local defpl = com.make_plural(lemma, gender_for_default_plural, special)
if defpl then
insert_pl(pl, defpl)
end
end
end
for _, pl in ipairs(plurals) do
if pl.term == "cap*" or pl.term == "cap*+" then
make_gendered_plural(pl, pl.term)
elseif pl.term == "+" then
make_gendered_plural(pl)
elseif pl.term:find("^%+") then
local special = require(romut_module).get_special_indicator(pl.term)
make_gendered_plural(pl, special)
else
insert_pl(pl, replace_hash_with_lemma(pl.term, lemma))
end
end
plurals = newpls
end
if plurals[2] then
inscat(category_plpos .. " with multiple plurals")
end
-- If the first or only plural is the same as the singular, replace it with 'invariable', or 'usually
-- invariable' if there is more than one plural.
pl1 = plurals[1]
if pl1 and pl1.term == lemma then
if plurals[2] then
insert(data.inflections, {label = "usually " .. glossary_link("invariable"),
q = pl1.q, qq = pl1.qq, l = pl1.l, ll = pl1.ll, refs = pl1.refs
})
else
insert(data.inflections, {label = glossary_link("invariable"),
q = pl1.q, qq = pl1.qq, l = pl1.l, ll = pl1.ll, refs = pl1.refs
})
end
remove(plurals, 1)
inscat("indeclinable " .. category_plpos)
end
if plurals[1] then
-- Check for gender-changing plurals.
for _, pl in ipairs(plurals) do
if pl.genders then
for _, g in ipairs(pl.genders) do
if type(g) ~= "table" then
g = {spec = g}
end
if g.spec == "m" and not saw_m or g.spec == "f" and not saw_f then
inscat(category_plpos .. " that change gender in the plural")
end
end
end
end
end
end
-- Gather masculines/feminines. For each one, generate the corresponding plural. `field` is the name of the field
-- containing the masculine or feminine forms (normally "m" or "f"); `inflect` is a function of one or two arguments
-- to generate the default masculine or feminine from the lemma (the arguments are the lemma and optionally a
-- "special" flag to indicate how to handle multiword lemmas, and the function is normally make_feminine or
-- make_masculine from [[Module:it-common]]); and `default_plurals` is a list into which the corresponding default
-- plurals of the gathered or generated masculine or feminine forms are stored.
local function handle_mf(field, inflect, default_plurals)
local special
local mfs = m_headword_utilities.parse_term_list_with_modifiers {
paramname = field,
forms = args[field],
splitchar = ",",
frob = function(term)
if term == "+" then
-- Generate default masculine/feminine.
term = inflect(lemma)
else
term = replace_hash_with_lemma(term, lemma)
end
special = require(romut_module).get_special_indicator(term)
if special then
term = inflect(lemma, special)
end
return term
end
}
for _, mf in ipairs(mfs) do
local plobj = m_table.shallowCopy(mf)
plobj.term = com.make_plural(mf.term, field, special)
if plobj.term then
-- Add an accelerator for each masculine/feminine plural whose lemma is the corresponding singular, so that
-- the accelerated entry that is generated has a definition that looks like
-- # {{plural of|it|MFSING}}
plobj.accel = {form = "p", lemma = mf.term}
insert(default_plurals, plobj)
end
end
return mfs
end
local feminine_plurals = {}
local feminines = handle_mf("f", com.make_feminine, feminine_plurals)
local masculine_plurals = {}
local masculines = handle_mf("m", com.make_masculine, masculine_plurals)
local function handle_mf_plural(mfplfield, mfpls, gender, default_plurals, singulars)
if is_plurale_tantum then
return mfpls, true
end
local new_mfpls = {}
local saw_plus
local noinv
for i, mfpl in ipairs(mfpls) do
local accel
if #mfpls == #singulars then
-- If same number of overriding masculine/feminine plurals as singulars, assume each plural goes with
-- the corresponding singular and use each corresponding singular as the lemma in the accelerator. The
-- generated entry will have
-- # {{plural of|it|SINGULAR}}
-- as the definition.
accel = {form = "p", lemma = singulars[i].term}
else
accel = nil
end
if mfpl.term == "+" then
-- We should never see + twice. If we do, it will lead to problems since we overwrite the values of
-- default_plurals the first time around.
if saw_plus then
error(("Saw + twice when handling %s="):format(mfplfield))
end
saw_plus = true
if not default_plurals[1] then
local defpl = com.make_plural(lemma, gender)
if not defpl then
error("Unable to generate default plural of '" .. lemma .. "'")
end
default_plurals[1] = {term = defpl}
end
for _, defpl in ipairs(default_plurals) do
-- defpl is already a table and has an accel field
m_headword_utilities.combine_termobj_decorations(defpl, mfpl)
insert(new_mfpls, defpl)
end
-- don't use "invariable" because the plural is not with respect to the lemma but with respect to the
-- masc/fem singular
noinv = true
elseif mfpl.term == "cap*" or mfpl.term == "cap*+" or mfpl.term:find("^%+") then
if mfpl.term:find("^%+") then
mfpl.term = require(romut_module).get_special_indicator(mfpl.term)
end
if singulars[1] then
for _, mf in ipairs(singulars) do
local mfplobj = m_table.shallowCopy(mfpl)
mfplobj.term = com.make_plural(mf.term, gender, mfpl.term)
if mfplobj.term then
mfplobj.accel = accel
m_headword_utilities.combine_termobj_decorations(mfplobj, mf)
insert(new_mfpls, mfplobj)
end
-- don't use "invariable" because the plural is not with respect to the lemma but with respect
-- to the masc/fem singular
noinv = true
-- FIXME: Should we throw an error if no plural could be generated?
end
else
-- FIXME: This clause didn't exist in the corresponding code in [[Module:pt-headword]]. Is it
-- correct?
mfpl.term = com.make_plural(lemma, gender, mfpl.term)
if mfpl.term then
insert(new_mfpls, mfpl)
end
end
else
mfpl.accel = accel
mfpl.term = replace_hash_with_lemma(mfpl.term, lemma)
insert(new_mfpls, mfpl)
-- don't use "invariable" if masc/fem singular present because the plural is not with respect to
-- the lemma but with respect to the masc/fem singular
noinv = noinv or #singulars > 0
end
end
return new_mfpls, noinv
end
local mpl_noinv, fpl_noinv
-- Not fpls[1] because if the user didn't specify any explicit mpl= or fpl= but the lemma gender is mf or mfbysense
-- and has separate masculine and feminine plural forms (e.g. any term in -ista), we don't want to reprocess those
-- auto-generated forms.
if args.fpl[1] then
-- Override any existing feminine plurals.
feminine_plurals, fpl_noinv = handle_mf_plural("fpl", fpls, "f", feminine_plurals, feminines)
else
feminine_plurals, fpl_noinv = fpls, false
end
if args.mpl[1] then
-- Override any existing masculine plurals.
masculine_plurals, mpl_noinv = handle_mf_plural("mpl", mpls, "m", masculine_plurals, masculines)
else
masculine_plurals, mpl_noinv = mpls, false
end
local function redundant_plural(pl)
for _, p in ipairs(plurals) do
if p.term == pl.term then
return true
end
end
return false
end
for _, mpl in ipairs(masculine_plurals) do
if redundant_plural(mpl) then
track("noun-redundant-mpl")
end
end
for _, fpl in ipairs(feminine_plurals) do
if redundant_plural(fpl) then
track("noun-redundant-fpl")
end
end
if plurals[1] then
-- Set 'noinv' because we already took care of invariable plurals above.
insert_noun_inflection(plurals, "plural", "p", "noinv")
end
insert_noun_inflection(masculines, "masculine")
insert_noun_inflection(masculine_plurals, "masculine plural", nil, mpl_noinv)
insert_noun_inflection(feminines, "feminine", "f")
insert_noun_inflection(feminine_plurals, "feminine plural", nil, fpl_noinv)
local function parse_and_insert_noun_inflection(field, label, accel)
parse_and_insert_inflection(data, args, field, label, accel)
end
parse_and_insert_noun_inflection("adj", glossary_link("relational", "relational adjective"))
parse_and_insert_noun_inflection("adv", glossary_link("adverb"))
parse_and_insert_noun_inflection("dem", glossary_link("demonym"))
parse_and_insert_noun_inflection("fdem", "female " .. glossary_link("demonym"))
insert_deriv_inflections(data, args)
-- Maybe add category 'Italian nouns with irregular gender' (or similar)
local irreg_gender_lemma = lemma:gsub(" .*", "") -- only look at first word
if (irreg_gender_lemma:find("o$") and (gender_for_default_plural == "f" or gender_for_default_plural == "mf"
or gender_for_default_plural == "mfbysense")) or
(irreg_gender_lemma:find("a$") and (gender_for_default_plural == "m" or gender_for_default_plural == "mf"
or gender_for_default_plural == "mfbysense")) then
inscat(category_plpos .. " dengan genus tak tentu")
end
end
local function get_noun_params(nountype)
local params = {
[1] = {list = "g", disallow_holes = true, required = nountype ~= "proper", default = "?", type = "genders",
flatten = true},
[2] = {list = "pl", disallow_holes = true},
["m"] = list_param,
["f"] = list_param,
["mpl"] = list_param,
["fpl"] = list_param,
["adj"] = list_param, --adjective(s)
["adv"] = list_param, --adverb(s)
["dem"] = list_param, --demonym(s)
["fdem"] = list_param, --female demonym(s)
}
insert_deriv_params(params)
return params
end
pos_functions["Kata nama"] = {
params = get_noun_params("base"),
func = do_noun,
}
pos_functions["Kata nama khas"] = {
params = get_noun_params("proper"),
func = function(args, data)
do_noun(args, data, "is proper noun")
end,
}
pos_functions["Kata nama kardinal"] = {
params = get_noun_params("base"),
func = function(args, data)
do_noun(args, data)
insert(data.categories, 1, "Nombor kardinal " .. langname)
end,
pos_category = "Kata bilangan",
}
-----------------------------------------------------------------------------------------
-- Adjectives --
-----------------------------------------------------------------------------------------
local function do_adjective(args, data, is_superlative)
local feminines = {}
local masculine_plurals = {}
local feminine_plurals = {}
-- Use "participle" not "past participle" for categories such as 'invariable participles'
local category_plpos = data.checkredlinks
if category_plpos == true then
category_plpos = data.pos_category
end
local category_pos = m_en_utilities.singularize(category_plpos)
if args.sp then
local romut = require(romut_module)
if not romut.allowed_special_indicators[args.sp] then
local indicators = {}
for indic, _ in pairs(romut.allowed_special_indicators) do
insert(indicators, "'" .. indic .. "'")
end
table.sort(indicators)
error("Special inflection indicator beginning can only be " ..
mw.text.listToText(indicators) .. ": " .. args.sp)
end
end
local lemma = data.pagename
local function fetch_inflections(field)
local retval = m_headword_utilities.parse_term_list_with_modifiers {
paramname = field,
forms = args[field],
splitchar = ",",
}
if not retval[1] then
return {{term = "+"}}
end
return retval
end
local function insert_inflection(terms, label, accel)
m_headword_utilities.insert_inflection {
headdata = data,
terms = terms,
label = label,
accel = accel and {form = accel} or nil,
}
end
if args.inv then
-- invariable adjective
insert(data.inflections, {label = glossary_link("invariable")})
insert(data.categories, langname .. " indeclinable " .. category_plpos)
end
if args.noforms then
-- [[bello]] and any others too complicated to describe in headword
insert(data.inflections, {label = "see below for inflection"})
end
if args.inv or args.apoc or args.noforms then
if args.sp or args.f[1] or args.pl[1] or args.mpl[1] or args.fpl[1] then
error("Can't specify inflections with an invariable or apocopated adjective or with noforms=")
end
elseif args.fonly then
-- feminine-only
if args.f[1] then
error("Can't specify explicit feminines with feminine-only " .. category_pos)
end
if args.pl[1] then
error("Can't specify explicit plurals with feminine-only " .. category_pos .. ", use fpl=")
end
if args.mpl[1] then
error("Can't specify explicit masculine plurals with feminine-only " .. category_pos)
end
local argsfpl = fetch_inflections("fpl")
for _, fpl in ipairs(argsfpl) do
if fpl.term == "+" then
local defpl = com.make_plural(lemma, "f", args.sp)
if not defpl then
error("Unable to generate default plural of '" .. lemma .. "'")
end
fpl.term = defpl
else
fpl.term = replace_hash_with_lemma(fpl.term, lemma)
end
insert(feminine_plurals, fpl)
end
insert(data.inflections, {label = "feminine-only"})
insert_inflection(feminine_plurals, "feminine plural", "f|p")
else
-- Gather feminines.
for _, f in ipairs(fetch_inflections("f")) do
if f.term == "+" then
-- Generate default feminine.
f.term = com.make_feminine(lemma, args.sp)
else
f.term = replace_hash_with_lemma(f.term, lemma)
end
insert(feminines, f)
end
local fem_like_lemma = #feminines == 1 and feminines[1].term == lemma and
not m_headword_utilities.termobj_has_decorations(feminines[1])
if fem_like_lemma then
insert(data.categories, langname .. " epicene " .. category_plpos)
end
local mpl_field = "mpl"
local fpl_field = "fpl"
if args.pl[1] then
if args.mpl[1] or args.fpl[1] then
error("Can't specify both pl= and mpl=/fpl=")
end
mpl_field = "pl"
fpl_field = "pl"
end
local argsmpl = fetch_inflections(mpl_field)
local argsfpl = fetch_inflections(fpl_field)
for _, mpl in ipairs(argsmpl) do
if mpl.term == "+" then
-- Generate default masculine plural.
local defpl = com.make_plural(lemma, "m", args.sp)
if not defpl then
error("Unable to generate default plural of '" .. lemma .. "'")
end
mpl.term = defpl
else
mpl.term = replace_hash_with_lemma(mpl.term, lemma)
end
insert(masculine_plurals, mpl)
end
for _, fpl in ipairs(argsfpl) do
if fpl.term == "+" then
for _, f in ipairs(feminines) do
-- Generate default feminine plural; f is a table.
local fplobj = m_table.shallowCopy(fpl)
local defpl = com.make_plural(f.term, "f", args.sp)
if not defpl then
error("Unable to generate default plural of '" .. f.term .. "'")
end
fplobj.term = defpl
m_headword_utilities.combine_termobj_decorations(fplobj, f)
insert(feminine_plurals, fplobj)
end
else
fpl.term = replace_hash_with_lemma(fpl.term, lemma)
insert(feminine_plurals, fpl)
end
end
local fem_pl_like_masc_pl = masculine_plurals[1] and feminine_plurals[1] and
m_table.deepEquals(masculine_plurals, feminine_plurals)
local masc_pl_like_lemma = #masculine_plurals == 1 and masculine_plurals[1].term == lemma and
not m_headword_utilities.termobj_has_decorations(masculine_plurals[1])
if fem_like_lemma and fem_pl_like_masc_pl and masc_pl_like_lemma then
-- actually invariable
insert(data.inflections, {label = glossary_link("invariable")})
insert(data.categories, langname .. " indeclinable " .. category_plpos)
else
-- Make sure there are feminines given and not same as lemma.
if not fem_like_lemma then
insert_inflection(feminines, "feminine", "f|s")
elseif args.gneut then
data.genders = {"gneut"}
else
data.genders = {"mfbysense"}
end
if fem_pl_like_masc_pl then
if args.gneut then
insert_inflection(masculine_plurals, "plural", "p")
else
-- This is how the Spanish module works.
-- insert_inflection(masculine_plurals, "masculine and feminine plural", "p")
insert_inflection(masculine_plurals, "plural", "p")
end
else
insert_inflection(masculine_plurals, "masculine plural", "m|p")
insert_inflection(feminine_plurals, "feminine plural", "f|p")
end
end
end
local function parse_and_insert_adj_inflection(field, label, accel, frob)
parse_and_insert_inflection(data, args, field, label, accel, frob)
end
parse_and_insert_adj_inflection("n", "neuter")
parse_and_insert_adj_inflection("comp", glossary_link("comparative"))
parse_and_insert_adj_inflection("sup", glossary_link("superlative"))
parse_and_insert_adj_inflection("adv", glossary_link("adverb"))
insert_deriv_inflections(data, args)
if args.irreg and is_superlative then
insert(data.categories, langname .. " irregular superlative " .. category_plpos)
end
end
local function get_adjective_params(adjtype)
local params = {
["inv"] = boolean_param, --invariable
["noforms"] = boolean_param, --too complicated to list forms except in a table
["sp"] = true, -- special indicator: "first", "first-last", etc.
["f"] = list_param, --feminine form(s)
["pl"] = list_param, --plural override(s)
["fpl"] = list_param, --feminine plural override(s)
["mpl"] = list_param, --masculine plural override(s)
["adv"] = list_param, --adverb(s)
}
if adjtype == "base" or adjtype == "part" or adjtype == "det" then
params["comp"] = list_param --comparative(s)
params["sup"] = list_param --superlative(s)
params["fonly"] = boolean_param -- feminine only
end
if adjtype == "sup" then
params["irreg"] = boolean_param
end
insert_deriv_params(params)
return params
end
pos_functions["Kata sifat"] = {
params = get_adjective_params("base"),
func = do_adjective,
}
pos_functions["Kata sifat bandingan"] = {
params = get_adjective_params("comp"),
func = do_adjective,
pos_category = "Kata sifat",
}
pos_functions["Kata sifat superlatif"] = {
params = get_adjective_params("sup"),
func = function(args, data)
do_adjective(args, data, "is superlative")
end,
pos_category = "Kata sifat",
}
pos_functions["Kata sifat kardinal"] = {
params = get_adjective_params("card"),
func = function(args, data)
do_adjective(args, data)
insert(data.categories, 1, "Nombor kardinal bahasa " .. langname)
end,
pos_category = "numerals",
}
pos_functions["past participles"] = {
params = get_adjective_params("part"),
func = do_adjective,
redlink_pos = "participles",
}
pos_functions["present participles"] = {
params = get_adjective_params("part"),
func = do_adjective,
redlink_pos = "participles",
}
pos_functions["determiners"] = {
params = get_adjective_params("det"),
func = do_adjective,
}
pos_functions["articles"] = {
params = get_adjective_params("det"),
func = do_adjective,
}
pos_functions["adjective-like pronouns"] = {
params = get_adjective_params("pron"),
func = do_adjective,
pos_category = "pronouns",
}
pos_functions["cardinal invariable"] = {
params = {},
func = function(args, data)
insert(data.categories, langname .. " cardinal numbers")
insert(data.categories, langname .. " indeclinable numerals")
insert(data.inflections, {label = glossary_link("invariable")})
end,
pos_category = "numerals",
}
-----------------------------------------------------------------------------------------
-- Adverbs --
-----------------------------------------------------------------------------------------
local function do_adverb(args, data)
local function parse_and_insert_adv_inflection(field, label, accel, frob)
parse_and_insert_inflection(data, args, field, label, accel, frob)
end
parse_and_insert_adv_inflection("comp", glossary_link("comparative"))
parse_and_insert_adv_inflection("sup", glossary_link("superlative"))
parse_and_insert_adv_inflection("adj", glossary_link("adjective"))
end
local function get_adverb_params(advtype)
local params = {
["adj"] = list_param, --adjective(s)
}
if advtype == "base" then
params["comp"] = list_param --comparative(s)
params["sup"] = list_param --superlative(s)
end
return params
end
pos_functions["Adverba"] = {
params = get_adverb_params("base"),
func = do_adverb,
}
pos_functions["comparative adverbs"] = {
params = get_adverb_params("comp"),
func = do_adverb,
pos_category = "Adverba",
}
pos_functions["superlative adverbs"] = {
params = get_adverb_params("sup"),
func = do_adverb,
pos_category = "Adverba",
}
-----------------------------------------------------------------------------------------
-- Verbs --
-----------------------------------------------------------------------------------------
pos_functions["Kata kerja"] = {
params = {
[1] = {},
["noautolinktext"] = boolean_param,
["noautolinkverb"] = boolean_param,
},
func = function(args, data)
if args[1] then
local alternant_multiword_spec = require(it_verb_module).do_generate_forms(args, "from headword", data.heads[1])
local function do_verb_form(slot, label, rowslot, rowlabel)
local forms = alternant_multiword_spec.forms[slot]
local retval
if alternant_multiword_spec.rowprops.all_defective[rowslot] then
if not alternant_multiword_spec.rowprops.defective[rowslot] then
-- No forms, but none expected; don't display anything
return
end
retval = {label = "no " .. rowlabel}
elseif not forms then
retval = {label = "no " .. label}
elseif alternant_multiword_spec.rowprops.all_unknown[rowslot] then
retval = {label = "unknown " .. rowlabel}
elseif forms[1].form == "?" then
retval = {label = "unknown " .. label}
else
-- Disable accelerators for now because we don't want the added accents going into the headwords.
-- FIXME: We now have support in [[Module:accel]] to specify the target explicitly; we can use this
-- so we can add the accelerators back with a param to avoid the accents.
local accel_form = nil -- all_verb_slots[slot]
retval = {label = label, accel = accel_form and {form = accel_form} or nil}
local prev_footnotes = nil
-- If the footnotes for this form are the same as the footnotes for the preceding form or
-- contain the preceding footnotes, replace the footnotes that are the same with "ditto".
-- This avoids repetition on pages like [[succedere]] where the form ''succedétti'' has a long
-- footnote which gets repeated in the traditional form ''succedètti'' (which also has the
-- footnote "[traditional]").
for _, form in ipairs(forms) do
local quals, refs = require(inflection_utilities_module).
convert_footnotes_to_qualifiers_and_references(form.footnotes)
local quals_with_ditto = quals
if quals and prev_footnotes then
local quals_contains_previous = true
for _, qual in ipairs(prev_footnotes) do
if not m_table.contains(quals, qual) then
quals_contains_previous = false
break
end
end
if quals_contains_previous then
local inserted_ditto = false
quals_with_ditto = {}
for _, qual in ipairs(quals) do
if m_table.contains(prev_footnotes, qual) then
if not inserted_ditto then
insert(quals_with_ditto, "ditto")
inserted_ditto = true
end
else
insert(quals_with_ditto, qual)
end
end
end
end
prev_footnotes = quals
insert(retval, {term = form.form, q = quals_with_ditto, refs = refs})
end
end
insert(data.inflections, retval)
end
if alternant_multiword_spec.props.is_pronominal then
insert(data.inflections, {label = glossary_link("pronominal")})
end
if alternant_multiword_spec.props.impers then
insert(data.inflections, {label = glossary_link("impersonal")})
end
if alternant_multiword_spec.props.thirdonly then
insert(data.inflections, {label = "third-person only"})
end
local thirdonly = alternant_multiword_spec.props.impers or alternant_multiword_spec.props.thirdonly
local sing_label = thirdonly and "third-person singular" or "first-person singular"
for _, rowspec in ipairs {
{"pres", "present", true},
{"phis", "past historic", true},
{"pp", "past participle", true},
{"imperf", "imperfect"},
{"fut", "future"},
{"sub", "subjunctive"},
{"impsub", "imperfect subjunctive"},
} do
local rowslot, desc, always_show = unpack(rowspec)
local slot = rowslot .. (thirdonly and "3s" or "1s")
local must_show = alternant_multiword_spec.is_irreg[slot]
if always_show then
must_show = true
elseif rowslot == "imperf" and alternant_multiword_spec.props.has_explicit_stem_spec then
-- If there is an explicit stem spec, make sure it gets displayed; the imperfect is a good way of
-- showing this.
must_show = true
elseif not alternant_multiword_spec.forms[slot] then
-- If the principal part is unexpectedly missing, make sure we show this.
must_show = true
elseif alternant_multiword_spec.forms[slot][1].form == "?" then
-- If the principal part is unknown, make sure we show this.
must_show = true
end
if must_show then
if rowslot == "pp" then
do_verb_form(rowslot, desc, rowslot, desc)
else
do_verb_form(slot, sing_label .. " " .. desc, rowslot, desc)
end
end
end
-- Also do the imperative, but not for third-only verbs, which are always missing the imperative.
if not thirdonly and (alternant_multiword_spec.is_irreg.imp2s
or not alternant_multiword_spec.forms.imp2s) then
do_verb_form("imp2s", "second-person singular imperative", "imp", "imperative")
end
-- If there is a past participle but no auxiliary (e.g. [[malfare]]), explicitly add "no auxiliary". In
-- cases where there's no past participle and no auxiliary (e.g. [[irrompere]]), we don't do this as we
-- already get "no past participle" displayed. Don't display an auxiliary in any case if the lemma
-- consists entirely of reflexive verbs (for which the auxiliary is always [[essere]]).
if alternant_multiword_spec.props.is_non_reflexive and (
alternant_multiword_spec.forms.aux or alternant_multiword_spec.forms.pp
) then
do_verb_form("aux", "auxiliary", "aux", "auxiliary")
end
-- Add categories.
for _, cat in ipairs(alternant_multiword_spec.categories) do
insert(data.categories, cat)
end
-- If the user didn't explicitly specify head=, or specified exactly one head (not 2+) and we were able to
-- incorporate any links in that head into the 1= specification, use the infinitive generated by
-- [[Module:it-verb]] it in place of the user-specified or auto-generated head so that we get accents marked
-- on the verb(s). Don't do this if the user gave multiple heads or gave a head with a multiword-linked
-- verbal expression such as '[[dare esca]] [[al]] [[fuoco]]'.
if #data.user_specified_heads == 0 or (
#data.user_specified_heads == 1 and alternant_multiword_spec.incorporated_headword_head_into_lemma
) then
data.heads = {}
for _, lemma_obj in ipairs(alternant_multiword_spec.forms.inf) do
local quals, refs = require(inflection_utilities_module).
convert_footnotes_to_qualifiers_and_references(lemma_obj.footnotes)
insert(data.heads, {term = lemma_obj.form, q = quals, refs = refs})
end
end
end
end
}
-----------------------------------------------------------------------------------------
-- Suffix forms --
-----------------------------------------------------------------------------------------
pos_functions["Bentuk akhiran"] = {
params = {
[1] = {required = true, list = true, disallow_holes = true},
["g"] = {list = true, disallow_holes = true, type = "genders", flatten = true},
},
func = function(args, data)
validate_genders(args.g)
data.genders = args.g
local suffix_type = {}
for _, typ in ipairs(args[1]) do
insert(suffix_type, typ .. "-forming suffix")
end
insert(data.inflections, {label = "non-lemma form of " .. m_table.serialCommaJoin(suffix_type, {conj = "or"})})
end,
}
-----------------------------------------------------------------------------------------
-- Arbitrary parts of speech --
-----------------------------------------------------------------------------------------
pos_functions["arbitrary part of speech"] = {
params = {
[1] = {required = true},
["g"] = {list = true, disallow_holes = true, type = "genders", flatten = true},
},
func = function(args, data)
if data.is_suffix then
error("Can't use [[Template:it-pos]] with suffixes")
end
validate_genders(args.g)
data.genders = args.g
local plpos = m_en_utilities.pluralize(args[1])
data.pos_category = plpos
end,
}
return export
n67t9wmm4vw3gj7ikaaxvdk3hmo3yob
ramo
0
19944
375828
286462
2026-09-25T04:14:05Z
Hakimi97
2668
/* Kata nama */ Betulkan templat
375828
wikitext
text/x-wiki
==Bahasa Itali==
{{wikipedia|lang=it}}
===Kata nama===
{{it-noun|m|rami|pl2=ramora<l:Old Italian>}}
# {{lb|it|botani}} [[ranting]] {{gloss|bahagian tumbuhan}}
# {{lb|it|anatomi}} Cabang pada suatu badan.
===Etimologi===
Daripada {{der|it|la|rāmus}}.
==Bahasa Sunda==
{{Wikipedia|lang=su}}
===Kata nama===
{{head|su|kata nama}}
# {{lb|su|anatomi}} [[jari]]
===Sebutan===
* {{penyempangan|su|ra|mo}}
o6l4cep5aw8mvo9fsefvqjhvnoepder
Templat:ar-kk
10
22366
375801
126772
2026-09-24T13:57:25Z
Hakimi97
2668
Mengemas kini templat
375801
wikitext
text/x-wiki
{{#invoke:ar-headword|show|verbs}}<!--
--><noinclude>{{documentation}}</noinclude>
rtquc1bhaywxmpy48r6f9wweta8n7nt
375802
375801
2026-09-24T14:03:10Z
Hakimi97
2668
Betulkan templat
375802
wikitext
text/x-wiki
{{#invoke:ar-headword|show|Kata kerja}}<!--
--><noinclude>{{documentation}}</noinclude>
0hr9ekuwiq0szq42cn422tlgy2tdjx3
375803
375802
2026-09-24T14:03:31Z
Hakimi97
2668
Membatalkan semakan [[Special:Diff/375802|375802]] oleh [[Special:Contributions/Hakimi97|Hakimi97]] ([[User talk:Hakimi97|bincang]])
375803
wikitext
text/x-wiki
{{#invoke:ar-headword|show|verbs}}<!--
--><noinclude>{{documentation}}</noinclude>
rtquc1bhaywxmpy48r6f9wweta8n7nt
Modul:ar-verb
828
22367
375798
281308
2026-09-24T13:39:11Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/92729661|92729661]])
375798
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 ar_nominals = require("Module:ar-nominals")
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 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",
"2d",
"3md",
"3fd",
"1p",
"2mp",
"2fp",
"3mp",
"3fp"
}
local function make_person_number_slot_accel_list(list)
local slot_accel_list = {}
return slot_accel_list
end
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", "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 '" ..
table.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
-- 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
-- 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 table.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 = table.concat(formvals),
translit = has_manual_translit and table.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
local function reorder_shadda(word)
-- shadda+short-vowel (including tanwīn vowels, i.e. -an -in -un) gets
-- replaced with short-vowel+shadda during NFC normalisation, which
-- MediaWiki does for all Unicode strings; however, it makes various
-- processes inconvenient, so undo it.
word = rsub(word, "(" .. DIACRITIC_ANY_BUT_SH .. ")" .. SH, SH .. "%1")
return word
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 13 endings of the sound/hollow/geminate past tense.
local past_endings = {
-- singular
SK .. TU, SK .. TA, SK .. "تِ", A, A .. "تْ",
--dual
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 .. 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 .. "تُمَا", {}, {},
-- 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 .. 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
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
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[6], endings[10], endings[11]}
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, 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,
-- 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
-- tadullā, yadullā, tadullā
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
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[7], past_endings[8],
past_endings[12]},
{"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: '" .. table.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(table.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, table.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(table.concat(vowel_spec.pasts, "/"), table.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 = table.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 = table.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 = table.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(table.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 .. "-" .. table.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" | verbal noun<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="12" class="outer" | active voice<br /><<الْفِعْل الْمَعْلُوم>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="12" class="separator" |
! colspan="2" | 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 /><<الْغَائِب>>
! 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 (perfect) indicative<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_1s}
| {past_2ms}
| {past_3ms}
| 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 (imperfect) indicative<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_1s}
| {ind_2ms}
| {ind_3ms}
| 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_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_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" | {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="12" class="outer" | passive voice<br /><<الْفِعْل الْمَجْهُول>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="10" class="separator" |
! colspan="2" | 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 /><<الْغَائِب>>
! 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 (perfect) indicative<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_pass_1s}
| {past_pass_2ms}
| {past_pass_3ms}
| 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 (imperfect) indicative<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_pass_1s}
| {ind_pass_2ms}
| {ind_pass_3ms}
| 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_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_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 = table.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") .. " " ..
table.concat(linked_vns, ", "))
end
local annotation = table.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 == "Template"
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 = table.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/" .. table.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 table.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 = "* '''[[Appendix:Arabic verbs#Form " .. form .. "|Form " .. 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 .. table.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 .. table.concat(linktext, ", ")
table.insert(textarr, text)
end
end
table.insert(formtextarr, table.concat(textarr, "\n"))
end
end
end
return table.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 = table.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 " .. table.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
jpnmr6gjw5dyppyrrr1cqcb36k0dyw6
375799
375798
2026-09-24T13:44:32Z
Hakimi97
2668
Membetulkan kod
375799
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 ar_nominals = require("Module:ar-nominals")
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 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",
"2d",
"3md",
"3fd",
"1p",
"2mp",
"2fp",
"3mp",
"3fp"
}
local function make_person_number_slot_accel_list(list)
local slot_accel_list = {}
return slot_accel_list
end
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", "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 '" ..
table.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
-- 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
-- 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 table.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 = table.concat(formvals),
translit = has_manual_translit and table.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
local function reorder_shadda(word)
-- shadda+short-vowel (including tanwīn vowels, i.e. -an -in -un) gets
-- replaced with short-vowel+shadda during NFC normalisation, which
-- MediaWiki does for all Unicode strings; however, it makes various
-- processes inconvenient, so undo it.
word = rsub(word, "(" .. DIACRITIC_ANY_BUT_SH .. ")" .. SH, SH .. "%1")
return word
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 13 endings of the sound/hollow/geminate past tense.
local past_endings = {
-- singular
SK .. TU, SK .. TA, SK .. "تِ", A, A .. "تْ",
--dual
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 .. 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 .. "تُمَا", {}, {},
-- 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 .. 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
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
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[6], endings[10], endings[11]}
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, 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,
-- 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
-- tadullā, yadullā, tadullā
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
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[7], past_endings[8],
past_endings[12]},
{"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: '" .. table.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(table.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, table.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(table.concat(vowel_spec.pasts, "/"), table.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 = table.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 = table.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 = table.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(table.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 .. "-" .. table.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" | verbal noun<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="12" class="outer" | active voice<br /><<الْفِعْل الْمَعْلُوم>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="12" class="separator" |
! colspan="2" | 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 /><<الْغَائِب>>
! 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 (perfect) indicative<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_1s}
| {past_2ms}
| {past_3ms}
| 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 (imperfect) indicative<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_1s}
| {ind_2ms}
| {ind_3ms}
| 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_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_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" | {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="12" class="outer" | passive voice<br /><<الْفِعْل الْمَجْهُول>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="10" class="separator" |
! colspan="2" | 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 /><<الْغَائِب>>
! 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 (perfect) indicative<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_pass_1s}
| {past_pass_2ms}
| {past_pass_3ms}
| 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 (imperfect) indicative<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_pass_1s}
| {ind_pass_2ms}
| {ind_pass_3ms}
| 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_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_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 = table.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") .. " " ..
table.concat(linked_vns, ", "))
end
local annotation = table.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 "Kata kerja"
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 = table.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/" .. table.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 table.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#Form " .. form .. "|Form " .. 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 .. table.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 .. table.concat(linktext, ", ")
table.insert(textarr, text)
end
end
table.insert(formtextarr, table.concat(textarr, "\n"))
end
end
end
return table.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 = table.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 " .. table.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
967d4sc4aechq97f1oyavk3pxvudre7
375805
375799
2026-09-24T14:06:31Z
Hakimi97
2668
Betulkan (1)
375805
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 ar_nominals = require("Module:ar-nominals")
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 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",
"2d",
"3md",
"3fd",
"1p",
"2mp",
"2fp",
"3mp",
"3fp"
}
local function make_person_number_slot_accel_list(list)
local slot_accel_list = {}
return slot_accel_list
end
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", "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 '" ..
table.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
-- 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
-- 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 table.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 = table.concat(formvals),
translit = has_manual_translit and table.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
local function reorder_shadda(word)
-- shadda+short-vowel (including tanwīn vowels, i.e. -an -in -un) gets
-- replaced with short-vowel+shadda during NFC normalisation, which
-- MediaWiki does for all Unicode strings; however, it makes various
-- processes inconvenient, so undo it.
word = rsub(word, "(" .. DIACRITIC_ANY_BUT_SH .. ")" .. SH, SH .. "%1")
return word
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 13 endings of the sound/hollow/geminate past tense.
local past_endings = {
-- singular
SK .. TU, SK .. TA, SK .. "تِ", A, A .. "تْ",
--dual
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 .. 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 .. "تُمَا", {}, {},
-- 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 .. 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
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
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[6], endings[10], endings[11]}
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, 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,
-- 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
-- tadullā, yadullā, tadullā
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
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[7], past_endings[8],
past_endings[12]},
{"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: '" .. table.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(table.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, table.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(table.concat(vowel_spec.pasts, "/"), table.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 = table.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 = table.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 = table.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(table.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 .. "-" .. table.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" | verbal noun<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="12" class="outer" | active voice<br /><<الْفِعْل الْمَعْلُوم>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="12" class="separator" |
! colspan="2" | 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 /><<الْغَائِب>>
! 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 (perfect) indicative<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_1s}
| {past_2ms}
| {past_3ms}
| 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 (imperfect) indicative<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_1s}
| {ind_2ms}
| {ind_3ms}
| 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_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_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" | {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="12" class="outer" | passive voice<br /><<الْفِعْل الْمَجْهُول>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="10" class="separator" |
! colspan="2" | 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 /><<الْغَائِب>>
! 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 (perfect) indicative<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_pass_1s}
| {past_pass_2ms}
| {past_pass_3ms}
| 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 (imperfect) indicative<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_pass_1s}
| {ind_pass_2ms}
| {ind_pass_3ms}
| 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_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_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 = table.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") .. " " ..
table.concat(linked_vns, ", "))
end
local annotation = table.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 = table.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/" .. table.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 table.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#Form " .. form .. "|Form " .. 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 .. table.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 .. table.concat(linktext, ", ")
table.insert(textarr, text)
end
end
table.insert(formtextarr, table.concat(textarr, "\n"))
end
end
end
return table.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 = table.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 " .. table.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
kqiv2n4q3azbgjnwx69nno9409930sd
375808
375805
2026-09-24T14:40:54Z
Hakimi97
2668
375808
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 ar_nominals = require("Module:ar-nominals")
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 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",
"2d",
"3md",
"3fd",
"1p",
"2mp",
"2fp",
"3mp",
"3fp"
}
local function make_person_number_slot_accel_list(list)
local slot_accel_list = {}
return slot_accel_list
end
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", "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 '" ..
table.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
-- 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
-- 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 table.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 = table.concat(formvals),
translit = has_manual_translit and table.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
local function reorder_shadda(word)
-- shadda+short-vowel (including tanwīn vowels, i.e. -an -in -un) gets
-- replaced with short-vowel+shadda during NFC normalisation, which
-- MediaWiki does for all Unicode strings; however, it makes various
-- processes inconvenient, so undo it.
word = rsub(word, "(" .. DIACRITIC_ANY_BUT_SH .. ")" .. SH, SH .. "%1")
return word
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 13 endings of the sound/hollow/geminate past tense.
local past_endings = {
-- singular
SK .. TU, SK .. TA, SK .. "تِ", A, A .. "تْ",
--dual
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 .. 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 .. "تُمَا", {}, {},
-- 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 .. 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
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
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[6], endings[10], endings[11]}
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, 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,
-- 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
-- tadullā, yadullā, tadullā
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
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[7], past_endings[8],
past_endings[12]},
{"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: '" .. table.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(table.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, table.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(table.concat(vowel_spec.pasts, "/"), table.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 = table.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 = table.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 = table.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(table.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 .. "-" .. table.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" | verbal noun<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="12" class="outer" | active voice<br /><<الْفِعْل الْمَعْلُوم>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="12" class="separator" |
! colspan="2" | 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 /><<الْغَائِب>>
! 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 (perfect) indicative<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_1s}
| {past_2ms}
| {past_3ms}
| 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 (imperfect) indicative<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_1s}
| {ind_2ms}
| {ind_3ms}
| 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_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_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" | {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="12" class="outer" | passive voice<br /><<الْفِعْل الْمَجْهُول>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="10" class="separator" |
! colspan="2" | 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 /><<الْغَائِب>>
! 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 (perfect) indicative<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_pass_1s}
| {past_pass_2ms}
| {past_pass_3ms}
| 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 (imperfect) indicative<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_pass_1s}
| {ind_pass_2ms}
| {ind_pass_3ms}
| 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_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_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 = table.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") .. " " ..
table.concat(linked_vns, ", "))
end
local annotation = table.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 "Kata kerja"
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 = table.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/" .. table.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 table.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#Form " .. form .. "|Form " .. 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 .. table.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 .. table.concat(linktext, ", ")
table.insert(textarr, text)
end
end
table.insert(formtextarr, table.concat(textarr, "\n"))
end
end
end
return table.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 = table.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 " .. table.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
967d4sc4aechq97f1oyavk3pxvudre7
375809
375808
2026-09-24T14:46:19Z
Hakimi97
2668
375809
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 ar_nominals = require("Module:ar-nominals")
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 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",
"2d",
"3md",
"3fd",
"1p",
"2mp",
"2fp",
"3mp",
"3fp"
}
local function make_person_number_slot_accel_list(list)
local slot_accel_list = {}
return slot_accel_list
end
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", "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 '" ..
table.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
-- 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
-- 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 table.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 = table.concat(formvals),
translit = has_manual_translit and table.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
local function reorder_shadda(word)
-- shadda+short-vowel (including tanwīn vowels, i.e. -an -in -un) gets
-- replaced with short-vowel+shadda during NFC normalisation, which
-- MediaWiki does for all Unicode strings; however, it makes various
-- processes inconvenient, so undo it.
word = rsub(word, "(" .. DIACRITIC_ANY_BUT_SH .. ")" .. SH, SH .. "%1")
return word
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 13 endings of the sound/hollow/geminate past tense.
local past_endings = {
-- singular
SK .. TU, SK .. TA, SK .. "تِ", A, A .. "تْ",
--dual
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 .. 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 .. "تُمَا", {}, {},
-- 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 .. 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
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
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[6], endings[10], endings[11]}
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, 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,
-- 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
-- tadullā, yadullā, tadullā
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
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[7], past_endings[8],
past_endings[12]},
{"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: '" .. table.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(table.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, table.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(table.concat(vowel_spec.pasts, "/"), table.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 = table.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 = table.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 = table.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(table.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 .. "-" .. table.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" | verbal noun<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="12" class="outer" | active voice<br /><<الْفِعْل الْمَعْلُوم>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="12" class="separator" |
! colspan="2" | 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 /><<الْغَائِب>>
! 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 (perfect) indicative<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_1s}
| {past_2ms}
| {past_3ms}
| 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 (imperfect) indicative<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_1s}
| {ind_2ms}
| {ind_3ms}
| 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_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_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" | {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="12" class="outer" | passive voice<br /><<الْفِعْل الْمَجْهُول>>
|-
! colspan="2" |
! colspan="3" | singular<br /><<الْمُفْرَد>>
! rowspan="10" class="separator" |
! colspan="2" | 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 /><<الْغَائِب>>
! 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 (perfect) indicative<br /><<الْمَاضِي>>
! class="secondary" | m
| rowspan="2" | {past_pass_1s}
| {past_pass_2ms}
| {past_pass_3ms}
| 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 (imperfect) indicative<br /><<الْمُضَارِع الْمَرْفُوع>>
! class="secondary" | m
| rowspan="2" | {ind_pass_1s}
| {ind_pass_2ms}
| {ind_pass_3ms}
| 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_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_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 = table.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") .. " " ..
table.concat(linked_vns, ", "))
end
local annotation = table.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 = table.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/" .. table.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 table.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#Form " .. form .. "|Form " .. 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 .. table.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 .. table.concat(linktext, ", ")
table.insert(textarr, text)
end
end
table.insert(formtextarr, table.concat(textarr, "\n"))
end
end
end
return table.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 = table.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 " .. table.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
kqiv2n4q3azbgjnwx69nno9409930sd
sumambayang
0
30825
375818
306330
2026-09-25T02:45:17Z
Hakimi97
2668
/* Sebutan */ Membetulkan ng bukannya AFA, gantikan dengan ŋ
375818
wikitext
text/x-wiki
==Bahasa Kadazandusun==
===Kata kerja===
{{inti|dtp|kata kerja}}
# [[berdoa, sembahyang]].
#: {{cp|dtp|'''Sumambayang''' i Kina monikid tadau.|Kina '''berdoa''' setiap hari.}}
===Sebutan===
* {{IPA|dtp|/su.mam.ba.yaŋ/}}
* {{penyempangan|dtp|su|mam|ba|yang}}
kkql059jifnnndoebuy4otxifkos9w2
Modul:pa-Arab-translit
828
30917
375812
139442
2026-09-24T14:50:31Z
Hakimi97
2668
lencongkan
375812
Scribunto
text/plain
require [[Modul:pa-Aran-translit]]
cb01zr4sbmh9p1b0yjcc1ef3mga11sl
375813
375812
2026-09-24T14:51:33Z
Hakimi97
2668
Betulkan lencongan
375813
Scribunto
text/plain
return require [[Modul:pa-Aran-translit]]
90pmwk646m2av3g9iv0jg8h85acgxz5
Modul:list of scripts
828
34810
375832
227359
2026-09-25T04:40:54Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/83059198|83059198]])
375832
Scribunto
text/plain
local export = {}
local filters = {}
local m_scripts = require("Module:scripts")
local m_scripts_data = require("Module:scripts/data")
local concat = table.concat
local insert = table.insert
local remove = table.remove
local split = require("Module:string utilities").split
local m_table = require("Module:table")
local language_code_to_name = require("Module:languages/code to canonical name")
-- fa-Arab -> Arab-fa
local function rearrange_lang_and_script(script_code)
local lang, script = script_code:match("^([%w-]+)%-(%w+)$")
if lang then script_code = script .. '-' .. lang end
return script_code
end
-- Determines the order in which the scripts are shown in the resulting table.
local function compare_codes(apple, orange)
-- "None" before all other scripts
if apple == "None" then
return true
elseif orange == "None" then
return false
end
-- Sort language-code-prefixed script codes immediately after unmodified ones:
-- fa-Arab after Arab.
return rearrange_lang_and_script(apple) < rearrange_lang_and_script(orange)
end
local function map_to_limit(func, t, limit)
local new_t = {}
for i = 1, limit do
new_t[i] = func(t[i], i)
end
return new_t
end
local function get_canonical_name(code)
return language_code_to_name[code] or "???"
end
local function sort_by_canonical_name(code1, code2)
return get_canonical_name(code1) < get_canonical_name(code2)
end
local function sort_and_return(t, comp)
table.sort(t, comp)
return t
end
local function print_langs(langs, total)
local limit = 20
local suffix = ""
if total > limit then
suffix = ", ..."
else
limit = total
end
return concat(
map_to_limit(
get_canonical_name,
sort_and_return(
langs,
sort_by_canonical_name),
limit),
", ") .. suffix
end
function export.show(frame)
local args = frame.args
local filter = filters[args[1]]
local m_languages = require("Module:languages/data/all")
local script_to_langs = setmetatable(
{},
{
__index = function(self, key) -- Initialize fields in table with 0.
local val = {}
self[key] = val
return val
end
})
local m_scripts_size = m_table.size(m_scripts_data)
for code, data in pairs(m_languages) do
local checkData = type(data[4]) == "table" and data[4] or type(data[4]) == "string" and split(data[4], ",") or {"None"}
if m_table.size(checkData) ~= m_scripts_size then
for _, sc in ipairs(checkData) do
insert(script_to_langs[sc], code)
end
end
end
local rows = {}
for code, data in m_table.sortedPairs(m_scripts_data, compare_codes) do
if (not filter) or filter(code, data, args) then
local sc = m_scripts.makeObject(code, data)
local names = sc:getAllNames()
local canonicalName = remove(names, 1)
local catname = sc:getCategoryName()
local namecol = ""
if code == "None" then
namecol = canonicalName
else
namecol = "[[:Kategori:" .. catname .. "|" .. canonicalName .. "]]"
end
local lang_count = #script_to_langs[code]
insert(rows, string.format([=[
id="%s"
| <code>%s</code>
| %s
| %s
| %s
| %s
]=],
code, code,
namecol,
#names > 0 and concat(names, ", ") or " ",
data.characters and "Yes" or "",
lang_count > 0 and
(' title="%s" | [[:Kategori:Bahasa-bahasa Tulisan %s|%i]]'):format(
print_langs(script_to_langs[code], lang_count),
catname,
lang_count)
or ("%i"):format(lang_count)))
end
end
return [[
{| class="wikitable sortable"
! Kod
! Nama berkanun
! Nama lain
! Autokesan
! Bahasa-bahasa
|-]] .. concat(rows, "\n|-") .. [[
|}]]
end
return export
15iku8g5dzh9ri2s9sx5jb5a0hcl2ti
Modul:family tree
828
34913
375836
373776
2026-09-25T07:49:16Z
SNN95
2113
kemaskini
375836
Scribunto
text/plain
--[=[
Authors: [[User:AryamanA]], [[User:Erutuon]], [[User:Fenakhay]], [[User:JohnC5]], [[User:kc_kennylau]], [[User:Suzukaze-c]], [[User:Theknightwho]]
]=]
local export = {}
local M = require("Module:module loader").init{
require = {
languages = "Module:languages",
table = "Module:table",
yesno = "Module:yesno",
parameters = "Module:parameters",
TemplateStyles = "Module:TemplateStyles",
},
loadData = {
language_names = "Module:languages/code to canonical name",
etymology_names = "Module:etymology languages/code to canonical name",
family_names = "Module:families/code to canonical name",
},
}
local STYLESHEET = "Module:family tree/style.css"
local function new_cache()
local cache = { language = {}, family = {} }
return function(namespace, code, fetch, ...)
local slot = cache[namespace]
local obj = slot[code]
if obj == nil then
obj = fetch(...) or false
slot[code] = obj
end
return obj or nil
end
end
local function kind_of(obj)
if not obj:hasType("language") then
return "family"
elseif obj:hasType("etymology-only") then
return "variety"
end
return "language"
end
local function sortkey_of(obj)
return (obj:getCanonicalName():gsub(" Purba$", ""))
end
local placement_rules = {
{
name = "etymology-only language under its parent",
place = function(obj, get)
if not (obj:hasType("language") and obj:hasType("etymology-only")) then
return nil
end
local parent_code = obj:getParentCode()
if not parent_code then
return nil
end
local parent = get("language", parent_code, obj.getParent, obj)
if parent
and M.table.deepEquals(parent:getAncestorCodes(), obj:getAncestorCodes())
and parent:getFamilyCode() == obj:getFamilyCode() then
return { parent:getCode() }
end
end,
},
{
name = "language under its declared ancestors",
place = function(obj)
if not obj:hasType("language") then
return nil
end
local ancestors = obj:getAncestorCodes()
if ancestors[1] then
return M.table.shallowCopy(ancestors)
end
end,
},
{
name = "family under its ancestral proto-language",
place = function(obj)
if obj:hasType("language") then
return nil
end
local proto = obj:getProtoLanguageCode()
if proto and obj:hasAncestor(proto) then
return { proto }
end
end,
},
{
name = "node under its family's proto-language",
place = function(obj, get)
local family_code = obj:getFamilyCode()
if not family_code then
return nil
end
local family = get("family", family_code, obj.getFamily, obj)
local proto_code = family and family:getProtoLanguageCode()
local proto = proto_code and get("language", proto_code, family.getProtoLanguage, family)
if proto and proto:inFamily(family) and proto:getCode() ~= obj:getCode() then
return { proto:getCode() }
end
end,
},
{
name = "node under its family",
place = function(obj)
local family_code = obj:getFamilyCode()
if family_code then
return { family_code }
end
end,
},
}
local function parents_of(obj, get)
for _, rule in ipairs(placement_rules) do
local parents = rule.place(obj, get)
if parents then
return parents, rule.name
end
end
return {}
end
function export.build(root_code)
local get = new_cache()
local root = M.languages.getByCode(root_code, nil, true, true)
if not root then
return nil
end
local descendants, in_tree, children_of, info = {}, {}, {}, {}
local function include(code)
if not in_tree[code] then
in_tree[code] = true
descendants[#descendants + 1] = code
end
end
include(root:getCode())
for _, code in ipairs(root:getDescendantCodes()) do
include(code)
end
for _, code in ipairs(descendants) do
local obj = get("language", code, M.languages.getByCode, code, nil, true, true)
local kind = kind_of(obj)
info[code] = {
kind = kind,
category = obj:getCategoryName(),
sortkey = sortkey_of(obj),
protolanguage = kind == "family" and obj:getProtoLanguageCode() or nil,
reconstructed = obj:hasType("reconstructed"),
}
for _, parent_code in ipairs(parents_of(obj, get)) do
if in_tree[parent_code] and parent_code ~= code then
local siblings = children_of[parent_code]
if not siblings then
siblings = {}
children_of[parent_code] = siblings
end
table.insert(siblings, code)
end
end
end
local protolanguage_of = {}
for code, data in pairs(info) do
if data.protolanguage and in_tree[data.protolanguage] then
protolanguage_of[data.protolanguage] = code
end
end
local function by_sortkey(a, b)
local ka, kb = info[a].sortkey, info[b].sortkey
if ka == kb then
return a < b
end
return ka < kb
end
for _, siblings in pairs(children_of) do
table.sort(siblings, by_sortkey)
end
local function roles_of(kind, is_family_ancestor, reconstructed)
local roles = {}
if kind == "family" then
roles[#roles + 1] = "family"
end
if is_family_ancestor and reconstructed then
roles[#roles + 1] = "proto"
end
if kind == "variety" then
roles[#roles + 1] = "variety"
end
return roles
end
local function materialise(code, path)
local data = info[code]
local node = {
code = code,
kind = data.kind,
category = data.category,
sortkey = data.sortkey,
protolanguage = data.protolanguage,
protolanguage_of = protolanguage_of[code],
roles = roles_of(data.kind, protolanguage_of[code] ~= nil, data.reconstructed),
children = {},
}
local child_codes = children_of[code]
if child_codes and not path[code] then
path[code] = true
for _, child_code in ipairs(child_codes) do
table.insert(node.children, materialise(child_code, path))
end
path[code] = nil
end
return node
end
return materialise(root:getCode(), {})
end
local function is_protolanguage_pair(node)
local only_child = node.children[1]
return only_child ~= nil and node.children[2] == nil
and node.kind == "family" and node.protolanguage == only_child.code
end
local function has_non_variety_descendant(node)
for _, child in ipairs(node.children) do
if child.kind ~= "variety" or has_non_variety_descendant(child) then
return true
end
end
return false
end
function export.layout(node, options)
if options.varieties == "hide" and node.kind == "variety" and not has_non_variety_descendant(node) then
return nil
end
if options.families == "hide" and is_protolanguage_pair(node) then
return export.layout(node.children[1], options)
end
-- FIXME!!! Why do we copy the same data from `info` to `node` to `out`?
local out = {
code = node.code,
kind = node.kind,
category = node.category,
sortkey = node.sortkey,
protolanguage = node.protolanguage,
protolanguage_of = node.protolanguage_of,
roles = node.roles,
children = {},
}
for _, child in ipairs(node.children) do
local laid_out = export.layout(child, options)
if laid_out then
table.insert(out.children, laid_out)
end
end
if options.pair ~= "none" and is_protolanguage_pair(out) then
local proto = out.children[1]
out.pair = options.pair
out.partner = proto
out.children = proto.children
proto.children = {}
end
return out
end
local function canonical_name(node)
if node.kind == "family" then
return M.family_names[node.code]
elseif node.kind == "variety" then
return M.etymology_names[node.code]
end
return M.language_names[node.code]
end
local BADGE = {
family = { icon = "Kb", tooltip = "Keluarga bahasa", colour = "green" },
proto = { icon = "Bp", tooltip = "Bahasa purba", colour = "purple" },
variety = { icon = "Kl", tooltip = "Kelainan", colour = "blue" },
}
local function add_label(parent, node, options)
local name = canonical_name(node) or node.code
local span = parent:tag("span"):addClass("familytree-" .. node.kind)
if node.kind == "family" and node.protolanguage then
span:addClass("familytree-hasprotolang")
end
for _, role in ipairs(node.roles) do
if role == "proto" then
span:addClass("familytree-protolang")
end
end
span:wikitext(('[[:Kategori:%s|%s <span class="familytree-code">(%s)</span>]]')
:format(node.category, name, node.code))
for _, role in ipairs(node.roles) do
local b = BADGE[role]
span:wikitext(" ")
:tag("span")
:addClass("familytree-icon")
:attr("data-wikt-palette-p", b.colour)
:attr("title", b.tooltip)
:wikitext(b.icon)
end
if options.lemmas and node.kind == "language" then
span:wikitext(" (" .. mw.site.stats.pagesInCategory(name .. " lemmas", "pages") .. ")")
end
return span
end
local function all_children_are_varieties(node)
if not node.children[1] then
return false
end
for _, child in ipairs(node.children) do
if child.kind ~= "variety" then
return false
end
if child.children[1] and not all_children_are_varieties(child) then
return false
end
end
return true
end
local function pair_nodes(node)
if node.pair == "proto-first" then
return node.partner, node
end
return node, node.partner
end
local TOGGLE_EXPAND, TOGGLE_COLLAPSE = "[+]", "[-]"
local function render_node(list, node, options, state)
local li = list:tag("li")
if not node.children[1] and not node.partner then
add_label(li, node, options)
return
end
state.toggles = state.toggles + 1
local id = "familytree" .. state.toggles
local first, second = node, nil
if node.partner then
first, second = pair_nodes(node)
end
li:addClass("familytree-branch")
li:tag("span")
:addClass("familytree-toggle")
:addClass("mw-customtoggle-" .. id)
:wikitext(TOGGLE_COLLAPSE)
add_label(li, first, options)
local ul = li:tag("ul"):addClass("mw-collapsible")
if all_children_are_varieties(node) then
ul:addClass("familytree-only-etym-children")
end
ul:attr("id", "mw-customcollapsible-" .. id)
:attr("data-expandtext", TOGGLE_EXPAND)
:attr("data-collapsetext", TOGGLE_COLLAPSE)
if second then
add_label(ul:tag("li"):addClass("familytree-pair"), second, options)
end
for _, child in ipairs(node.children) do
render_node(ul, child, options, state)
end
end
function export.render_tree(root, options)
local state = { toggles = 1 }
local id = "familytree1"
local first, second = root, nil
if root.partner then
first, second = pair_nodes(root)
end
local div = mw.html.create("div"):addClass("familytree")
local li = div:tag("ul"):tag("li"):addClass("familytree-root")
add_label(li, first, options)
li:tag("span")
:addClass("familytree-toptoggle")
:addClass("mw-customtoggle-" .. id)
:css("display", "none")
:wikitext(options.collapsed and "Kembang" or "Runtuh")
if second then
add_label(li:tag("div"):addClass("familytree-pair"), second, options)
end
local ul = li:tag("ul"):addClass("mw-collapsible")
if options.collapsed then
ul:addClass("mw-collapsed")
end
ul:attr("id", "mw-customcollapsible-" .. id)
:attr("data-expandtext", "Kembang")
:attr("data-collapsetext", "Runtuh")
for _, child in ipairs(root.children) do
render_node(ul, child, options, state)
end
return tostring(div) .. M.TemplateStyles(STYLESHEET)
end
local DEFAULT_OPTIONS = {
families = "hide",
varieties = "hide",
pair = "none",
collapsed = false,
lemmas = false,
must_have_descendants = false,
}
local function with_defaults(options)
local out = {}
for key, default in pairs(DEFAULT_OPTIONS) do
local value = options[key]
if value == nil then
value = default
end
out[key] = value
end
return out
end
function export.render(code, options)
options = with_defaults(options or {})
local root = export.build(code)
if not root then
error(("The code %q is not a language, family or etymology-only language."):format(code))
end
root = export.layout(root, options)
if not root or (options.must_have_descendants and not root.children[1] and not root.partner) then
return nil
end
return export.render_tree(root, options)
end
function export.show(frame)
local boolean = { type = "boolean" }
local args = M.parameters.process(frame.args, {
[1] = { required = true },
families = { set = { "show", "hide" } },
varieties = { set = { "show", "hide" } },
pair = { set = { "family-first", "proto-first", "none" } },
collapsed = boolean,
lemmas = boolean,
})
return export.render(args[1], {
families = args.families,
varieties = args.varieties,
pair = args.pair,
collapsed = args.collapsed,
lemmas = args.lemmas,
}) or ""
end
return export
agovqndsh5eeu3w81066ots1k0um6af
375840
375836
2026-09-25T08:08:42Z
SNN95
2113
375840
Scribunto
text/plain
--[=[
Authors: [[User:AryamanA]], [[User:Erutuon]], [[User:Fenakhay]], [[User:JohnC5]], [[User:kc_kennylau]], [[User:Suzukaze-c]], [[User:Theknightwho]]
]=]
local export = {}
local M = require("Module:module loader").init{
require = {
languages = "Module:languages",
table = "Module:table",
yesno = "Module:yesno",
parameters = "Module:parameters",
TemplateStyles = "Module:TemplateStyles",
},
loadData = {
language_names = "Module:languages/code to canonical name",
etymology_names = "Module:etymology languages/code to canonical name",
family_names = "Module:families/code to canonical name",
},
}
local STYLESHEET = "Module:family tree/style.css"
local function new_cache()
local cache = { language = {}, family = {} }
return function(namespace, code, fetch, ...)
local slot = cache[namespace]
local obj = slot[code]
if obj == nil then
obj = fetch(...) or false
slot[code] = obj
end
return obj or nil
end
end
local function kind_of(obj)
if not obj:hasType("language") then
return "family"
elseif obj:hasType("etymology-only") then
return "variety"
end
return "language"
end
local function sortkey_of(obj)
return (obj:getCanonicalName():gsub(" Purba$", ""))
end
local placement_rules = {
{
name = "etymology-only language under its parent",
place = function(obj, get)
if not (obj:hasType("language") and obj:hasType("etymology-only")) then
return nil
end
local parent_code = obj:getParentCode()
if not parent_code then
return nil
end
local parent = get("language", parent_code, obj.getParent, obj)
if parent
and M.table.deepEquals(parent:getAncestorCodes(), obj:getAncestorCodes())
and parent:getFamilyCode() == obj:getFamilyCode() then
return { parent:getCode() }
end
end,
},
{
name = "language under its declared ancestors",
place = function(obj)
if not obj:hasType("language") then
return nil
end
local ancestors = obj:getAncestorCodes()
if ancestors[1] then
return M.table.shallowCopy(ancestors)
end
end,
},
{
name = "family under its ancestral proto-language",
place = function(obj)
if obj:hasType("language") then
return nil
end
local proto = obj:getProtoLanguageCode()
if proto and obj:hasAncestor(proto) then
return { proto }
end
end,
},
{
name = "node under its family's proto-language",
place = function(obj, get)
local family_code = obj:getFamilyCode()
if not family_code then
return nil
end
local family = get("family", family_code, obj.getFamily, obj)
local proto_code = family and family:getProtoLanguageCode()
local proto = proto_code and get("language", proto_code, family.getProtoLanguage, family)
if proto and proto:inFamily(family) and proto:getCode() ~= obj:getCode() then
return { proto:getCode() }
end
end,
},
{
name = "node under its family",
place = function(obj)
local family_code = obj:getFamilyCode()
if family_code then
return { family_code }
end
end,
},
}
local function parents_of(obj, get)
for _, rule in ipairs(placement_rules) do
local parents = rule.place(obj, get)
if parents then
return parents, rule.name
end
end
return {}
end
function export.build(root_code)
local get = new_cache()
local root = M.languages.getByCode(root_code, nil, true, true)
if not root then
return nil
end
local descendants, in_tree, children_of, info = {}, {}, {}, {}
local function include(code)
if not in_tree[code] then
in_tree[code] = true
descendants[#descendants + 1] = code
end
end
include(root:getCode())
for _, code in ipairs(root:getDescendantCodes()) do
include(code)
end
for _, code in ipairs(descendants) do
local obj = get("language", code, M.languages.getByCode, code, nil, true, true)
local kind = kind_of(obj)
info[code] = {
kind = kind,
category = obj:getCategoryName(),
sortkey = sortkey_of(obj),
protolanguage = kind == "family" and obj:getProtoLanguageCode() or nil,
reconstructed = obj:hasType("reconstructed"),
}
for _, parent_code in ipairs(parents_of(obj, get)) do
if in_tree[parent_code] and parent_code ~= code then
local siblings = children_of[parent_code]
if not siblings then
siblings = {}
children_of[parent_code] = siblings
end
table.insert(siblings, code)
end
end
end
local protolanguage_of = {}
for code, data in pairs(info) do
if data.protolanguage and in_tree[data.protolanguage] then
protolanguage_of[data.protolanguage] = code
end
end
local function by_sortkey(a, b)
local ka, kb = info[a].sortkey, info[b].sortkey
if ka == kb then
return a < b
end
return ka < kb
end
for _, siblings in pairs(children_of) do
table.sort(siblings, by_sortkey)
end
local function roles_of(kind, is_family_ancestor, reconstructed)
local roles = {}
if kind == "family" then
roles[#roles + 1] = "family"
end
if is_family_ancestor and reconstructed then
roles[#roles + 1] = "proto"
end
if kind == "variety" then
roles[#roles + 1] = "variety"
end
return roles
end
local function materialise(code, path)
local data = info[code]
local node = {
code = code,
kind = data.kind,
category = data.category,
sortkey = data.sortkey,
protolanguage = data.protolanguage,
protolanguage_of = protolanguage_of[code],
roles = roles_of(data.kind, protolanguage_of[code] ~= nil, data.reconstructed),
children = {},
}
local child_codes = children_of[code]
if child_codes and not path[code] then
path[code] = true
for _, child_code in ipairs(child_codes) do
table.insert(node.children, materialise(child_code, path))
end
path[code] = nil
end
return node
end
return materialise(root:getCode(), {})
end
local function is_protolanguage_pair(node)
local only_child = node.children[1]
return only_child ~= nil and node.children[2] == nil
and node.kind == "family" and node.protolanguage == only_child.code
end
local function has_non_variety_descendant(node)
for _, child in ipairs(node.children) do
if child.kind ~= "variety" or has_non_variety_descendant(child) then
return true
end
end
return false
end
function export.layout(node, options)
if options.varieties == "hide" and node.kind == "variety" and not has_non_variety_descendant(node) then
return nil
end
if options.families == "hide" and is_protolanguage_pair(node) then
return export.layout(node.children[1], options)
end
-- FIXME!!! Why do we copy the same data from `info` to `node` to `out`?
local out = {
code = node.code,
kind = node.kind,
category = node.category,
sortkey = node.sortkey,
protolanguage = node.protolanguage,
protolanguage_of = node.protolanguage_of,
roles = node.roles,
children = {},
}
for _, child in ipairs(node.children) do
local laid_out = export.layout(child, options)
if laid_out then
table.insert(out.children, laid_out)
end
end
if options.pair ~= "none" and is_protolanguage_pair(out) then
local proto = out.children[1]
out.pair = options.pair
out.partner = proto
out.children = proto.children
proto.children = {}
end
return out
end
local function canonical_name(node)
if node.kind == "family" then
return M.family_names[node.code]
elseif node.kind == "variety" then
return M.etymology_names[node.code]
end
return M.language_names[node.code]
end
local BADGE = {
family = { icon = "Kb", tooltip = "Keluarga bahasa", colour = "green" },
proto = { icon = "Bp", tooltip = "Bahasa purba", colour = "purple" },
variety = { icon = "Kl", tooltip = "Kelainan", colour = "blue" },
}
local function add_label(parent, node, options)
local name = canonical_name(node) or node.code
local span = parent:tag("span"):addClass("familytree-" .. node.kind)
if node.kind == "family" and node.protolanguage then
span:addClass("familytree-hasprotolang")
end
for _, role in ipairs(node.roles) do
if role == "proto" then
span:addClass("familytree-protolang")
end
end
span:wikitext(('[[:Kategori:%s|%s <span class="familytree-code">(%s)</span>]]')
:format(node.category, name, node.code))
for _, role in ipairs(node.roles) do
local b = BADGE[role]
span:wikitext(" ")
:tag("span")
:addClass("familytree-icon")
:attr("data-wikt-palette-p", b.colour)
:attr("title", b.tooltip)
:wikitext(b.icon)
end
if options.lemmas and node.kind == "language" then
span:wikitext(" (" .. mw.site.stats.pagesInCategory("Lema bahasa " .. name, "pages") .. ")")
end
return span
end
local function all_children_are_varieties(node)
if not node.children[1] then
return false
end
for _, child in ipairs(node.children) do
if child.kind ~= "variety" then
return false
end
if child.children[1] and not all_children_are_varieties(child) then
return false
end
end
return true
end
local function pair_nodes(node)
if node.pair == "proto-first" then
return node.partner, node
end
return node, node.partner
end
local TOGGLE_EXPAND, TOGGLE_COLLAPSE = "[+]", "[-]"
local function render_node(list, node, options, state)
local li = list:tag("li")
if not node.children[1] and not node.partner then
add_label(li, node, options)
return
end
state.toggles = state.toggles + 1
local id = "familytree" .. state.toggles
local first, second = node, nil
if node.partner then
first, second = pair_nodes(node)
end
li:addClass("familytree-branch")
li:tag("span")
:addClass("familytree-toggle")
:addClass("mw-customtoggle-" .. id)
:wikitext(TOGGLE_COLLAPSE)
add_label(li, first, options)
local ul = li:tag("ul"):addClass("mw-collapsible")
if all_children_are_varieties(node) then
ul:addClass("familytree-only-etym-children")
end
ul:attr("id", "mw-customcollapsible-" .. id)
:attr("data-expandtext", TOGGLE_EXPAND)
:attr("data-collapsetext", TOGGLE_COLLAPSE)
if second then
add_label(ul:tag("li"):addClass("familytree-pair"), second, options)
end
for _, child in ipairs(node.children) do
render_node(ul, child, options, state)
end
end
function export.render_tree(root, options)
local state = { toggles = 1 }
local id = "familytree1"
local first, second = root, nil
if root.partner then
first, second = pair_nodes(root)
end
local div = mw.html.create("div"):addClass("familytree")
local li = div:tag("ul"):tag("li"):addClass("familytree-root")
add_label(li, first, options)
li:tag("span")
:addClass("familytree-toptoggle")
:addClass("mw-customtoggle-" .. id)
:css("display", "none")
:wikitext(options.collapsed and "Kembang" or "Runtuh")
if second then
add_label(li:tag("div"):addClass("familytree-pair"), second, options)
end
local ul = li:tag("ul"):addClass("mw-collapsible")
if options.collapsed then
ul:addClass("mw-collapsed")
end
ul:attr("id", "mw-customcollapsible-" .. id)
:attr("data-expandtext", "Kembang")
:attr("data-collapsetext", "Runtuh")
for _, child in ipairs(root.children) do
render_node(ul, child, options, state)
end
return tostring(div) .. M.TemplateStyles(STYLESHEET)
end
local DEFAULT_OPTIONS = {
families = "hide",
varieties = "hide",
pair = "none",
collapsed = false,
lemmas = false,
must_have_descendants = false,
}
local function with_defaults(options)
local out = {}
for key, default in pairs(DEFAULT_OPTIONS) do
local value = options[key]
if value == nil then
value = default
end
out[key] = value
end
return out
end
function export.render(code, options)
options = with_defaults(options or {})
local root = export.build(code)
if not root then
error(("The code %q is not a language, family or etymology-only language."):format(code))
end
root = export.layout(root, options)
if not root or (options.must_have_descendants and not root.children[1] and not root.partner) then
return nil
end
return export.render_tree(root, options)
end
function export.show(frame)
local boolean = { type = "boolean" }
local args = M.parameters.process(frame.args, {
[1] = { required = true },
families = { set = { "show", "hide" } },
varieties = { set = { "show", "hide" } },
pair = { set = { "family-first", "proto-first", "none" } },
collapsed = boolean,
lemmas = boolean,
})
return export.render(args[1], {
families = args.families,
varieties = args.varieties,
pair = args.pair,
collapsed = args.collapsed,
lemmas = args.lemmas,
}) or ""
end
return export
iyqdxadke6t00lb6nxiy5jxzn8yqqxf
Templat:bukan kognat
10
34982
375834
230217
2026-09-25T07:03:29Z
SNN95
2113
375834
wikitext
text/x-wiki
<includeonly>{{#invoke:etymology/templates|noncognate}}</includeonly><!--
--><noinclude>{{ncog|und|ujian}}{{documentation}}</noinclude>
9plpkkahn5l3p70k983o6hfhteyd0c2
Modul:tl-sortkey
828
35240
375843
147135
2026-09-25T09:00:42Z
SNN95
2113
kemaskini
375843
Scribunto
text/plain
local export = {}
local u = mw.ustring.char
local a, b = u(0xF000), u(0xF001)
local oneChar = {
["ë"] = "e" .. a, ["ñ"] = "n" .. a
}
local twoChars = {
["ng"] = "n" .. b, ["ng̃"] = "n" .. b, ["ñg"] = "n" .. b
}
function export.makeSortKey(text, lang, sc)
text = mw.ustring.gsub(text, "([!-&])([^%s]+)", "%2%1")
for from, to in pairs(twoChars) do
text = text:gsub(from, to)
end
return mw.ustring.upper(mw.ustring.gsub(mw.ustring.toNFC(text), ".", oneChar))
end
local tl = require("Module:languages").getByCode("tl")
local function tag(text)
return require("Module:script utilities").tag_text(text, tl)
end
local showsubst = {
[a] = "₂",
[b] = "₃"
}
function export.showSortkey(frame)
local output = {}
for _, word in ipairs(frame.args) do
local sc = tl:findBestScript(word):getCode()
local sortkey = mw.ustring.gsub(export.makeSortKey(word, "tl", sc), ".", showsubst)
sortkey = mw.ustring.gsub(sortkey, "([^0-9²])$", "%1¹")
sortkey = mw.ustring.gsub(sortkey, "([^0-9²])(%s)", "%1¹%2")
local example = "\n* <code>" .. sortkey .. "</code>\n: " .. tag(word)
table.insert(output, example)
end
return table.concat(output)
end
function export.showSorting(frame)
local terms = {}
for _, term in ipairs(frame.args) do
table.insert(terms, term)
end
local makeSortKey = require("Module:memoize")(export.makeSortKey)
local function comp(term1, term2)
return makeSortKey(term1) < makeSortKey(term2)
end
table.sort(terms, comp)
for i, term in pairs(terms) do
local sc = tl:findBestScript(term):getCode()
local sortkey = mw.ustring.gsub(export.makeSortKey(term, "tl", sc), ".", showsubst)
sortkey = mw.ustring.gsub(sortkey, "([^0-9²])$", "%1¹")
sortkey = mw.ustring.gsub(sortkey, "([^0-9²])(%s)", "%1¹%2")
terms[i] = "\n* " .. tag(term) .. " (<code>" .. sortkey .. "</code>)"
end
return table.concat(terms)
end
return export
23dbyqy9y604ohmrfdlqf06q7opklut
apanat
0
39659
375819
284678
2026-09-25T02:48:32Z
Hakimi97
2668
Menambah kod bahasa
375819
wikitext
text/x-wiki
==Bahasa Kadazandusun==
===Kata adjektif===
{{inti|dtp|kata adjektif}}
# penat
#:{{apanat e tapaku mantad karajawon/bapa saya penat dari kerja
==Bahasa Sungai==
===Kata adjektif===
{{inti|abf|kata adjektif}}
# penat
shp6vnrx0obqqi4sg5ej23vhprg7get
375820
375819
2026-09-25T02:49:56Z
Hakimi97
2668
Tambah templat:cp
375820
wikitext
text/x-wiki
==Bahasa Kadazandusun==
===Kata adjektif===
{{inti|dtp|kata adjektif}}
# penat
#:{{cp|dtp|apanat e tapaku mantad karajawon|bapa saya penat dari kerja}}
==Bahasa Sungai==
===Kata adjektif===
{{inti|dmg|kata adjektif}}
# penat
l4cxa6qln5m3qto69rv766rurhst1jj
Sumambayang
0
39909
375817
285632
2026-09-25T02:43:13Z
Hakimi97
2668
Tolong hapuskan laman ini
375817
wikitext
text/x-wiki
{{delete|sudah ada [[sumambayang]]}}
6xsg1aa5akgokk1a7n9fp4gzxhn2smg
Modul:it-pronunciation
828
48700
375829
166892
2026-09-25T04:16:50Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/92307983|92307983]])
375829
Scribunto
text/plain
--[=[
This module implements the templates {{it-pr}} and {{it-IPA}}.
Author: benwing2
FIXME:
1. Support raw pronunciations in {{it-pr}}. (DONE)
2. ahimè should generate aj.mɛ not a.i.mɛ. (DONE)
3. oriuòlo should divide as o.riuò.lo, both in phonemic and hyphenation. (DONE)
4. Handle <hmp:...> for homophones. (DONE)
5. Raw pronunciations need to use raw:, esp. for [...] phonetic pronunciation. (DONE)
6. Handle hyphenation of Uppsala, massmediale correctly. (DONE)
7. Homophone may end up before rhyme/hyphenation when it should come after, e.g. in [[hanno]].
8. Cases like [[Katmandu]] with respelling ''Katmandù'' should auto-hyphenate.
]=]
local export = {}
local force_cat = false -- for testing
local audio_module = "Module:audio"
local homophones_module = "Module:homophones"
local hyphenation_module = "Module:hyphenation"
local ipa_module = "Module:IPA"
local it_common_module = "Module:it-common"
local pages_module = "Module:pages"
local parameters_module = "Module:parameters"
local parse_utilities_module = "Module:parse utilities"
local references_module = "Module:references"
local rhymes_module = "Module:rhymes"
local string_char_module = "Module:string/char"
local string_pattern_escape_module = "Module:string/patternEscape"
local string_replacement_escape_module = "Module:string/replacementEscape"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local com = require(it_common_module)
local concat = table.concat
local insert = table.insert
local remove = table.remove
local rep = string.rep
local require = require
local rsub = com.rsub
local rsub_repeatedly = com.rsub_repeatedly
local rsubb = com.rsubb
local toNFC = mw.ustring.toNFC
local u = require(string_char_module)
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local function deep_equals(...)
deep_equals = require(table_module).deepEquals
return deep_equals(...)
end
local function format_audio(...)
format_audio = require(audio_module).format_audio
return format_audio(...)
end
local function format_homophones(...)
format_homophones = require(homophones_module).format_homophones
return format_homophones(...)
end
local function format_hyphenations(...)
format_hyphenations = require(hyphenation_module).format_hyphenations
return format_hyphenations(...)
end
local function format_IPA_full(...)
format_IPA_full = require(ipa_module).format_IPA_full
return format_IPA_full(...)
end
local function format_rhymes(...)
format_rhymes = require(rhymes_module).format_rhymes
return format_rhymes(...)
end
local function insert_if_not(...)
insert_if_not = require(table_module).insertIfNot
return insert_if_not(...)
end
local function list_to_set(...)
list_to_set = require(table_module).listToSet
return list_to_set(...)
end
local function parse_balanced_segment_run(...)
parse_balanced_segment_run = require(parse_utilities_module).parse_balanced_segment_run
return parse_balanced_segment_run(...)
end
local function parse_multi_delimiter_balanced_segment_run(...)
parse_multi_delimiter_balanced_segment_run = require(parse_utilities_module).parse_multi_delimiter_balanced_segment_run
return parse_multi_delimiter_balanced_segment_run(...)
end
local function parse_references(...)
parse_references = require(references_module).parse_references
return parse_references(...)
end
local function pattern_escape(...)
pattern_escape = require(string_pattern_escape_module)
return pattern_escape(...)
end
local function process_params(...)
process_params = require(parameters_module).process
return process_params(...)
end
local function replacement_escape(...)
replacement_escape = require(string_replacement_escape_module)
return replacement_escape(...)
end
local function split(...)
split = require(string_utilities_module).split
return split(...)
end
local function split_alternating_runs(...)
split_alternating_runs = require(parse_utilities_module).split_alternating_runs
return split_alternating_runs(...)
end
local function ufind(...)
ufind = require(string_utilities_module).find
return ufind(...)
end
local function ugsub(...)
ugsub = require(string_utilities_module).gsub
return ugsub(...)
end
local function ulen(...)
ulen = require(string_utilities_module).len
return ulen(...)
end
local function ulower(...)
ulower = require(string_utilities_module).lower
return ulower(...)
end
local function umatch(...)
umatch = require(string_utilities_module).match
return umatch(...)
end
local function usub(...)
usub = require(string_utilities_module).sub
return usub(...)
end
local lang = require("Module:languages").getByCode("it")
-- Temporarily substitutes for comma+space.
local TEMP1 = u(0xFFF0)
local SYLDIV = u(0xFFF0) -- used to represent a user-specific syllable divider (.) so we won't change it
local WORDDIV = u(0xFFF1) -- used to represent a user-specific word divider (.) so we won't change it
local TEMP_Z = u(0xFFF2)
local TEMP_S = u(0xFFF3)
local TEMP_H = u(0xFFF4)
local TEMP_X = u(0xFFF5)
local glide = "jwJW"
local liquid = "lrLR"
local tie = "‿⁀'"
local W = "[" .. glide .. "]"
local W_OR_TIE = "[" .. glide .. tie .. "]"
-- We include both phonemic and spelling forms of vowels and both lowercase and uppercase
-- for flexibility in applying at various stages of the transformation from spelling -> phonemes.
local vowel_not_high = "aeɛoɔøöüAEƐOƆØÖÜ"
local vowel_not_i = vowel_not_high .. "uU"
local vowel_not_u = vowel_not_high .. "iyIY"
local vowel = vowel_not_high .. "iuyIUY"
local V = "[" .. vowel .. "]"
local V_NOT_HIGH = "[" .. vowel_not_high .. "]"
local V_NOT_I = "[" .. vowel_not_i .. "]"
local V_NOT_U = "[" .. vowel_not_u .. "]"
local VW = "[" .. vowel .. glide .. "]"
local NV = "[^" .. vowel .. "]"
local charsep_not_tie = com.accent .. "." .. SYLDIV
local charsep_not_tie_c = "[" .. charsep_not_tie .. "]"
local charsep = charsep_not_tie .. tie
local charsep_c = "[" .. charsep .. "]"
local wordsep_not_tie = charsep_not_tie .. " #"
local wordsep = charsep .. " #"
local wordsep_c = "[" .. wordsep .. "]"
local cons_guts = "^" .. vowel .. wordsep .. "_" -- guts of consonant class
local C = "[" .. cons_guts .. "]" -- consonant
local C_NOT_SRZ = "[" .. cons_guts .. "srzSRZ]" -- consonant not including srz
local C_NOT_SIBILANT_OR_R = "[" .. cons_guts .. "rszʃʒʦʣʧʤRSZ" .. TEMP_S .. TEMP_Z .. "]" -- consonant not including r or sibilant
local C_NOT_H = "[" .. cons_guts .. "hH]" -- consonant not including h
local C_OR_EOW_NOT_GLIDE_LIQUID = "[^" .. vowel .. charsep .. " _" .. glide .. liquid .. "]" -- consonant not lrjw, or end of word
local C_OR_TIE = "[^" .. vowel .. wordsep_not_tie .. "_]" -- consonant or tie (‿⁀')
local front = "eɛij"
local front_c = "[" .. front .. "]"
local voiced_C_c = "[bdglmnrvʣʤŋʎɲ]"
local pron_sign = "#!*°"
local pron_sign_c = "[" .. pron_sign .. "]"
local full_affricates = { ["ʦ"] = "t͡s", ["ʣ"] = "d͡z", ["ʧ"] = "t͡ʃ", ["ʤ"] = "d͡ʒ" }
local recognized_suffixes = {
-- -(m)ente, -(m)ento
{"izzamento", "iddzaménto"}, -- must precede -mento below
{"ment([eo])", "mént%1"}, -- must precede -ente/o below; must follow -izzamento above
{"ent([eo])", "ènt%1"}, -- must follow -mente/o above
-- verbs
{"izzare", "iddzàre"}, -- must precede -are below
{"izzarsi", "iddzàrsi"}, -- must precede -arsi below
{"([ai])re", "%1" .. com.GR .. "re"}, -- must follow -izzare above
{"([ai])rsi", "%1" .. com.GR .. "rsi"}, -- must follow -izzarsi above
-- nouns
{"izzatore", "iddzatóre"}, -- must precede -tore below
{"([st])ore", "%1óre"}, -- must follow -izzatore above
{"izzatrice", "iddzatrìce"}, -- must precede -trice below
{"trice", "trìce"}, -- must follow -izzatrice above
{"izzazione", "iddzatsióne"}, -- must precede -zione below
{"zione", "tsióne"}, -- must precede -one below and follow -izzazione above
{"one", "óne"}, -- must follow -zione above
{"acchio", "àcchio"},
{"acci([ao])", "àcci%1"},
{"([aiu])ggine", "%1" .. com.GR .. "ggine"},
{"aggio", "àggio"},
{"([ai])gli([ao])", "%1" .. com.GR .. "gli%2"},
{"ai([ao])", "ài%1"},
{"([au])me", "%1" .. com.GR .. "me"},
{"([ae])nza", "%1" .. com.GR .. "ntsa"},
{"ario", "àrio"},
{"([st])orio", "%1òrio"},
{"astr([ao])", "àstr%1"},
{"ell([ao])", "èll%1"},
-- exceptions to the following: antimatèria, artèria, cattivèria, fèria, Ibèria, Libèria, matèria, misèria, Nigèria, Sibèria, Valèria
{"eria", "erìa"},
{"etta", "étta"},
-- do not include -etto, both ètto and étto are common
{"ezza", "éttsa"},
{"ficio", "fìcio"},
{"ier([ao])", "ièr%1"},
-- do not include -iere, lots of verbs in unstressed -iere
{"ifero", "ìfero"},
{"ism([io])", "ìsm%1"},
{"ista", "ìsta"},
{"izi([ao])", "ìtsi%1"},
{"logia", "logìa"},
-- do not include -otto, both òtto and ótto are common
{"tudine", "tùdine"},
{"ura", "ùra"},
{"([^aeo])uro", "%1ùro"},
-- adjectives
{"izzante", "iddzànte"}, -- must precede -ante below
{"ante", "ànte"}, -- must follow -izzante above
{"izzando", "iddzàndo"}, -- must precede -ando below
{"([ae])ndo", "%1" .. com.GR .. "ndo"}, -- must follow -izzando above
{"izzabile", "iddzàbile"}, -- must precede -abile below
{"([ai])bile", "%1" .. com.GR .. "bile"}, --must follow -izzabile above
{"ale", "àle"},
{"([aeiou])nico", "%1" .. com.GR .. "nico"},
{"([ai])stic([aio])", "%1" .. com.GR .. "stic%2"},
{"([ai])stiche", "%1" .. com.GR .. "stiche"},
{"izzat([ao])", "iddzàt%1"}, -- must precede -at[ao] below
-- exceptions to the following: àbato, àcato, acròbata, àgata, apòstata, àstato, cìato, fégato, omeòpata,
-- sàb(b)ato, others?
{"at([ao])", "àt%1"}, -- must follow -izzat[ao] above
-- exceptions to the following: (s)còmputo, (pre)scòrbuto, tànguto; cùscuta, dìsputa, rècluta, lui vàluta
{"([^aeo])ut([ao])", "%1ùt%2"},
{"([ae])tic([ao])", "%1" .. com.GR .. "tic%2"},
{"ense", "ènse"},
{"esc([ao])", "ésc%1"},
{"evole", "évole"},
-- FIXME: Systematic exceptions to the following in 3rd plural present tense verb forms
{"ian([ao])", "iàn%1"},
{"iv([ao])", "ìv%1"},
{"oide", "òide"},
{"oso", "óso"},
}
local unstressed_words = list_to_set{
"il", "lo", "la", "i", "gli", "le", -- definite articles
"un", -- indefinite articles
"mi", "ti", "si", "ci", "vi", "li", -- object pronouns
"me", "te", "se", "ce", "ve", "ne", -- conjunctive object pronouns
"e", "ed", "o", "od", -- conjunctions
"ho", "hai", "ha", -- forms of [[avere]]
"chi", "che", "non", -- misc particles
"di", "del", "dei", -- prepositions
"a", "ad", "al", "ai",
"da", "dal", "dai",
"in", "nel", "nei",
"con", "col", "coi",
"su", "sul", "sui",
"per", "pei",
"tra", "fra",
}
-- Canonicalize multiple spaces and remove leading and trailing spaces.
local function canon_spaces(text)
text = rsub(text, "%s+", " ")
text = rsub(text, "^ ", "")
text = rsub(text, " $", "")
return text
end
-- Remove word-final accents on monosyllabic words. NOTE: `text` on entry must be decomposed using decompose().
local function remove_final_monosyllabic_accents(text)
local words = com.split_but_rejoin_affixes(text)
for i, word in ipairs(words) do
if (i % 2) == 1 then -- an actual word, not a separator
word = rsub(word, "^(" .. NV .. "*" .. V .. ")" .. com.accent_c .. "$", "%1")
words[i] = word
end
end
return concat(words)
end
-- Return true if all words in `term` have vowels. NOTE: `term` on entry must be decomposed using com.decompose().
local function all_words_have_vowels(term)
local words = com.split_but_rejoin_affixes(term)
for i, word in ipairs(words) do
if (i % 2) == 1 and not ufind(word, V) then -- an actual word, not a separator; check for a vowel
return false
end
end
return true
end
-- Convert respelling conventions back to the original spelling. This does not affect accents or syllable dividers.
local function convert_respelling_to_original(respelling)
-- discard second return value
respelling = respelling:gsub("ddz", "zz"):gsub("tts", "zz"):gsub("dz", "z"):gsub("ts", "z")
:gsub("Dz", "Z"):gsub("Ts", "Z"):gsub("%[([szh])%]", "%1"):gsub("%[w%]", "u")
:gsub("ʎi", "gli"):gsub("ʎ", "gli")
return respelling
end
-- Given raw respelling, canonicalize it and apply auto-accenting where warranted. This does the following:
-- (1) Convert abbreviated specs like ^à to the appropriate accented page name (hence the page name must be passed in).
-- (2) Decompose the text, normalize áíú and similar to àìù, convert commas and em/en dashes to foot boundaries and
-- similarly with other punctuation.
-- (3) Apply suffix respellings as appropriate, e.g -zione -> -tsióne.
-- (4) Auto-accent monosyllabic and bisyllabic words when possible.
-- (5) Throw an error if non-unstressed words remain without accents on them.
local function canonicalize_and_auto_accent(text, pagename)
text = com.decompose(text)
pagename = com.decompose(pagename)
-- First apply abbrev spec e.g. ^à or ^Ó if given.
if text == "+" then
text = pagename
elseif ufind(text, "^%[[wxzsh]%]$") or text == "[tʃ]" or text == "[dʒ]" then
-- Don't do anything to bare use of [C] notation; don't interpret as substitution.
else
if ufind(text, "^%^[aeiouAEIOU]" .. com.GR .. "$") or ufind(text, "^%^[eoEO]" .. com.AC .. "$") then
text = "[" .. text .. "]"
end
-- Implement substitution notation.
if ufind(text, "^%[.*%]$") then
local subs = split(umatch(text, "^%[(.*)%]$"), ",", true)
text = pagename
local function err(msg)
error(msg .. ": " .. text)
end
for _, sub in ipairs(subs) do
if ufind(sub, "^%^[aeiouAEIOU]" .. com.GR .. "$") or ufind(sub, "^%^[eoEO]" .. com.AC .. "$") then
-- single vowel spec
local abbrev_text = sub
if text:find(" ") or text:find("[^ ]%-[^ ]") then
err("With abbreviated vowel spec " .. abbrev_text .. ", the processed page name should be a single word")
end
if ufind(text, com.quality_c) then
err("With abbreviated vowel spec " .. abbrev_text .. ", the processed page name should not already have an accent")
end
local vowel_count = ulen(rsub(text, NV, ""))
local abbrev_sub = abbrev_text:gsub("%^", "")
local abbrev_vowel = usub(abbrev_sub, 1, 1)
if vowel_count == 0 then
err("Abbreviated spec " .. abbrev_text .. " can't be used with nonsyllabic word")
elseif vowel_count == 1 then
local before, vow, after = umatch(text, "^(.*)(" .. V .. ")(" .. NV .. "*)$")
if not before then
err("Internal error: Couldn't match monosyllabic word")
end
if abbrev_vowel ~= vow then
err("Abbreviated spec " .. abbrev_text .. " doesn't match vowel " .. vow)
end
text = before .. abbrev_sub .. after
else
local before, penultimate, after = umatch(text, "^(.-)(" .. V .. ")(" .. NV .. "*" .. V .. NV .. "*)$")
if not before then
err("Internal error: Couldn't match multisyllabic word")
end
local before2, antepenultimate, after2 = umatch(before, "^(.-)(" .. V .. ")(" .. NV .. "*)$")
if abbrev_vowel ~= penultimate and abbrev_vowel ~= antepenultimate then
err("Abbreviated spec " .. abbrev_text .. " doesn't match penultimate vowel " ..
penultimate .. (antepenultimate and " or antepenultimate vowel " ..
antepenultimate or ""))
end
if penultimate == antepenultimate then
err("Can't use abbreviated spec " .. abbrev_text .. " here because penultimate and " ..
"antepenultimate are the same")
end
if abbrev_vowel == antepenultimate then
text = before2 .. abbrev_sub .. after2 .. penultimate .. after
elseif abbrev_vowel == penultimate then
text = before .. abbrev_sub .. after
else
err("Internal error: abbrev_vowel from abbrev_text " .. abbrev_text ..
" didn't match any vowel or glide")
end
end
else
local from, escaped_from, to, escaped_to
if sub:find(":") then
from, to = umatch(sub, "^(.-):(.*)$")
else
to = sub
from = convert_respelling_to_original(to)
from = com.remove_accents(from)
from = from:gsub("[.*]", "")
end
if ufind(from, "^~") then
-- whole-word match
from = umatch(from, "^~(.*)$")
escaped_from = "%f[%a]" .. pattern_escape(from) .. "%f[%A]"
else
escaped_from = pattern_escape(from)
end
escaped_to = replacement_escape(to)
local subbed_text, nsubs = ugsub(text, escaped_from, escaped_to)
if nsubs == 0 then
err(("Substitution spec %s -> %s didn't match processed pagename"):format(from, to))
elseif nsubs > 1 then
err(("Substitution spec %s -> %s matched multiple substrings in processed pagename, add more context"):format(from, to))
else
text = subbed_text
end
end
end
end
end
text = rsub(text, "([aiuöüyAIUÖÜY])" .. com.AC, "%1" .. com.GR) -- áíú -> àìù
-- convert commas and en/en dashes to IPA foot boundaries
text = rsub_repeatedly(text, "%s*[,–—]%s*", " | ")
-- question mark or exclamation point in the middle of a sentence -> IPA foot boundary
text = rsub_repeatedly(text, "([^%s])%s*[!?]%s*([^%s])", "%1 | %2")
text = rsub(text, "[!?]", "") -- eliminate remaining punctuation
text = canon_spaces(text)
local words = com.split_but_rejoin_affixes(text)
for i, word in ipairs(words) do
if (i % 2) == 1 then -- an actual word, not a separator
local function err(msg)
error(msg .. ": " .. words[i])
end
local is_prefix = word:find("%-$")
local is_suffix = word:find("^%-")
if not is_prefix then
if not ufind(word, com.quality_c) then
-- Apply suffix respellings.
for _, suffix_pair in ipairs(recognized_suffixes) do
local orig, respelling = unpack(suffix_pair)
local replaced
word, replaced = rsubb(word, orig .. "$", respelling)
if replaced then
-- Decompose again because suffix replacements may have accented chars.
word = com.decompose(word)
break
end
end
end
-- Auto-stress some monosyllabic and bisyllabic words. Don't auto-stress inherently unstressed words
-- (including those with a * at the end of them indicating syntactic gemination).
if not unstressed_words[rsub(word, "%*$", "")] and not ufind(word, "[" .. com.AC .. com.GR .. com.DOTOVER .. "]") then
local vowel_count = ulen(rsub(word, NV, ""))
if vowel_count > 2 then
err("With more than two vowels and an unrecognized suffix, stress must be explicitly given")
elseif not is_suffix or vowel_count == 2 then -- don't try to stress suffixes with only one vowel
local before, vow, after = umatch(word, "^(.-)(" .. V .. ")(.*)$")
if before then
if ufind(vow, "^[eoEO]$") then
err("When stressed vowel is e or o, it must be marked é/è or ó/ò to indicate quality")
end
word = before .. vow .. com.GR .. after
end
end
end
end
words[i] = word
end
end
return words
end
function export.to_phonetic(respelling)
-- Checks the delimeters of the transcription.
local raw = umatch(respelling, "^/(.*)/$")
local raw_type
if raw then
raw_type = "phonemic"
else
raw = umatch(respelling, "^%[(.*)%]$")
if raw then
raw_type = "phonetic"
end
end
local pron = {
orig_respelling = respelling,
canon_respelling = com.decompose(respelling),
phonemic = raw,
raw = raw_type,
}
-- Checks the last phone. Should be made to also support all the IPA vowels.
pron.last_word_ends_in_vowel = ufind(raw, V .. '$')
pron.last_word_ends_in_consonant = not pron.last_word_ends_in_vowel
return pron
end
function export.to_phonemic(text, pagename)
local orig_respelling = text
local words = canonicalize_and_auto_accent(text, pagename)
text = concat(words)
local canon_respelling = text
text = ulower(text)
text = rsub(text, com.CFLEX, "") -- eliminate circumflex over î, etc.
text = rsub(text, "y", "i")
text = rsub_repeatedly(text, "([^ ])'([^ ])", "%1‿%2") -- apostrophe between letters is a tie
text = rsub(text, "(" .. C .. ")'$", "%1‿") -- final apostrophe after a consonant is a tie, e.g. [[anch']]
text = rsub(text, "(" .. C .. ")' ", "%1‿ ") -- final apostrophe in non-utterance-final word is a tie
text = rsub(text, "'", "") -- other apostrophes just get removed, e.g. [['ndragheta]], [[ca']].
-- For now, use a special marker of syntactic gemination at beginning of word; later we will
-- convert to ‿ and remove the space.
text = rsub(text, "%*([ %-])(" .. C .. ")", "%1⁀%2")
if ufind(text, "%*[ %-]") then
error("* for syntactic gemination can only be used when the next word begins with a consonant: " .. canon_respelling)
end
words = com.split_but_rejoin_affixes(text)
for i, word in ipairs(words) do
if (i % 2) == 1 then -- an actual word, not a separator
-- Words marked with an acute or grave (quality marker) not followed by an indicator of secondary stress
-- or non-stress, and not marked with com.DOTOVER (unstressed word), get primary stress.
if not word:find(com.DOTOVER) then
word = rsub(word, "(" .. com.quality_c .. ")([^" .. com.DOTUNDER .. com.LINEUNDER .. "])", "%1ˈ%2")
word = rsub(word, "(" .. com.quality_c .. ")$", "%1ˈ")
end
-- Apply quality markers: è -> ɛ, ò -> ɔ
word = rsub(word, "[eo]" .. com.GR, {
["e" .. com.GR] = "ɛ",
["o" .. com.GR] = "ɔ",
})
-- Eliminate quality markers and com.DOTOVER/com.DOTUNDER, which have served their purpose.
word = rsub(word, "[" .. com.quality .. com.DOTOVER .. com.DOTUNDER .. "]", "")
-- com.LINEUNDER means secondary stress.
word = rsub(word, com.LINEUNDER, "ˌ")
-- Make prefixes unstressed. Primary stress markers become secondary.
if word:find("%-$") then
word = rsub(word, "ˈ", "ˌ")
end
words[i] = word
end
end
text = concat(words)
-- Convert hyphens to spaces, to handle [[Austria-Hungria]], [[franco-italiano]], etc.
text = rsub(text, "%-", " ")
-- canonicalize multiple spaces again, which may have been introduced by hyphens
text = canon_spaces(text)
-- put # at word beginning and end and double ## at text/foot boundary beginning/end
text = rsub(text, " | ", "# | #")
text = "##" .. rsub(text, " ", "# #") .. "##"
-- Random consonant substitutions.
text = rsub(text, "%[w%]", "w") -- [w] means /w/ when the spelling is ⟨u⟩, esp. in ⟨ui⟩ sequences. This helps with hyphenation.
text = rsub(text, "%[x%]", TEMP_X) -- [x] means /x/
text = rsub(text, "#ex(" .. V .. ")", "eg[z]%1")
text = text:gsub("x", "ks"):gsub("ck", "k"):gsub("sh", "ʃ")
text = rsub(text, TEMP_X, "x")
text = rsub(text, "%[z%]", TEMP_Z) -- [z] means /z/
text = rsub(text, "%[s%]", TEMP_S) -- [z] means /s/
text = rsub(text, "%[h%]", TEMP_H) -- [h] means /h/
-- ci, gi + vowel
-- Do ci, gi + e, é, è sometimes contain /j/?
text = rsub(text,
"([cg])([cg]?)i(" .. V .. ")", function(c, double, v)
local out_cons
if c == "c" then
out_cons = "ʧ"
else
out_cons = "ʤ"
end
if double ~= "" then
if double ~= c then
error("Invalid sequence " .. c .. double .. ".")
end
out_cons = out_cons .. out_cons
end
return out_cons .. v
end)
-- Handle gl and gn.
text = rsub(text, "gn", "ɲ")
-- The vast majority of words beginning with gli- have /ɡl/ not /ʎ/ so don't substitute here, although we special-case
-- [[gli]]. Use ʎ exlicitly to get it in [[glielo]] and such.
text = rsub(text, "#gli#", "#ʎi#")
text = rsub_repeatedly(text, "([^#])gli(" .. V .. ")", "%1ʎ%2")
text = rsub_repeatedly(text, "([^#])gl(‿?i)", "%1ʎ%2")
-- Handle other cases of c, g.
text = rsub(text, "([cg])([cg]?)(h?)(" .. charsep_c .. "*.)", function(first, double, h, after)
-- Don't allow the combinations cg, gc. Or do something else?
if double ~= "" and double ~= first then
error("Invalid sequence " .. first .. double .. ".")
end
-- c, g is soft before e, i.
local cons
if ufind(after, front_c) and not ufind(h, "h") then
if first == "c" then
cons = "ʧ"
else
cons = "ʤ"
end
else
if first == "c" then
cons = "k"
else
cons = "g"
end
end
if double ~= "" then
cons = cons .. cons
end
return cons .. after
end)
-- sc before e, i is /ʃ/, doubled after a vowel.
text = text:gsub("sʧ", "ʃ")
text = rsub(text, "%[tʃ%]", "ʧ")
text = rsub(text, "%[dʒ%]", "ʤ")
text = rsub(text, "ddz", "ʣʣ")
text = rsub(text, "dz", "ʣ")
text = rsub(text, "tts", "ʦʦ")
text = rsub(text, "ts", "ʦ")
if ufind(text, "z") then
error("z must be respelled (d)dz or (t)ts: " .. canon_respelling)
end
-- ⟨qu⟩ represents /kw/.
text = text:gsub("qu", "kw")
-- ⟨gu⟩ (unstressed) + vowel represents /gw/.
text = text:gsub("gu(" .. V .. ")", "gw%1")
text = rsub(text, "q", "k") -- [[soqquadro]], [[qatariota]], etc.
-- Assimilate n before labial, including across word boundaries; DiPI marks pronunciations like
-- /ʤanˈpaolo/ for [[Gian Paolo]] as wrong. To prevent this, use _ or h between n and following labial.
text = rsub(text, "n(" .. wordsep_c .. "*[mpb])", "m%1")
-- Remove 'h' before converting vowels to glides; h should not block e.g. ahimè -> aj.mɛ.
text = text:gsub("h", "")
-- Unaccented u or i following a non-high vowel (with or without accent) is a semivowel. Exclude high vowels because
-- 'iu' should be interpreted as /ju/ not /iw/, and 'ii' (as in [[sii]]) and ''uu'' (as in [[duumvirato]]), should
-- remain as vowels. We handle ui specially. By preceding the conversion of glides before vowels, this works
-- correctly in the common sequence 'aiuo' e.g. [[guerraiuola]], [[acquaiuolo]]. Note that ci, gi + vowel, gli, qu
-- must be dealt with beforehand.
text = rsub_repeatedly(text, "(" .. V_NOT_HIGH .. com.accent_c .. "*)([iu])([^" .. com.accent .. "])", function(v, gl, acc)
return v .. (gl == "i" and "j" or "w") .. acc
end)
text = rsub_repeatedly(text, "(u" .. com.accent_c .. "*)i([^" .. com.accent .. "])", "%1j%2")
-- Unaccented i or u before another vowel is a glide. Separate into i and u cases to avoid converting ii or uu
-- except in the sequences iiV or uuV. Do i first so [[oriuolo]] -> or.jwɔ.lo.
text = rsub(text, "i(" .. V_NOT_I .. ")", "j%1")
text = rsub(text, "u(" .. V_NOT_U .. ")", "w%1")
-- Double consonant followed by end of word (e.g. [[stress]], [[staff]], [[jazz]]), or followed by a consonant
-- other than a glide or liquid (e.g. [[pullman]], [[Uppsala]]), should be reduced to single. Should not affect double
-- consonants between vowels or before glides (e.g. [[occhio]], [[acqua]]) or liquids ([[pubblico]], [[febbraio]]),
-- or words before a tie ([[mezz']], [[tutt']]).
text = rsub_repeatedly(text, "(" .. C .. ")%1(" .. charsep_not_tie_c .. "*" .. C_OR_EOW_NOT_GLIDE_LIQUID .. ")", "%1%2")
-- Between vowels (including glides), /ʃ ʎ ɲ t͡s d͡z/ are doubled (unless already doubled).
-- Not simply after a vowel; 'z' is not doubled in e.g. [[azteco]].
text = rsub_repeatedly(text, "(" .. VW .. com.stress_c .. "?" .. charsep_c .. "*)([ʦʣʃʎɲ])(" .. charsep_c .. "*" .. VW .. ")",
"%1%2%2%3")
-- Change user-specified . into SYLDIV so we don't shuffle it around when dividing into syllables.
text = rsub(text, "%.", SYLDIV)
-- Divide into syllables.
-- First remove '_', which has served its purpose of preventing context-dependent changes.
-- It should not interfere with syllabification.
text = text:gsub("_", "")
-- Also now convert ⁀ into a copy of the following consonant with the preceding space converted to ⁀
-- (which we will eventually convert to a tie symbol ‿, but for awhile we need to distinguish the two
-- because automatic syllabic gemination in final-stress words happens only in multisyllabic words,
-- and we don't want it to happen in monosyllabic words joined to a previous word by ⁀). We want to do
-- this after all consonants have been converted to IPA (so the correct consonant is geminated)
-- but before syllabification, since e.g. 'va* bène' should be treated as a single word 'va⁀b.bɛne' for
-- syllabification.
text = rsub(text, "# #⁀(‿?)(.)", "⁀%2%2")
-- Divide before the last consonant (possibly followed by a glide). We then move the syllable division marker
-- leftwards over clusters that can form onsets.
text = rsub_repeatedly(text, "(" .. V .. com.accent_c .. "*[‿⁀]?" .. C_OR_TIE .. "-)(" .. C .. W_OR_TIE .. "*" .. V .. ")", "%1.%2")
-- The previous regex divided VjjV as V.jjV but we want Vj.jV; same for VwwV. Correct this now.
text = rsub_repeatedly(text, "(" .. V .. com.accent_c .. "*[‿⁀]?)%.(" .. W .. ")([‿⁀]?)(%2[‿⁀]?" .. V .. ")", "%1%2.%3%4")
-- Existing hyphenations of [[atlante]], [[Betlemme]], [[genetliaco]], [[betlemita]] all divide as .tl,
-- and none divide as t.l. No examples of -dl- but it should be the same per
-- http://www.italianlanguageguide.com/pronunciation/syllabication.asp.
text = rsub(text, "([pbfvkgtd][‿⁀]?)%.([lr])", ".%1%2")
-- Several existing hyphenations divide .pn and .ps and Olivetti agrees. We do this after moving across s so that
-- dispnea is divided dis.pnea. Olivetti has tec.no.lo.gì.a for [[tecnologia]], showing that cn divides as c.n, and
-- clàc.son, fuc.sì.na, ric.siò for [[clacson]], [[fucsina]], [[ricsiò]], showing that cs divides as c.s.
text = rsub(text, "(p[‿⁀]?)%.([ns])", ".%1%2")
text = rsub_repeatedly(text, "(" .. V .. com.accent_c .. "*[‿⁀]?)(" .. V .. ")", "%1.%2")
-- User-specified syllable divider should now be treated like regular one.
text = rsub(text, SYLDIV, ".")
text = rsub(text, TEMP_H, "h")
-- Do the following after syllabification so we can distinguish written s from z, e.g. u.sbè.co but uz.bè.co per Olivetti.
-- Single ⟨s⟩ between vowels is /z/.
text = rsub_repeatedly(text, "(" .. VW .. com.stress_c .. "?" .. charsep_c .. "*)s(" .. charsep_c .. "*" .. VW .. ")", "%1z%2")
-- ⟨s⟩ immediately before a voiced consonant is always /z/
text = rsub(text, "s(" .. charsep_c .. "*" .. voiced_C_c .. ")", "z%1")
text = rsub(text, TEMP_Z, "z")
text = rsub(text, TEMP_S, "s")
-- French/German vowels
text = rsub(text, "ü", "y")
text = rsub(text, "ö", "ø")
text = rsub(text, "g", "ɡ") -- U+0261 LATIN SMALL LETTER SCRIPT G
local last_word_self_gemination = ufind(text, VW .. com.stress_c .. "?" .. charsep_c .. "*[ʦʣʃʎɲ]" .. com.stress_c .."*##$") and not
-- In case the user used t͡ʃ explicitly
ufind(text, "t͡ʃ" .. com.stress_c .."*##$")
local first_word_self_gemination = ufind(text, "^##" .. com.stress_c .. "*[ʦʣʃʎɲ]" .. charsep_c .. "*" .. VW) or
ufind(text, "^##" .. com.stress_c .. "*[ʦʣʃʎɲ]" .. com.stress_c .. "*" .. "##")
text = rsub(text, "([ʦʣʧʤ])(" .. charsep_c .. "*%.?)([ʦʣʧʤ]*)", function(affricate1, divider, affricate2)
local full_affricate = full_affricates[affricate1]
if affricate2 ~= "" then
return usub(full_affricate, 1, 1) .. divider .. full_affricate
end
return full_affricate .. divider
end)
local last_word_ends_in_primary_stressed_vowel = ufind(text, "ˈ##$")
-- Last word is multisyllabic if it has a syllable marker in it. This should not happen across word boundaries
-- (spaces) including ⁀, marking where two words were joined by syntactic gemination.
local last_word_is_multisyllabic = ufind(text, "%.[^ ⁀]*$")
local retval = {
orig_respelling = orig_respelling,
canon_respelling = canon_respelling,
-- Automatic co-gemination (syntactic gemination of the following consonant in a multisyllabic word ending in
-- a stressed vowel)
auto_cogemination = last_word_ends_in_primary_stressed_vowel and last_word_is_multisyllabic,
-- Last word ends in a vowel (an explicit * indicates co-gemination, i.e. syntactic gemination of the
-- following consonant)
last_word_ends_in_vowel = ufind(text, V .. com.stress_c .. "*" .. "##$"),
-- Last word ends in a consonant (an explicit * indicates self-gemination of this consonant, i.e. the
-- consonant doubles before a following vowel)
last_word_ends_in_consonant = ufind(text, C .. "##$"),
-- Last word ends in a consonant (ts dz ʃ ʎ ɲ) that triggers self-gemination before a following vowel
auto_final_self_gemination = last_word_self_gemination,
-- First word begins in a consonant (ts dz ʃ ʎ ɲ) that triggers self-gemination after a preceding vowel
auto_initial_self_gemination = first_word_self_gemination,
}
-- Now that ⁀ has served its purpose, convert to a regular tie ‿.
text = rsub(text, "⁀", "‿")
-- Stress marks.
-- Move IPA stress marks to the beginning of the syllable.
text = rsub_repeatedly(text, "([#.])([^#.]*)(" .. com.stress_c .. ")", "%1%3%2")
-- Suppress syllable mark before IPA stress indicator.
text = rsub(text, "%.(" .. com.stress_c .. ")", "%1")
-- Make all primary stresses but the last one in a given word be secondary. May be fed by the first rule above.
text = rsub_repeatedly(text, "ˈ([^ #]+)ˈ", "ˌ%1ˈ")
-- Remove # symbols at word/text boundaries and recompose.
text = rsub(text, "#", "")
text = toNFC(text)
retval.phonemic = text
return retval
end
-- For bot usage; {{#invoke:it-pronunciation|to_phonemic_bot|SPELLING}}
function export.to_phonemic_bot(frame)
local iparams = {
[1] = {},
}
local iargs = process_params(frame.args, iparams)
return export.to_phonemic(iargs[1], mw.loadData("Module:headword/data").pagename).phonemic
end
--[=[
Entry point to construct the arguments to a call to m_IPA.format_IPA_full() and make a call to this function.
This formats one line of pronunciation, potentially including multiple individual pronunciations (representing
differing pronunciations of the same underlying term), potentialy along with attached qualifiers and/or references.
`data` is a table currently containing two fields, as follows:
{
terms = {{term = RESPELLING, q = {QUALIFIER, QUALIFIER, ...}, refs = {REFSPEC, REFSPEC, ...}}, ...},
pagename = PAGENAME,
}
Here:
* RESPELLING is a pronunciation respelling of the term in question and may contain initial and/or final specs
indicating the presence, absence and nature of self-gemination and co-gemination along with initial specs
indicating the register of the pronunciation (traditional, careful style, elevated style).
* QUALIFIER is an arbitrary string to be displayed as a qualifier before the pronunciation in question; multiple
qualifiers will be comma-separated. `q` should always be given as a table even if it's empty.
* REFSPEC is a string of the same format as is passed to {{IPA}}; see the documentation of [[Template:IPA]] for more
info. `refs`, as with `q`, should always be given as a table even if it's empty.
* PAGENAME is the name of the page, used when an abbreviated spec like '^ò' is given.
The return value is an object with the following structure:
{
formatted = FORMATTED_IPA_LINE,
terms = {
{phonemic = PHONEMIC_PRON,
raw = "phonemic" or "phonetic",
auto_cogemination = BOOLEAN,
last_word_ends_in_vowel = BOOLEAN,
last_word_ends_in_consonant = BOOLEAN,
auto_final_self_gemination = BOOLEAN,
auto_initial_self_gemination = BOOLEAN,
orig_respelling = ORIG_RESPELLING,
canon_respelling = CANON_RESPELLING,
prespec = nil or PRESPEC,
pretext = nil or PRETEXT,
postspec = nil or POSTSPEC,
posttext = nil or POSTTEXT,
q = {QUALIFIER, QUALIFIER, ...},
refs = nil or {{text = TEXT, name = nil or NAME, group = nil or GROUP}, ...},
}, ...
}
}
Here:
* FORMATTED_IPA_LINE is the output of format_IPA_full(), a string.
* `terms` contains one entry per term in the input object to show_IPA_full().
* PHONEMIC_PRON is the phonemic IPA pronunciation of the respelling passed in, generated by to_phonemic().
* RAW is "phonemic" if the user specified a raw pronunciation using /.../, "phonetic" if using [...]. In such a case,
PHONEMIC_PRON is the user-specified pronunciation without the surrounding slashes or brackets.
* `last_word_ends_in_vowel` and the other boolean properties are documented in the source code of to_phonemic().
* ORIG_RESPELLING is the respelling for which the phonemic IPA pronunciation was generated. This is the same as was
passed in to show_IPA_full(), but stripped of initial and final spec symbols such as *, **, °, °°, #, !, !!.
* CANON_RESPELLING is the canonicalized, Unicode-decomposed version of ORIG_RESPELLING, as output by
canonicalize_and_auto_accent(). This includes expansion of abbreviations such as ^à, suffix application such as
-zione -> tsióne, and other auto-accenting.
* PRESPEC is a string containing the raw symbol(s) added before the phonemic output, currently one or *, **, ° or °°.
These may be explicitly specified by the user or added automatically based on the properties in the `phonemic`
structure described above.
* PRETEXT is the actual text added before the phonemic output, based on the prespec symbols annotated with HTML that
causes a tooltip to be displayed when the cursor hovers over the symbol.
* POSTSPEC is like PRESPEC but contains the raw symbol(s) added after the phonemic output rather than before.
* POSTTEXT is like PRETEXT but based on POSTSPEC.
* `q` contains the actual qualifiers passed to format_IPA_full() for this term. The qualifiers come from the
qualifiers passed in the per-term input and/or qualifiers added based on symbols preceding the respelling such as
#, ! or !!.
* `refs` is either nil or a list of objects describing parsed references, as returned by parse_references() in
[[Module:references]].
]=]
function export.show_IPA_full(data)
local final_cogemination = "triggers final cogemination (syntactic gemination of the initial consonant of the following word)"
local final_non_cogemination = "does not trigger final cogemination (syntactic gemination of the initial consonant of the following word)"
local final_self_cogemination = "triggers final self-gemination (syntactic gemination of the final consonant before a vowel)"
local final_non_self_cogemination = "does not trigger final self-gemination (syntactic gemination of the final consonant before a vowel)"
local initial_self_gemination = "triggers initial self-gemination (syntactic gemination of the initial consonant following a vowel)"
local initial_non_cogemination = "blocks initial cogemination (syntactic gemination of the initial consonant when it would normally occur, i.e. following a stressed final vowel)"
local initial_symbol_specs = {
{"**", "optionally " .. initial_self_gemination},
{"*", initial_self_gemination},
{"°°", "optionally " .. initial_non_cogemination},
{"°", initial_non_cogemination},
}
local final_vowel_symbol_specs = {
{"**", "optionally " .. final_cogemination},
{"*", final_cogemination},
{"°", final_non_cogemination},
}
local final_consonant_symbol_specs = {
{"**", "optionally " .. final_self_cogemination},
{"*", final_self_cogemination},
{"°", final_non_self_cogemination},
}
local retval = {terms = {}}
local transcriptions = {}
for _, term in ipairs(data.terms) do
local respelling = term.term
local q = term.q
-- Check if raw and strips the 'raw:' prefix.
local is_raw = umatch(respelling, "^raw:")
if is_raw then
respelling = usub(respelling, 5)
end
local prespec, actual_respelling, postspec = umatch(respelling, "^(" .. pron_sign_c .. "*)(.-)([*°]*)$")
respelling = actual_respelling
if prespec:find("!!") then
insert(q, 1, "elevated style")
prespec = prespec:gsub("!!", "")
end
if prespec:find("!") then
insert(q, 1, "careful style")
prespec = prespec:gsub("!", "")
end
if prespec:find("#") then
insert(q, 1, "traditional")
prespec = prespec:gsub("#", "")
end
local pron, pretext, posttext
if is_raw then
pron = export.to_phonetic(respelling)
else
pron = export.to_phonemic(respelling, data.pagename)
end
if prespec == "" and pron.auto_initial_self_gemination then
prespec = "*"
end
if postspec == "" and (pron.auto_cogemination or pron.auto_final_self_gemination) then
postspec = "*"
end
local function check_symbol_spec(spec, recognized_specs, is_pre)
for _, symbol_spec in ipairs(recognized_specs) do
local symbol, text = unpack(symbol_spec)
if symbol == spec then
local abbr = '<abbr title="' .. text .. '"><sup>' .. symbol .. "</sup></abbr>"
if is_pre then
pretext = abbr
else
posttext = abbr
end
return
end
end
error("Unrecognized " .. (is_pre and "initial" or "final") .. " symbol " .. spec)
end
if prespec ~= "" then
check_symbol_spec(prespec, initial_symbol_specs, true)
end
if postspec ~= "" then
if pron.last_word_ends_in_vowel then
check_symbol_spec(postspec, final_vowel_symbol_specs, false)
elseif pron.last_word_ends_in_consonant then
check_symbol_spec(postspec, final_consonant_symbol_specs, false)
else
error("Last word ends in neither vowel nor consonant; final symbol " .. postspec .. " not allowed here")
end
end
local refs
if not term.refs[1] then
refs = nil
elseif not term.refs[2] then
refs = parse_references(term.refs[1])
else
refs = {}
for _, refspec in ipairs(term.refs) do
local this_refs = parse_references(refspec)
for _, this_ref in ipairs(this_refs) do
insert(refs, this_ref)
end
end
end
pron.prespec = prespec
pron.pretext = pretext
pron.postspec = postspec
pron.posttext = posttext
pron.q = q
pron.refs = refs
insert(retval.terms, pron)
insert(transcriptions, {
pron = pron.raw == "phonetic" and "[" .. pron.phonemic .. "]" or "/" .. pron.phonemic .. "/",
q = q[1] and q or nil,
pretext = pretext,
posttext = posttext,
refs = refs,
})
end
retval.formatted = format_IPA_full{ lang = lang, items = transcriptions }
return retval
end
-- External entry point for {{it-IPA}}.
function export.show(frame)
local args = process_params(
frame:getParent().args,
{
-- terms to transcribe
[1] = { list = true },
-- FIXME: Support left/right accent qualifiers and regular qualifiers
["qual"] = { list = true, allow_holes = true },
["ref"] = { list = true, allow_holes = true },
["pagename"] = {}, -- for testing
})
local pagename = args.pagename or mw.loadData("Module:headword/data").pagename
local respellings = args[1]
if #respellings == 0 then
respellings = {pagename}
end
local data = {terms = {}, pagename = pagename}
for i, respelling in ipairs(respellings) do
insert(data.terms, {term = respelling, q = {args.qual[i]}, refs = {args.ref[i]}})
end
local retval_IPA_full = export.show_IPA_full(data)
return retval_IPA_full.formatted
end
-- Return the number of syllables of a phonemic representation, which should have syllable dividers in it but no
-- hyphens.
local function get_num_syl_from_phonemic(phonemic)
-- Maybe we should just count vowels instead of the below code.
phonemic = rsub(phonemic, "|", " ") -- remove IPA foot boundaries
local words
if not phonemic:find(" ") then
words = {phonemic}
else
words = split(phonemic, "( +)")
end
for i, word in ipairs(words) do
if (i % 2) == 1 then -- an actual word, not a separator
-- IPA stress marks are syllable divisions if between characters; otherwise just remove.
word = rsub(word, "(.)[ˌˈ](.)", "%1.%2")
word = rsub(word, "[ˌˈ]", "")
words[i] = word
else
-- Convert spaces and word-separating hyphens into syllable divisions.
words[i] = "."
end
end
phonemic = concat(words)
return ulen(rsub(phonemic, "[^.]", "")) + 1
end
-- Given the output structure from show_IPA_full, generate a list of rhyme objects. The resulting list can be directly
-- passed in as the `rhymes` field of the data object passed into format_rhymes() in [[Module:rhymes]].
local function generate_rhymes_from_phonemic_output(ipa_full_output, always_rhyme)
local rhymes = {}
for _, termobj in ipairs(ipa_full_output.terms) do
local pronun = termobj.phonemic
local no_rhyme
if always_rhyme then
if termobj.raw == "phonetic" then
error("Can't generate rhyme for raw phonetic output")
else
no_rhyme = false
end
else
local words = com.split_but_rejoin_affixes(termobj.canon_respelling)
-- Figure out if we should not generate a rhyme for this term.
no_rhyme =
termobj.raw -- raw phonemic or phonetic output given
or #words > 1 -- more than one word
or words[1]:find("%-$") -- a prefix
or words[1]:find("^%-") and ( -- an unstressed suffix:
-- (1): com.DOTOVER explicitly indicates unstressed suffix
words[1]:find(com.DOTOVER)
-- (2): com.DOTUNDER directly after acute or grave indicates unstressed vowel; after discounting
-- such vowels, we see no stressed words.
or not ufind(rsub(words[1], com.quality_c .. com.DOTUNDER, ""), com.quality_c)
)
end
if not no_rhyme then
local rhyme_pronun = rsub(rsub(pronun, ".*[ˌˈ]", ""), "^[^aeiouɛɔ]*", ""):gsub(com.TIE, ""):gsub("%.", "")
if rhyme_pronun ~= "" then
local nsyl = get_num_syl_from_phonemic(pronun)
local saw_rhyme = false
for _, rhyme in ipairs(rhymes) do
if rhyme.rhyme == rhyme_pronun then
-- already saw rhyme
local saw_nsyl = false
for _, this_nsyl in ipairs(rhyme.num_syl) do
if this_nsyl == nsyl then
saw_nsyl = true
break
end
end
if not saw_nsyl then
insert(rhyme.num_syl, nsyl)
end
saw_rhyme = true
break
end
end
if not saw_rhyme then
-- Only show pronunciation qualifiers with rhymes if there's more than one pronunciation given.
-- Otherwise the qualifier is just redundant since it's already specified before the pronunciation
-- and doesn't serve to distinguish rhymes. Also, only set the qualifier for new rhymes; don't
-- combine qualifiers from different pronunciations with the same rhyme. For example, for
-- {{it-pr|scandìnavo,#!scandinàvo}} we want to see
--
-- Rhymes: -inavo, ''(traditional, careful style)'' -avo
--
-- but for {{it-pr|quàlche°,#quàlche*}} we do *NOT* want to see ''(traditional)'' by the rhyme -alke,
-- which would be incorrect as the same rhyme applies to both pronunciations and only one of the
-- pronunciations is traditional.
local q = ipa_full_output.terms[2] and termobj.q and termobj.q[1] and termobj.q or nil
insert(rhymes, {rhyme = rhyme_pronun, q = q, num_syl = {nsyl}})
end
end
end
end
return rhymes
end
-- Syllabify a single word based on its spelling. The text should be decomposed using com.decompose() and have extraneous
-- characters (e.g. initial or final *) removed.
local function syllabify_word_from_spelling(text)
-- NOTE: In all of the following, we have to be careful to allow for apostrophes between letters and for capital
-- letters in the middle of words, as in [[anch'io]], [[all'osso]], [[altr'ieri]], [[cardellino dell'Himalaya]],
-- [[UEFA]], etc.
local TEMP_I = u(0xFFF2)
local TEMP_I_CAPS = u(0xFFF3)
local TEMP_U = u(0xFFF4)
local TEMP_U_CAPS = u(0xFFF5)
local TEMP_Y = u(0xFFF6)
local TEMP_Y_CAPS = u(0xFFF7)
local TEMP_G = u(0xFFF8)
local TEMP_G_CAPS = u(0xFFF9)
-- Change user-specified . into SYLDIV so we don't shuffle it around when dividing into syllables.
text = text:gsub("%.", SYLDIV)
-- We propagate underscore this far specifically so we can distinguish g_n ([[wagneriano]]) from gn.
-- g_n should end up as g.n but gn should end up as .gn.
local g_to_temp_g = {["g"] = TEMP_G, ["G"] = TEMP_G_CAPS}
text = rsub(text, "([gG])('?)_('?[nN])", function (g, sep, n) return g_to_temp_g[g] .. sep .. n end)
-- Now remove underscores before any further processing.
text = text:gsub("_", "")
-- i, u, y between vowels -> consonant-like substitutions:
-- With i: [[paranoia]], [[febbraio]], [[abbaiare]], [[aiutare]], etc.
-- With u: [[portauovo]], [[schopenhaueriano]], [[Malaui]], [[oltreuomo]], [[palauano]], [[tauone]], etc.
-- With y: [[ayatollah]], [[coyote]], [[hathayoga]], [[kayak]], [[uruguayano]], etc. [[kefiyyah]] needs special
-- handling.
-- Also with h, as in [[nahuatl]], [[ahia]], etc.
-- With h not dividing diphthongs: [[ahi]], [[ehi]], [[ahimè]], [[ehilà]], [[ohimè]], [[ohilà]], etc.
-- But in the common sequence -Ciuo- ([[figliuolo]], [[begliuomini]], [[giuoco]], [[nocciuola]], [[stacciuolo]],
-- [[oriuolo]], [[guerricciuola]], [[ghiaggiuolo]], etc.), both i and u are glides. In the sequence -quiV-
-- ([[quieto]], [[reliquia]], etc.), both u and i are glides, and probably also in -guiV-, but not in other -CuiV-
-- sequences such as [[buio]], [[abbuiamento]], [[gianduia]], [[cuiusso]], [[alleluia]], etc.). Special cases are
-- French-origin words like [[feuilleton]], [[rousseauiano]], [[gargouille]]; it's unlikely we can handle these
-- correctly automatically.
--
-- We handle these cases as follows:
-- 1. q+TEMP_U etc. replace sequences of qu and gu with consonant-type codes. This allows us to distinguish
-- -quiV-/-guiV- from other -CuiV-.
-- 2. We convert i in -ViV- sequences to consonant-type TEMP_I, but similarly for u in -VuV- sequences only if the
-- first V isn't i, so -CiuV- remains with two vowels. The syllabification algorithm below will not divide iu
-- or uV unless in each case the first vowel is stressed, so -CiuV- remains in a single syllable.
-- 3. As soon as we convert i to TEMP_I, we undo the u -> TEMP_U change for -quiV-/-guiV-, before u -> TEMP_U in
-- -VuV- sequences.
local u_to_temp_u = {["u"] = TEMP_U, ["U"] = TEMP_U_CAPS}
text = rsub(text, "([qQgG])([uU])('?" .. V .. ")", function(qg, u, v) return qg .. u_to_temp_u[u] .. v end)
local i_to_temp_i = {["i"] = TEMP_I, ["I"] = TEMP_I_CAPS, ["y"] = TEMP_Y, ["Y"] = TEMP_Y_CAPS}
text = rsub_repeatedly(text, "(" .. V .. com.accent_c .. "*[hH]?)([iIyY])(" .. V .. ")",
function(v1, iy, v2) return v1 .. i_to_temp_i[iy] .. v2 end)
text = text:gsub(TEMP_U, "u")
text = text:gsub(TEMP_U_CAPS, "U")
text = rsub_repeatedly(text, "(" .. V_NOT_I .. com.accent_c .. "*[hH]?)([uU])(" .. V .. ")",
function(v1, u, v2) return v1 .. u_to_temp_u[u] .. v2 end)
-- Divide VCV as V.CV; but don't divide if C == h, e.g. [[ahimè]] should be ahi.mè.
text = rsub_repeatedly(text, "(" .. V .. com.accent_c .. "*'?)(" .. C_NOT_H .. "'?" .. V .. ")", "%1.%2")
text = rsub_repeatedly(text, "(" .. V .. com.accent_c .. "*'?" .. C .. C_OR_TIE .. "*)(" .. C .. "'?" .. V .. ")", "%1.%2")
-- Examples in Olivetti like [[hathayoga]], [[telethon]], [[cellophane]], [[skyphos]], [[piranha]], [[bilharziosi]]
-- divide as .Ch. Exceptions are [[wahhabismo]], [[amharico]], [[kinderheim]], [[schopenhaueriano]] but the latter
-- three seem questionable as the pronunciation puts the first consonant in the following syllable and makes the h
-- silent.
text = rsub(text, "(" .. C_NOT_H .. "'?)%.([hH])", ".%1%2")
-- gn represents a single sound so it should not be divided.
text = rsub(text, "([gG])%.([nN])", ".%1%2")
-- Existing hyphenations of [[atlante]], [[Betlemme]], [[genetliaco]], [[betlemita]] all divide as .tl,
-- and none divide as t.l. No examples of -dl- but it should be the same per
-- http://www.italianlanguageguide.com/pronunciation/syllabication.asp.
text = rsub(text, "([pbfvkcgqtdPBFVKCGQTD]'?)%.([lrLR])", ".%1%2")
-- Italian appears to divide sCV as .sCV e.g. pé.sca for [[pesca]], and similarly for sCh, sCl, sCr. Exceptions are
-- ss, sr, sz and possibly others. We are careful not to move across s in [[massmediale]], [[password]], etc.
text = rsub(text, "([^sS])([sS]'?)%.(" .. C_NOT_SRZ .. ")", "%1.%2%3")
-- Several existing hyphenations divide .pn and .ps and Olivetti agrees. We do this after moving across s so that
-- dispnea is divided dis.pnea. We are careful not to move across p in [[Uppsala]]. Olivetti has tec.no.lo.gì.a for
-- [[tecnologia]], showing that cn divides as c.n, and clàc.son, fuc.sì.na, ric.siò for [[clacson]], [[fucsina]],
-- [[ricsiò]], showing that cs divides as c.s.
text = rsub(text, "([^pP])([pP]'?)%.([nsNS])", "%1.%2%3")
-- Any aeoö, or stressed iuüy, should be syllabically divided from a following aeoö or stressed iuüy.
-- A stressed vowel might be followed by another accent such as com.LINEUNDER (which we put after the acute/grave in
-- com.decompose()).
text = rsub_repeatedly(text, "([aeoöAEOÖ]" .. com.accent_c .. "*'?)([hH]?'?[aeoöAEOÖ])", "%1.%2")
text = rsub_repeatedly(text, "([aeoöAEOÖ]" .. com.accent_c .. "*'?)([hH]?'?" .. V .. com.quality_c .. ")", "%1.%2")
text = rsub(text, "([iuüyIUÜY]" .. com.quality_c .. com.accent_c .. "*'?)([hH]?'?[aeoöAEOÖ])", "%1.%2")
text = rsub_repeatedly(text, "([iuüyIUÜY]" .. com.quality_c .. com.accent_c .. "*'?)([hH]?'?" .. V .. com.quality_c .. ")", "%1.%2")
-- We divide ii as i.i ([[sii]]), but not iy or yi, which should hopefully cause [[kefiyyah]] to be handled
-- correctly as ke.fiy.yah. Only example with Cyi is [[dandyismo]], which may be exceptional.
text = rsub_repeatedly(text, "([iI]" .. com.accent_c .. "*'?)([hH]?'?[iI])", "%1.%2")
text = rsub_repeatedly(text, "([uüUÜ]" .. com.accent_c .. "*'?)([hH]?'?[uüUÜ])", "%1.%2")
text = text:gsub(SYLDIV, ".")
text = text:gsub(TEMP_I, "i")
text = text:gsub(TEMP_I_CAPS, "I")
text = text:gsub(TEMP_U, "u")
text = text:gsub(TEMP_U_CAPS, "U")
text = text:gsub(TEMP_Y, "y")
text = text:gsub(TEMP_Y_CAPS, "Y")
text = text:gsub(TEMP_G, "g")
text = text:gsub(TEMP_G_CAPS, "G")
return text
end
-- Syllabify text based on its spelling. The text should be decomposed using com.decompose() and have extraneous
-- characters (e.g. initial *) removed.
local function syllabify_from_spelling(text)
-- Convert spaces and word-separating hyphens into syllable divisions.
local words = com.split_but_rejoin_affixes(text)
for i, word in ipairs(words) do
if (i % 2) == 0 then -- a separator
words[i] = WORDDIV
else
words[i] = syllabify_word_from_spelling(word)
end
end
text = concat(words)
-- Convert word divisions into periods, but first into spaces so we can call com.remove_secondary_stress().
-- We have to call com.remove_secondary_stress() after syllabification so we correctly syllabify words like
-- bìobibliografìa.
text = text:gsub(WORDDIV, " ")
text = com.remove_secondary_stress(text)
text = text:gsub(" ", ".")
return text
end
-- Given the canon_respelling field in the structure output by show_IPA_full(), normalize it into the form that can
-- (a) be passed to syllabify_from_spelling() to produce the syllabification that is used to generate hyphenation
-- output, (b) be further processed to determine whether to generate hyphenation at all (by comparing the
-- further-processed result to the original pagename). NOTE: canon_respelling must be decomposed using com.decompose().
local function normalize_for_syllabification(respelling)
-- Remove IPA foot boundaries.
respelling = respelling:gsub("|", " ")
respelling = canon_spaces(respelling)
-- Convert respelling conventions back to the original spelling.
respelling = convert_respelling_to_original(respelling)
return respelling
end
-- Given the output of normalize_for_syllabification() (which should be decomposed using com.decompose()), see if it
-- matches the page name. If so, we auto-generate hyphenation output based on the respelling.
local function spelling_normalized_for_syllabification_matches_pagename(text, pagename)
pagename = com.decompose(pagename)
text = com.remove_secondary_stress(text)
text = text:gsub("_", "")
if text == pagename then
return true
end
text = text:gsub("%.", "")
if text == pagename then -- e.g. [[Abraàm]], [[piùe]] with non-final accent in the page name
return true
end
text = com.remove_non_final_accents(text)
-- Check if the normalized pronunciation is the same as the page name. If a word in the page name is a single
-- syllable, it may or may not have an accent on it, so also remove final monosyllabic accents from the normalized
-- pronunciation when comparing. (Don't remove from both normalized pronunciation and page name because we don't
-- want pronunciation rè to match page name ré or vice versa.)
return text == pagename or remove_final_monosyllabic_accents(text) == pagename
end
-- Given the output structure from show_IPA_full, generate a list of hyphenation objects. The resulting list can be
-- directly passed in as the `hyphs` field of the data object passed into format_hyphenation() in
-- [[Module:hyphenation]].
local function generate_hyphenation_from_phonemic_output(ipa_full_output, pagename)
local hyphs = {}
for _, termobj in ipairs(ipa_full_output.terms) do
local normtext
-- Figure out if we should not generate a hyphenation for this term.
local no_hyph
if termobj.raw then
no_hyph = true
else
normtext = normalize_for_syllabification(termobj.canon_respelling)
no_hyph = not all_words_have_vowels(normtext)
or not spelling_normalized_for_syllabification_matches_pagename(normtext, pagename)
end
if not no_hyph then
local syllabification = syllabify_from_spelling(normtext)
local saw_hyph = false
for _, hyph in ipairs(hyphs) do
if hyph.syllabification == syllabification then
-- already saw hyphenation
saw_hyph = true
break
end
end
if not saw_hyph then
-- Only show pronunciation qualifiers with hyphenations if there's more than one pronunciation given,
-- and only set the qualifier for new hyphenations. See generate_rhymes_from_phonemic_output().
local q = #ipa_full_output.terms > 1 and termobj.q and termobj.q[1] and termobj.q or nil
insert(hyphs, {syllabification = syllabification, hyph = split(syllabification, ".", true, true),
q = q})
end
end
end
return hyphs
end
local function parse_rhyme(arg, parse_err)
-- FIXME, use parse_inline_modifiers() and support q/qq/a/aa
local retval = {}
local rhyme_segments = parse_balanced_segment_run(arg, "<", ">")
local comma_separated_groups = split_alternating_runs(rhyme_segments, "%s*,%s*")
for _, group in ipairs(comma_separated_groups) do
local rhyme_obj = {rhyme = group[1]}
for j = 2, #group - 1, 2 do
if group[j + 1] ~= "" then
parse_err("Extraneous text '" .. group[j + 1] .. "' after modifier")
end
local modtext = group[j]:match("^<(.*)>$")
if not modtext then
parse_err("Internal error: Rhyme modifier '" .. group[j] .. "' isn't surrounded by angle brackets")
end
local prefix, arg = modtext:match("^([a-z]+):(.*)$")
if not prefix then
parse_err("Modifier " .. group[j] .. " lacks a prefix, should begin with one of 's:' or 'qual:'")
end
if prefix == "s" then
local nsyls = split(arg, "%s*,%s*")
for _, nsyl in ipairs(nsyls) do
if not nsyl:find("^[0-9]+$") then
parse_err("Number of syllables '" .. nsyl .. "' in rhyme " ..
concat(group) .. " should be numeric")
end
if not rhyme_obj.num_syl then
rhyme_obj.num_syl = {}
end
insert(rhyme_obj.num_syl, tonumber(nsyl))
end
elseif prefix == "qual" then
if not rhyme_obj.q then
rhyme_obj.q = {}
end
insert(rhyme_obj.q, arg)
else
parse_err("Unrecognized prefix '" .. prefix .. "' in modifier " .. group[j]
.. ", should be 's' or 'qual'")
end
end
insert(retval, rhyme_obj)
end
return retval
end
local function parse_hyph(arg, parse_err)
-- FIXME, use parse_inline_modifiers() and support q/qq/a/aa
local retval = {}
local hyph_segments = parse_balanced_segment_run(arg, "<", ">")
local comma_separated_groups = split_alternating_runs(hyph_segments, "%s*,%s*")
for _, group in ipairs(comma_separated_groups) do
local hyph_obj = {syllabification = group[1], hyph = split(group[1], ".", true, true)}
for j = 2, #group - 1, 2 do
if group[j + 1] ~= "" then
parse_err("Extraneous text '" .. group[j + 1] .. "' after modifier")
end
local modtext = group[j]:match("^<(.*)>$")
if not modtext then
parse_err("Internal error: Modifier '" .. group[j] .. "' isn't surrounded by angle brackets")
end
local prefix, arg = modtext:match("^([a-z]+):(.*)$")
if not prefix then
parse_err("Modifier " .. group[j] .. " lacks a prefix, should begin with 'qual:'")
end
if prefix == "qual" then
if not hyph_obj.q then
hyph_obj.q = {}
end
insert(hyph_obj.q, arg)
else
parse_err("Unrecognized prefix '" .. prefix .. "' in modifier " .. group[j] .. ", should be 'qual'")
end
end
insert(retval, hyph_obj)
end
return retval
end
local function parse_homophone(arg, parse_err)
-- FIXME, use parse_inline_modifiers() and support q/qq/a/aa
local retval = {}
local hmp_segments = parse_balanced_segment_run(arg, "<", ">")
local comma_separated_groups = split_alternating_runs(hmp_segments, "%s*,%s*")
for _, group in ipairs(comma_separated_groups) do
local hmp_obj = {term = group[1]}
for j = 2, #group - 1, 2 do
if group[j + 1] ~= "" then
parse_err("Extraneous text '" .. group[j + 1] .. "' after modifier")
end
local modtext = group[j]:match("^<(.*)>$")
if not modtext then
parse_err("Internal error: Modifier '" .. group[j] .. "' isn't surrounded by angle brackets")
end
local prefix, arg = modtext:match("^([a-z]+):(.*)$")
if not prefix then
parse_err("Modifier " .. group[j] .. " lacks a prefix, should begin with one of 'qual:', 't:', 'alt:' or 'pos:'")
end
if prefix == "qual" then
if not hmp_obj.qq then
hmp_obj.qq = {}
end
insert(hmp_obj.qq, arg)
elseif prefix == "t" or prefix == "alt" or prefix == "pos" then
local key = prefix == "t" and "gloss" or prefix
if hmp_obj[key] then
parse_err("Modifier '" .. prefix .. "' specified more than once")
end
hmp_obj[key] = arg
else
parse_err("Unrecognized prefix '" .. prefix .. "' in modifier " .. group[j]
.. ", should be 'qual', 't', 'alt' or 'pos'")
end
end
insert(retval, hmp_obj)
end
return retval
end
-- External entry point for {{it-pr}}.
function export.show_pr(frame)
local params = {
[1] = {list = true},
["pagename"] = {}, -- for testing
}
local parargs = frame:getParent().args
local args = process_params(parargs, params)
local pagename = args.pagename or mw.loadData("Module:headword/data").pagename
-- Parse the arguments.
local respellings = #args[1] > 0 and args[1] or {"+"}
local parsed_respellings = {}
for i, respelling in ipairs(respellings) do
if respelling:find("[<%[]") then
local function parse_err(msg)
error(msg .. ": " .. i .. "= " .. respelling)
end
-- We don't want to split off a comma followed by a space, as in [[uomo avvisato, mezzo salvato]], so replace
-- comma+space with a special character that we later undo.
respelling = respelling:gsub(", ", TEMP1)
-- Parse balanced segment runs involving either [...] (substitution notation) or <...> (inline modifiers). We do this
-- because we don't want commas inside of square or angle brackets to count as respelling delimiters. However, we
-- need to rejoin square-bracketed segments with nearby ones after splitting alternating runs on comma. For example,
-- if we are given "a[x]a<q:learned>,[tts,ìo]<q:nonstandard>", after calling
-- parse_multi_delimiter_balanced_segment_run() we get the following output:
--
-- {"a", "[x]", "a", "<q:learned>", ",", "[tts,ìo]", "", "<q:nonstandard>", ""}
--
-- After calling split_alternating_runs(), we get the following:
--
-- {{"a", "[x]", "a", "<q:learned>", ""}, {"", "[tts,ìo]", "", "<q:nonstandard>", ""}}
--
-- We need to rejoin stuff on either side of the square-bracketed portions.
local segments = parse_multi_delimiter_balanced_segment_run(respelling, {{"<", ">"}, {"[", "]"}})
-- Not with spaces around the comma; see above for why we don't want to split off comma followed by space.
local comma_separated_groups = split_alternating_runs(segments, ",")
local parsed = {terms = {}, audio = {}}
for i, group in ipairs(comma_separated_groups) do
-- Rejoin bracketed segments with nearby ones, as described above.
local j = 2
while j <= #group do
if group[j]:find("^%[") then
group[j - 1] = group[j - 1] .. group[j] .. group[j + 1]
remove(group, j)
remove(group, j)
else
j = j + 2
end
end
-- Undo replacement of comma+space.
for k, segment in ipairs(group) do
group[k] = segment:gsub(TEMP1, ", ")
end
local term = {term = group[1], refs = {}, q = {}}
-- FIXME, use parse_inline_modifiers() and support q/qq/a/aa
for j = 2, #group - 1, 2 do
if group[j + 1] ~= "" then
parse_err("Extraneous text '" .. group[j + 1] .. "' after modifier")
end
local modtext = group[j]:match("^<(.*)>$")
if not modtext then
parse_err("Internal error: Modifier '" .. group[j] .. "' isn't surrounded by angle brackets")
end
local prefix, arg = modtext:match("^([a-z]+):(.*)$")
if not prefix then
parse_err("Modifier " .. group[j] .. " lacks a prefix, should begin with one of " ..
"'pre:', 'post:', 'ref:', 'r:', 'bullets:', 'audio:', 'rhyme:', 'hyph:', 'hmp:' or 'qual:'")
end
if prefix == "ref" then
insert(term.refs, arg)
elseif prefix == "qual" then
insert(term.q, arg)
elseif prefix == "r" then
insert(term.refs, com.parse_abbreviated_references_spec(arg))
elseif prefix == "pre" or prefix == "post" or prefix == "bullets" or prefix == "rhyme"
or prefix == "hyph" or prefix == "hmp" or prefix == "audio" then
if i < #comma_separated_groups then
parse_err("Modifier '" .. prefix .. "' should occur after the last comma-separated term")
end
if prefix == "rhyme" then
local parsed_rhymes = parse_rhyme(arg, parse_err)
if not parsed.rhyme then
parsed.rhyme = parsed_rhymes
else
for _, parsed_rhyme in ipairs(parsed_rhymes) do
insert(parsed.rhyme, parsed_rhyme)
end
end
elseif prefix == "hyph" then
local parsed_hyphs = parse_hyph(arg, parse_err)
if not parsed.hyph then
parsed.hyph = parsed_hyphs
else
for _, parsed_hyph in ipairs(parsed_hyphs) do
insert(parsed.hyph, parsed_hyph)
end
end
elseif prefix == "hmp" then
local parsed_homophones = parse_homophone(arg, parse_err)
if not parsed.hmp then
parsed.hmp = parsed_homophones
else
for _, parsed_homophone in ipairs(parsed_homophones) do
insert(parsed.hmp, parsed_homophone)
end
end
elseif prefix == "audio" then
local file, gloss = arg:match("^(.-)%s*;%s*(.*)$")
file = file or arg
insert(parsed.audio, {file = file, gloss = gloss})
else
if parsed[prefix] then
parse_err("Modifier '" .. prefix .. "' occurs twice, second occurrence " .. group[j])
end
if prefix == "bullets" then
if not arg:find("^[0-9]+$") then
parse_err("Modifier 'bullets' should have a number as argument")
end
parsed.bullets = tonumber(arg)
else
parsed[prefix] = arg
end
end
else
parse_err("Unrecognized prefix '" .. prefix .. "' in modifier " .. group[j]
.. ", should be one of 'pre', 'post', 'ref', 'r', 'bullets', 'audio', 'rhyme', 'hyph', 'hmp'"
.. " or 'qual'")
end
end
insert(parsed.terms, term)
end
if not parsed.bullets then
parsed.bullets = 1
end
insert(parsed_respellings, parsed)
else
local terms = {}
-- We don't want to split on comma+space, which should become a foot boundary as in
-- [[uomo avvisato, mezzo salvato]].
local subbed_respelling = respelling:gsub(", ", TEMP1)
for _, term in ipairs(split(subbed_respelling, ",", true)) do
term = term:gsub(TEMP1, ", ")
insert(terms, {term = term, refs = {}, q = {}})
end
insert(parsed_respellings, {
terms = terms,
audio = {},
bullets = 1,
})
end
end
-- Loop over individual respellings, processing each.
for _, parsed in ipairs(parsed_respellings) do
-- Generate the phonemic pronunciation.
parsed.ipa_full = export.show_IPA_full {terms = parsed.terms, pagename = pagename}
-- Generate the rhymes.
local rhymes = {}
if not parsed.rhyme or #parsed.rhyme == 0 then
rhymes = generate_rhymes_from_phonemic_output(parsed.ipa_full)
else
for _, rhyme in ipairs(parsed.rhyme) do
if rhyme.rhyme == "-" then
rhymes = {}
break
elseif rhyme.rhyme == "+" then
local auto_rhymes = generate_rhymes_from_phonemic_output(parsed.ipa_full, "always rhyme")
for _, auto_rhyme in ipairs(auto_rhymes) do
insert(rhymes, auto_rhyme)
end
else
-- If user specified the rhyme explicitly but not the number of syllables, get this from the
-- phonemic representation of the terms.
if not rhyme.num_syl then
rhyme.num_syl = {}
for _, term in ipairs(parsed.ipa_full.terms) do
local nsyl = get_num_syl_from_phonemic(term.phonemic)
insert_if_not(rhyme.num_syl, nsyl)
end
end
insert(rhymes, rhyme)
end
end
end
parsed.rhyme = rhymes
-- Generate the hyphenations.
local hyphs = {}
if not parsed.hyph or #parsed.hyph == 0 then
hyphs = generate_hyphenation_from_phonemic_output(parsed.ipa_full, pagename)
else
for _, hyph in ipairs(parsed.hyph) do
if hyph.syllabification == "-" then
hyphs = {}
break
else
insert_if_not(hyphs, hyph)
end
end
end
parsed.hyph = hyphs
-- Generate the homophones.
parsed.hmp = parsed.hmp or {}
end
-- If all sets of pronunciations have the same rhymes, display them only once at the bottom.
-- Otherwise, display rhymes beneath each set, indented.
local first_rhyme_ret
local all_rhyme_sets_eq = true
for j, parsed in ipairs(parsed_respellings) do
if j == 1 then
first_rhyme_ret = parsed.rhyme
elseif not deep_equals(first_rhyme_ret, parsed.rhyme) then
all_rhyme_sets_eq = false
break
end
end
-- If all sets of pronunciations have the same hyphenations, display them only once at the bottom.
-- Otherwise, display hyphenations beneath each set, indented.
local first_hyphs
local all_hyph_sets_eq = true
for j, parsed in ipairs(parsed_respellings) do
if j == 1 then
first_hyphs = parsed.hyph
elseif not deep_equals(first_hyphs, parsed.hyph) then
all_hyph_sets_eq = false
break
end
end
local function _format_phonemic(parsed)
local pre = parsed.pre and parsed.pre .. " " or ""
local post = parsed.post and " " .. parsed.post or ""
return rep("*", parsed.bullets) .. pre .. parsed.ipa_full.formatted .. post
end
local function _format_rhymes(rhymes, num_bullets)
local rhymetext = format_rhymes{ lang = lang, rhymes = rhymes, force_cat = force_cat }
return rep("*", num_bullets) .. " " .. rhymetext
end
local function _format_hyphenations(hyphs, num_bullets)
local hyphtext = format_hyphenations{ lang = lang, hyphs = hyphs }
return rep("*", num_bullets) .. " " .. hyphtext
end
local function _format_homophones(hmps, num_bullets)
local hmptext = format_homophones{ lang = lang, homophones = hmps }
return rep("*", num_bullets) .. " " .. hmptext
end
local function _format_audio(audios, num_bullets)
local ret = {}
for _, audio in ipairs(audios) do
insert(ret, rep("*", num_bullets) .. " " .. format_audio{
lang = lang, file = audio.file, caption = audio.gloss
})
end
return concat(ret, "\n")
end
local textparts = {}
local min_num_bullets = math.huge
for j, parsed in ipairs(parsed_respellings) do
if parsed.bullets < min_num_bullets then
min_num_bullets = parsed.bullets
end
if j > 1 then
insert(textparts, "\n")
end
insert(textparts, _format_phonemic(parsed))
if #parsed.audio > 0 then
insert(textparts, "\n")
-- If only one pronunciation set, add the audio with the same number of bullets, otherwise
-- indent audio by one more bullet.
insert(textparts, _format_audio(parsed.audio,
#parsed_respellings == 1 and parsed.bullets or parsed.bullets + 1))
end
if not all_rhyme_sets_eq and #parsed.rhyme > 0 then
insert(textparts, "\n")
insert(textparts, _format_rhymes(parsed.rhyme, parsed.bullets + 1))
end
if not all_hyph_sets_eq and #parsed.hyph > 0 then
insert(textparts, "\n")
insert(textparts, _format_hyphenations(parsed.hyph, parsed.bullets + 1))
end
if #parsed.hmp > 0 then
insert(textparts, "\n")
-- If only one pronunciation set, add the homophones with the same number of bullets, otherwise
-- indent homophones by one more bullet.
insert(textparts, _format_homophones(parsed.hmp,
#parsed_respellings == 1 and parsed.bullets or parsed.bullets + 1))
end
end
if all_rhyme_sets_eq and #first_rhyme_ret > 0 then
insert(textparts, "\n")
insert(textparts, _format_rhymes(first_rhyme_ret, min_num_bullets))
end
if all_hyph_sets_eq and #first_hyphs > 0 then
insert(textparts, "\n")
insert(textparts, _format_hyphenations(first_hyphs, min_num_bullets))
end
return concat(textparts)
end
return export
oqmyzod5lr9r13yasiawxj3f3pr31r1
Nik
0
50740
375816
339425
2026-09-24T23:38:29Z
InternetArchiveBot
5017
Rescuing 1 sources and tagging 0 as dead.) #IABot (v2.0.9.6
375816
wikitext
text/x-wiki
==Bahasa Melayu==
===Kata nama khas===
{{ms-knk|j=نيء}}
# nama gelaran kebiasaannya wujud dalam masyarakat Melayu Kelantan
===Rujukan===
* [https://web.archive.org/web/20240429112329/http://psasir.upm.edu.my/id/eprint/78588/8/Gelaran%20warisan%20.pdf Gelaran warisan Kelantan: citra unik zaman-berzaman]
caay481zfl6onoetascve9yx1b5s3pj
pitulas
0
52267
375821
285624
2026-09-25T02:50:43Z
Hakimi97
2668
Tambah kod bahasa Jawa
375821
wikitext
text/x-wiki
==Bahasa Jawa==
===Kata nama===
{{inti|jv|kata nama}}
# [[tujuh belas]]
dij2nqq1b0kenwh7nf2l9yfzwmp9eyw
Modul:translations/multi-nowiki/doc
828
57637
375822
221110
2026-09-25T02:55:46Z
Hakimi97
2668
Buang bahasa Melayu
375822
wikitext
text/x-wiki
{{#invoke:translations/multi-nowiki|expand|
<nowiki>
* Nested templates: {{t|cmn|大老二|tr={{m|cmn|dà lǎo èr}}}}
* Piped link: {{t|ja|{{...}}から[[見る|見]]た|lit=seen from {{...}}|tr={{...}} kara mita}}
* Substed module invocation:
** {{subst:#invoke:languages/templates|getByCode|csb|getCanonicalName}}: {{t|csb|czerwiony}}
{{trans-top|sukar, tak senang}}
* Afrikaans: {{t|af|moeilik}}
* Albanian: {{t+|sq|vështirë|alt=i vështirë}}
* Amharic: {{t|am|አስቸጋሪ}}
* Arabic: {{t|ar|صَعْب}}
*: Moroccan Arabic: {{t|ary|واعر|tr=waʿr|sc=Arab}}
* Armenian: {{t+|hy|դժվար}}, {{t+|hy|բարդ}}, {{t+|hy|ծանր}}
* Assamese: {{t|as|টান}}
* Asturian: {{t+|ast|difícil}}
* Azerbaijani: {{t+|az|çətin}}, {{t|az|düşvar}}
* Bashkir: {{t|ba|ҡыйын|sc=Cyrl}}, {{t|ba|ауыр|sc=Cyrl}}
* Basque: {{t+|eu|nekez}}
* Belarusian: {{t+|be|ця́жкі}}, {{t|be|тру́дны}}
* Bengali: {{t+|bn|কঠিন|sc=Beng}}
* Bikol Central:
*: Bikol Legazpi: {{t+|bcl|dipisil}}
*: Bikol Naga: {{t|bcl|masakit}}
* Breton: {{t+|br|diaes}}
* Brunei Malay: {{t|kxd|payah}}
* Bulgarian: {{t+|bg|тру́ден}}, {{t+|bg|мъ́чен}}, {{t+|bg|те́жък}}
* Burmese: {{t+|my|ခဲယဉ်း|sc=Mymr}}, {{t+|my|ခက်|sc=Mymr}}
* Catalan: {{t+|ca|difícil}}
* Cebuano: {{t|ceb|lisud}}
* Chamicuro: {{t|ccc|yeewa}}
* Chechen: {{t|ce|хала}}
* Cherokee: {{t|chr|ᎤᏦᏍᏗ}}, {{t|chr|ᏍᏓᏯ}}
* Chinese:
*: Cantonese: {{t|yue|難|sc=Hani}}, {{t|yue|难|tr=nan4|sc=Hani}}, {{t|yue|困難|sc=Hani}}, {{t|yue|困难|tr=kwan3 nan4|sc=Hani}}
*: Mandarin: {{t+|cmn|難|sc=Hani}}, {{t+|cmn|难|tr=nán|sc=Hani}}, {{t+|cmn|困難|sc=Hani}}, {{t+|cmn|困难|tr=kùnnán|sc=Hani}}, {{t+|cmn|艱難|sc=Hani}}, {{t+|cmn|艰难|tr=jiānnán|sc=Hani}}
*: Wu: {{t|wuu|難|tr=ne̞²³}}
* Chukchi: {{t|ckt|аӈъачеты|tr=aṇʺačety}}
* Crimean Tatar: {{t|crh|qıyın}}
* Czech: {{t+|cs|obtížný}}, {{t+|cs|těžký}}
* Danish: {{t|da|vanskelig}}
* Dhivehi: {{t-needed|dv}}
* Dutch: {{t+|nl|lastig}}, {{t+|nl|moeilijk}}
* Egyptian: {{t-egy|qsn|h=q-qs-n:nDs}}
* Esperanto: {{t|eo|malfacila}}
* Estonian: {{t+|et|keeruline}}
* Faroese: {{t|fo|torførur}}
* Finnish: {{t+|fi|vaikea}}, {{t+|fi|hankala}}
* French: {{t+|fr|difficile}}
* Galician: {{t+|gl|difícil}}
* Georgian: {{t|ka|ძნელი}}, {{t|ka|რთული}}, {{t+|ka|მძიმე}}
* German: {{t+|de|schwer}}, {{t+|de|schwierig}}
* Gothic: {{t|got|𐌰𐌲𐌻𐌿𐍃}}
* Greek: {{t+|el|δύσκολος}}
*: Ancient: {{t|grc|χαλεπός}}, {{t|grc|δυσχερής}}
* Greenlandic: {{t|kl|sapernarpoq}}
* Gujarati: {{t|gu|કઠિન}}
* Haitian Creole: {{t|ht|difisil}}
* Hebrew: {{t+|he|קָשֶׁה|tr=kashé}}, {{t|he|מסובך|tr=mesubbakh}}
* Hindi: {{t+|hi|कठिन|sc=Deva}}, {{t+|hi|विकट}}, {{t+|hi|मुश्किल}}, {{t+|hi|दुश्वार}}
* Hungarian: {{t+|hu|nehéz}}
* Icelandic: {{t+|is|erfiður}}
* Ido: {{t+|io|desfacila}}
* Ilocano: {{t|ilo|narigat}}
* Indonesian: {{t+|id|sukar}}, {{t+|id|susah}}
* Ingush: {{t|inh|хала}}
* Interlingua: {{t|ia|difficile}}
* Irish: {{t|ga|deacair}}, {{t|ga|anfhurasta}}, {{t|ga|anacair}}, {{t|ga|achrannach}} {{qualifier|of terrain}}
* Italian: {{t+|it|difficile}}
* Japanese: {{t+|ja|難しい|tr=むずかしい, muzukashii}}, {{t+|ja|困難|tr=こんなんな, konnan na|alt=困難な|sc=Jpan}}
* Kabuverdianu: {{t|kea|puxadu}}, {{t|kea|pexóde}}
* Kannada: {{t+|kn|ಕಟ್|sc=Knda}}
* Kazakh: {{t+|kk|қиын|sc=Cyrl}}, {{t+|kk|ауыр|sc=Cyrl}}
* Khmer: {{t+|km|ពិបាក|tr=pi’baak|sc=Khmr}}, {{t+|km|យ៉ាប់|tr=yap|sc=Khmr}}
* Korean: {{t+|ko|어렵다}}, {{t+|ko|힘들다}}
* Kurdish:
*: Central Kurdish: {{t|ckb|زەحمەت}}, {{t|ckb|گران}}, {{t|ckb|سەخت}}
* Kyrgyz: {{t+|ky|кыйын|sc=Cyrl}}, {{t+|ky|оор|sc=Cyrl}}
* Ladino: {{t|lad|difísil}}, {{t|lad|zor}}
* Lao: {{t|lo|ຍາກ}}
* Latin: {{t+|la|difficilis}}
* Latvian: {{t|lv|sarežģīts}}, {{t|lv|grūts}}
* Lithuanian: {{t+|lt|sunkus}}
* Macedonian: {{t|mk|тежок|sc=Cyrl}}, {{t|mk|мачен|sc=Cyrl}}
* Maltese: {{t+|mt|diffiċli}}
* Manx: {{t|gv|doillee}}, {{t|gv|creoi}}, {{t|gv|trome}}, {{t|gv|mooar}}, {{t|gv|neuaashagh}}
* Maori: {{t|mi|whēuaua}}, {{t|mi|uaua}}, {{t|mi|taumaha hārukiruki}} {{qualifier|in the extreme}}
* Mirandese: {{t|mwl|defícel}}
* Mon: {{t|mnw|ဝါတ်|tr=wāt}}
* Mongolian: {{t|mn|хүнд хэцүү|sc=Cyrl}}, {{t+|mn|бэрх|sc=Cyrl}}
* Navajo: {{t|nv|nantłʼah}}
* Norwegian: {{t+|no|vanskelig}}
* Occitan: {{t+|oc|malaisit}}, {{t+|oc|dificil|m}}
* Odia: {{t-needed|or}}
* Old Church Slavonic:
*: Cyrillic: {{t|cu|тѩжькъ|sc=Cyrs}}
* Old English: {{t|ang|earfoþe}}
* Pashto: {{t|ps|ګران|m|tr=grân}}, {{t|ps|مشکل|m|tr=muškil}}
* Persian: {{t+|fa|دشوار|tr=došvâr}}, {{t+|fa|مشکل|tr=moškel}}, {{t+|fa|سخت|tr=saxt|sc=fa-Arab}}, {{t+|fa|صعب|tr=sa'b|sc=fa-Arab}}
* Polish: {{t+|pl|trudny}}, {{t+|pl|ciężki}}
* Portuguese: {{t+|pt|difícil}}
* Punjabi: {{t|pa|ਕਠਿਨ|sc=Guru}}
* Quechua: {{t|qu|sasa}}
* Romanian: {{t+|ro|greu}}, {{t+|ro|dificil}}, {{t+|ro|anevoios}}, {{t+|ro|complicat}}
* Russian: {{t+|ru|тру́дный|m}}, {{t+|ru|тяжёлый}}, {{t+|ru|тя́жкий}}, {{t+|ru|сло́жный}}
* Samoan: {{t-needed|sm}}
* Sanskrit: {{t+|sa|कठिन}}
* Scottish Gaelic: {{t|gd|doirbh}}, {{t|gd|mì-fhurasda}}, {{t|gd|cruaidh}}, {{t|gd|trom}}, {{t|gd|duilich}}
* Serbo-Croatian:
*: Cyrillic: {{t|sh|тежак}}
*: Roman: {{t+|sh|težak}}
* Sinhalese: {{t|si|අමාරු|sc=Sinh}}
* Slovak: {{t|sk|obtiažny}}, {{t|sk|ťažký}}
* Slovene: {{t+|sl|težek}}
* Sorbian:
*: Upper Sorbian: {{t|hsb|ćežki}}
* Spanish: {{t+|es|difícil}}
* Swedish: {{t+|sv|svår}}
* Tagalog: {{t|tl|mahirap}}
* Tajik: {{t+|tg|мушкил|sc=Cyrl}}, {{t|tg|душвор|sc=Cyrl}}, {{t+|tg|сахт|sc=Cyrl}}
* Tamil: {{t+|ta|கடினமான|sc=Taml}}
* Tatar: {{t|tt|кыен|sc=Cyrl}}, {{t+|tt|авыр|sc=Cyrl}}
* Telugu: {{t+|te|కఠినమైన|sc=Telu}}, {{t+|te|కష్టమైన|sc=Telu}}
* Tetum: {{t|tet|susar}}
* Thai: {{t+|th|ยาก}}, {{t+|th|ลำบาก}}
* Tibetan: {{t|bo|ཁག་པོ|sc=Tibt}}, {{t|bo|དཀའ་ལས་ཁག་པོ|sc=Tibt}}, {{t|bo|དཀའ་མོ|sc=Tibt}}, {{t|bo|དཀའ་ངལ་ཅན|sc=Tibt}}
* Tocharian B: {{t|txb|waimene}}, {{t|txb|āmāskai}}
* Tongan: {{t|to|faingataʻa}}
* Turkish: {{t+|tr|zor}}, {{t+|tr|güç}}, {{t+|tr|müşkül}}, {{t+|tr|çetin}}, {{t+|tr|kıyın}}, {{t|tr|düşvar}}, {{t+|tr|ağır}}
* Turkmen: {{t|tk|çatak}}, {{t|tk|çetin}}, {{t|tk|kyn}}, {{t|tk|müçgil}}
* Ukrainian: {{t+|uk|важки́й}}, {{t|uk|трудни́й}}
* Urdu: {{t|ur|مشکل|tr=muśkil}}, {{t|ur|دشوار|tr=duśvār}}, {{t|ur|کٹھن|tr=kaṭhin|sc=ur-Arab}}
* Uyghur: {{t|ug|قىيىن}}, {{t|ug|مۈشكۈل}}
* Uzbek: {{t+|uz|qiyin}}, {{t+|uz|mushkul}}, {{t+|uz|murakkab}}
* Vietnamese: {{t+|vi|khó}}, {{t+|vi|khó khăn}}
* Volapük: {{t-needed|vo}}
* Walloon: {{t-needed|wa}}
* Welsh: {{t+|cy|anodd}}, {{t+|cy|caled}}
* West Frisian: {{t+|fy|swier}}
* White Hmong: {{t-needed|mww}}
* Yiddish: {{t|yi|שווער|sc=Hebr}}, {{t|yi|האַרב|sc=Hebr}}
* Yoruba: {{t-needed|yo}}
* Zhuang: {{t-needed|za}}
{{trans-bottom}}
</nowiki>}}
{{module cat|-|Page generation}}
iw66glzud31m0xtd1vizpdo5rvr2vm4
375823
375822
2026-09-25T02:58:58Z
Hakimi97
2668
Betulkan ralat
375823
wikitext
text/x-wiki
{{#invoke:translations/multi-nowiki|expand|
<nowiki>
* Nested templates: {{t|cmn|大老二|tr={{m|cmn|dà lǎo èr}}}}
* Piped link: {{t|ja|{{...}}から[[見る|見]]た|lit=seen from {{...}}|tr={{...}} kara mita}}
* Substed module invocation:
** {{subst:#invoke:languages/templates|getByCode|csb|getCanonicalName}}: {{t|csb|czerwiony}}
{{trans-top|sukar, tak senang}}
* Afrikaans: {{t|af|moeilik}}
* Albanian: {{t+|sq|vështirë|alt=i vështirë}}
* Amharic: {{t|am|አስቸጋሪ}}
* Arabic: {{t|ar|صَعْب}}
*: Moroccan Arabic: {{t|ary|واعر|tr=waʿr|sc=Arab}}
* Armenian: {{t+|hy|դժվար}}, {{t+|hy|բարդ}}, {{t+|hy|ծանր}}
* Assamese: {{t|as|টান}}
* Asturian: {{t+|ast|difícil}}
* Azerbaijani: {{t+|az|çətin}}, {{t|az|düşvar}}
* Bashkir: {{t|ba|ҡыйын|sc=Cyrl}}, {{t|ba|ауыр|sc=Cyrl}}
* Basque: {{t+|eu|nekez}}
* Belarusian: {{t+|be|ця́жкі}}, {{t|be|тру́дны}}
* Bengali: {{t+|bn|কঠিন|sc=Beng}}
* Bikol Central:
*: Bikol Legazpi: {{t+|bcl|dipisil}}
*: Bikol Naga: {{t|bcl|masakit}}
* Breton: {{t+|br|diaes}}
* Brunei Malay: {{t|kxd|payah}}
* Bulgarian: {{t+|bg|тру́ден}}, {{t+|bg|мъ́чен}}, {{t+|bg|те́жък}}
* Burmese: {{t+|my|ခဲယဉ်း|sc=Mymr}}, {{t+|my|ခက်|sc=Mymr}}
* Catalan: {{t+|ca|difícil}}
* Cebuano: {{t|ceb|lisud}}
* Chamicuro: {{t|ccc|yeewa}}
* Chechen: {{t|ce|хала}}
* Cherokee: {{t|chr|ᎤᏦᏍᏗ}}, {{t|chr|ᏍᏓᏯ}}
* Chinese:
*: Cantonese: {{t|yue|難|sc=Hani}}, {{t|yue|难|tr=nan4|sc=Hani}}, {{t|yue|困難|sc=Hani}}, {{t|yue|困难|tr=kwan3 nan4|sc=Hani}}
*: Mandarin: {{t+|cmn|難|sc=Hani}}, {{t+|cmn|难|tr=nán|sc=Hani}}, {{t+|cmn|困難|sc=Hani}}, {{t+|cmn|困难|tr=kùnnán|sc=Hani}}, {{t+|cmn|艱難|sc=Hani}}, {{t+|cmn|艰难|tr=jiānnán|sc=Hani}}
*: Wu: {{t|wuu|難|tr=ne̞²³}}
* Chukchi: {{t|ckt|аӈъачеты|tr=aṇʺačety}}
* Crimean Tatar: {{t|crh|qıyın}}
* Czech: {{t+|cs|obtížný}}, {{t+|cs|těžký}}
* Danish: {{t|da|vanskelig}}
* Dhivehi: {{t-needed|dv}}
* Dutch: {{t+|nl|lastig}}, {{t+|nl|moeilijk}}
* Egyptian: {{t-egy|qsn|h=q-qs-n:nDs}}
* English: {{t+|en|difficult}}
* Esperanto: {{t|eo|malfacila}}
* Estonian: {{t+|et|keeruline}}
* Faroese: {{t|fo|torførur}}
* Finnish: {{t+|fi|vaikea}}, {{t+|fi|hankala}}
* French: {{t+|fr|difficile}}
* Galician: {{t+|gl|difícil}}
* Georgian: {{t|ka|ძნელი}}, {{t|ka|რთული}}, {{t+|ka|მძიმე}}
* German: {{t+|de|schwer}}, {{t+|de|schwierig}}
* Gothic: {{t|got|𐌰𐌲𐌻𐌿𐍃}}
* Greek: {{t+|el|δύσκολος}}
*: Ancient: {{t|grc|χαλεπός}}, {{t|grc|δυσχερής}}
* Greenlandic: {{t|kl|sapernarpoq}}
* Gujarati: {{t|gu|કઠિન}}
* Haitian Creole: {{t|ht|difisil}}
* Hebrew: {{t+|he|קָשֶׁה|tr=kashé}}, {{t|he|מסובך|tr=mesubbakh}}
* Hindi: {{t+|hi|कठिन|sc=Deva}}, {{t+|hi|विकट}}, {{t+|hi|मुश्किल}}, {{t+|hi|दुश्वार}}
* Hungarian: {{t+|hu|nehéz}}
* Icelandic: {{t+|is|erfiður}}
* Ido: {{t+|io|desfacila}}
* Ilocano: {{t|ilo|narigat}}
* Indonesian: {{t+|id|sukar}}, {{t+|id|susah}}
* Ingush: {{t|inh|хала}}
* Interlingua: {{t|ia|difficile}}
* Irish: {{t|ga|deacair}}, {{t|ga|anfhurasta}}, {{t|ga|anacair}}, {{t|ga|achrannach}} {{qualifier|of terrain}}
* Italian: {{t+|it|difficile}}
* Japanese: {{t+|ja|難しい|tr=むずかしい, muzukashii}}, {{t+|ja|困難|tr=こんなんな, konnan na|alt=困難な|sc=Jpan}}
* Kabuverdianu: {{t|kea|puxadu}}, {{t|kea|pexóde}}
* Kannada: {{t+|kn|ಕಟ್|sc=Knda}}
* Kazakh: {{t+|kk|қиын|sc=Cyrl}}, {{t+|kk|ауыр|sc=Cyrl}}
* Khmer: {{t+|km|ពិបាក|tr=pi’baak|sc=Khmr}}, {{t+|km|យ៉ាប់|tr=yap|sc=Khmr}}
* Korean: {{t+|ko|어렵다}}, {{t+|ko|힘들다}}
* Kurdish:
*: Central Kurdish: {{t|ckb|زەحمەت}}, {{t|ckb|گران}}, {{t|ckb|سەخت}}
* Kyrgyz: {{t+|ky|кыйын|sc=Cyrl}}, {{t+|ky|оор|sc=Cyrl}}
* Ladino: {{t|lad|difísil}}, {{t|lad|zor}}
* Lao: {{t|lo|ຍາກ}}
* Latin: {{t+|la|difficilis}}
* Latvian: {{t|lv|sarežģīts}}, {{t|lv|grūts}}
* Lithuanian: {{t+|lt|sunkus}}
* Macedonian: {{t|mk|тежок|sc=Cyrl}}, {{t|mk|мачен|sc=Cyrl}}
* Maltese: {{t+|mt|diffiċli}}
* Manx: {{t|gv|doillee}}, {{t|gv|creoi}}, {{t|gv|trome}}, {{t|gv|mooar}}, {{t|gv|neuaashagh}}
* Maori: {{t|mi|whēuaua}}, {{t|mi|uaua}}, {{t|mi|taumaha hārukiruki}} {{qualifier|in the extreme}}
* Mirandese: {{t|mwl|defícel}}
* Mon: {{t|mnw|ဝါတ်|tr=wāt}}
* Mongolian: {{t|mn|хүнд хэцүү|sc=Cyrl}}, {{t+|mn|бэрх|sc=Cyrl}}
* Navajo: {{t|nv|nantłʼah}}
* Norwegian: {{t+|no|vanskelig}}
* Occitan: {{t+|oc|malaisit}}, {{t+|oc|dificil|m}}
* Odia: {{t-needed|or}}
* Old Church Slavonic:
*: Cyrillic: {{t|cu|тѩжькъ|sc=Cyrs}}
* Old English: {{t|ang|earfoþe}}
* Pashto: {{t|ps|ګران|m|tr=grân}}, {{t|ps|مشکل|m|tr=muškil}}
* Persian: {{t+|fa|دشوار|tr=došvâr}}, {{t+|fa|مشکل|tr=moškel}}, {{t+|fa|سخت|tr=saxt|sc=fa-Arab}}, {{t+|fa|صعب|tr=sa'b|sc=fa-Arab}}
* Polish: {{t+|pl|trudny}}, {{t+|pl|ciężki}}
* Portuguese: {{t+|pt|difícil}}
* Punjabi: {{t|pa|ਕਠਿਨ|sc=Guru}}
* Quechua: {{t|qu|sasa}}
* Romanian: {{t+|ro|greu}}, {{t+|ro|dificil}}, {{t+|ro|anevoios}}, {{t+|ro|complicat}}
* Russian: {{t+|ru|тру́дный|m}}, {{t+|ru|тяжёлый}}, {{t+|ru|тя́жкий}}, {{t+|ru|сло́жный}}
* Samoan: {{t-needed|sm}}
* Sanskrit: {{t+|sa|कठिन}}
* Scottish Gaelic: {{t|gd|doirbh}}, {{t|gd|mì-fhurasda}}, {{t|gd|cruaidh}}, {{t|gd|trom}}, {{t|gd|duilich}}
* Serbo-Croatian:
*: Cyrillic: {{t|sh|тежак}}
*: Roman: {{t+|sh|težak}}
* Sinhalese: {{t|si|අමාරු|sc=Sinh}}
* Slovak: {{t|sk|obtiažny}}, {{t|sk|ťažký}}
* Slovene: {{t+|sl|težek}}
* Sorbian:
*: Upper Sorbian: {{t|hsb|ćežki}}
* Spanish: {{t+|es|difícil}}
* Swedish: {{t+|sv|svår}}
* Tagalog: {{t|tl|mahirap}}
* Tajik: {{t+|tg|мушкил|sc=Cyrl}}, {{t|tg|душвор|sc=Cyrl}}, {{t+|tg|сахт|sc=Cyrl}}
* Tamil: {{t+|ta|கடினமான|sc=Taml}}
* Tatar: {{t|tt|кыен|sc=Cyrl}}, {{t+|tt|авыр|sc=Cyrl}}
* Telugu: {{t+|te|కఠినమైన|sc=Telu}}, {{t+|te|కష్టమైన|sc=Telu}}
* Tetum: {{t|tet|susar}}
* Thai: {{t+|th|ยาก}}, {{t+|th|ลำบาก}}
* Tibetan: {{t|bo|ཁག་པོ|sc=Tibt}}, {{t|bo|དཀའ་ལས་ཁག་པོ|sc=Tibt}}, {{t|bo|དཀའ་མོ|sc=Tibt}}, {{t|bo|དཀའ་ངལ་ཅན|sc=Tibt}}
* Tocharian B: {{t|txb|waimene}}, {{t|txb|āmāskai}}
* Tongan: {{t|to|faingataʻa}}
* Turkish: {{t+|tr|zor}}, {{t+|tr|güç}}, {{t+|tr|müşkül}}, {{t+|tr|çetin}}, {{t+|tr|kıyın}}, {{t|tr|düşvar}}, {{t+|tr|ağır}}
* Turkmen: {{t|tk|çatak}}, {{t|tk|çetin}}, {{t|tk|kyn}}, {{t|tk|müçgil}}
* Ukrainian: {{t+|uk|важки́й}}, {{t|uk|трудни́й}}
* Urdu: {{t|ur|مشکل|tr=muśkil}}, {{t|ur|دشوار|tr=duśvār}}, {{t|ur|کٹھن|tr=kaṭhin|sc=Aran}}
* Uyghur: {{t|ug|قىيىن}}, {{t|ug|مۈشكۈل}}
* Uzbek: {{t+|uz|qiyin}}, {{t+|uz|mushkul}}, {{t+|uz|murakkab}}
* Vietnamese: {{t+|vi|khó}}, {{t+|vi|khó khăn}}
* Volapük: {{t-needed|vo}}
* Walloon: {{t-needed|wa}}
* Welsh: {{t+|cy|anodd}}, {{t+|cy|caled}}
* West Frisian: {{t+|fy|swier}}
* White Hmong: {{t-needed|mww}}
* Yiddish: {{t|yi|שווער|sc=Hebr}}, {{t|yi|האַרב|sc=Hebr}}
* Yoruba: {{t-needed|yo}}
* Zhuang: {{t-needed|za}}
{{trans-bottom}}
</nowiki>}}
{{module cat|-|Page generation}}
rpytwiaofcjoa1w4whaqh9a3t8r25a6
375824
375823
2026-09-25T03:00:13Z
Hakimi97
2668
Betulkan kod
375824
wikitext
text/x-wiki
{{#invoke:translations/multi-nowiki|expand|
<nowiki>
* Nested templates: {{t|cmn|大老二|tr={{m|cmn|dà lǎo èr}}}}
* Piped link: {{t|ja|{{...}}から[[見る|見]]た|lit=seen from {{...}}|tr={{...}} kara mita}}
* Substed module invocation:
** {{subst:#invoke:languages/templates|getByCode|csb|getCanonicalName}}: {{t|csb|czerwiony}}
{{trans-top|sukar, tak senang}}
* Afrikaans: {{t|af|moeilik}}
* Albanian: {{t+|sq|vështirë|alt=i vështirë}}
* Amharic: {{t|am|አስቸጋሪ}}
* Arabic: {{t|ar|صَعْب}}
*: Moroccan Arabic: {{t|ary|واعر|tr=waʿr|sc=Arab}}
* Armenian: {{t+|hy|դժվար}}, {{t+|hy|բարդ}}, {{t+|hy|ծանր}}
* Assamese: {{t|as|টান}}
* Asturian: {{t+|ast|difícil}}
* Azerbaijani: {{t+|az|çətin}}, {{t|az|düşvar}}
* Bashkir: {{t|ba|ҡыйын|sc=Cyrl}}, {{t|ba|ауыр|sc=Cyrl}}
* Basque: {{t+|eu|nekez}}
* Belarusian: {{t+|be|ця́жкі}}, {{t|be|тру́дны}}
* Bengali: {{t+|bn|কঠিন|sc=Beng}}
* Bikol Central:
*: Bikol Legazpi: {{t+|bcl|dipisil}}
*: Bikol Naga: {{t|bcl|masakit}}
* Breton: {{t+|br|diaes}}
* Brunei Malay: {{t|kxd|payah}}
* Bulgarian: {{t+|bg|тру́ден}}, {{t+|bg|мъ́чен}}, {{t+|bg|те́жък}}
* Burmese: {{t+|my|ခဲယဉ်း|sc=Mymr}}, {{t+|my|ခက်|sc=Mymr}}
* Catalan: {{t+|ca|difícil}}
* Cebuano: {{t|ceb|lisud}}
* Chamicuro: {{t|ccc|yeewa}}
* Chechen: {{t|ce|хала}}
* Cherokee: {{t|chr|ᎤᏦᏍᏗ}}, {{t|chr|ᏍᏓᏯ}}
* Chinese:
*: Cantonese: {{t|yue|難|sc=Hani}}, {{t|yue|难|tr=nan4|sc=Hani}}, {{t|yue|困難|sc=Hani}}, {{t|yue|困难|tr=kwan3 nan4|sc=Hani}}
*: Mandarin: {{t+|cmn|難|sc=Hani}}, {{t+|cmn|难|tr=nán|sc=Hani}}, {{t+|cmn|困難|sc=Hani}}, {{t+|cmn|困难|tr=kùnnán|sc=Hani}}, {{t+|cmn|艱難|sc=Hani}}, {{t+|cmn|艰难|tr=jiānnán|sc=Hani}}
*: Wu: {{t|wuu|難|tr=ne̞²³}}
* Chukchi: {{t|ckt|аӈъачеты|tr=aṇʺačety}}
* Crimean Tatar: {{t|crh|qıyın}}
* Czech: {{t+|cs|obtížný}}, {{t+|cs|těžký}}
* Danish: {{t|da|vanskelig}}
* Dhivehi: {{t-needed|dv}}
* Dutch: {{t+|nl|lastig}}, {{t+|nl|moeilijk}}
* Egyptian: {{t-egy|qsn|h=q-qs-n:nDs}}
* English: {{t+|en|difficult}}
* Esperanto: {{t|eo|malfacila}}
* Estonian: {{t+|et|keeruline}}
* Faroese: {{t|fo|torførur}}
* Finnish: {{t+|fi|vaikea}}, {{t+|fi|hankala}}
* French: {{t+|fr|difficile}}
* Galician: {{t+|gl|difícil}}
* Georgian: {{t|ka|ძნელი}}, {{t|ka|რთული}}, {{t+|ka|მძიმე}}
* German: {{t+|de|schwer}}, {{t+|de|schwierig}}
* Gothic: {{t|got|𐌰𐌲𐌻𐌿𐍃}}
* Greek: {{t+|el|δύσκολος}}
*: Ancient: {{t|grc|χαλεπός}}, {{t|grc|δυσχερής}}
* Greenlandic: {{t|kl|sapernarpoq}}
* Gujarati: {{t|gu|કઠિન}}
* Haitian Creole: {{t|ht|difisil}}
* Hebrew: {{t+|he|קָשֶׁה|tr=kashé}}, {{t|he|מסובך|tr=mesubbakh}}
* Hindi: {{t+|hi|कठिन|sc=Deva}}, {{t+|hi|विकट}}, {{t+|hi|मुश्किल}}, {{t+|hi|दुश्वार}}
* Hungarian: {{t+|hu|nehéz}}
* Icelandic: {{t+|is|erfiður}}
* Ido: {{t+|io|desfacila}}
* Ilocano: {{t|ilo|narigat}}
* Indonesian: {{t+|id|sukar}}, {{t+|id|susah}}
* Ingush: {{t|inh|хала}}
* Interlingua: {{t|ia|difficile}}
* Irish: {{t|ga|deacair}}, {{t|ga|anfhurasta}}, {{t|ga|anacair}}, {{t|ga|achrannach}} {{qualifier|of terrain}}
* Italian: {{t+|it|difficile}}
* Japanese: {{t+|ja|難しい|tr=むずかしい, muzukashii}}, {{t+|ja|困難|tr=こんなんな, konnan na|alt=困難な|sc=Jpan}}
* Kabuverdianu: {{t|kea|puxadu}}, {{t|kea|pexóde}}
* Kannada: {{t+|kn|ಕಟ್|sc=Knda}}
* Kazakh: {{t+|kk|қиын|sc=Cyrl}}, {{t+|kk|ауыр|sc=Cyrl}}
* Khmer: {{t+|km|ពិបាក|tr=pi’baak|sc=Khmr}}, {{t+|km|យ៉ាប់|tr=yap|sc=Khmr}}
* Korean: {{t+|ko|어렵다}}, {{t+|ko|힘들다}}
* Kurdish:
*: Central Kurdish: {{t|ckb|زەحمەت}}, {{t|ckb|گران}}, {{t|ckb|سەخت}}
* Kyrgyz: {{t+|ky|кыйын|sc=Cyrl}}, {{t+|ky|оор|sc=Cyrl}}
* Ladino: {{t|lad|difísil}}, {{t|lad|zor}}
* Lao: {{t|lo|ຍາກ}}
* Latin: {{t+|la|difficilis}}
* Latvian: {{t|lv|sarežģīts}}, {{t|lv|grūts}}
* Lithuanian: {{t+|lt|sunkus}}
* Macedonian: {{t|mk|тежок|sc=Cyrl}}, {{t|mk|мачен|sc=Cyrl}}
* Maltese: {{t+|mt|diffiċli}}
* Manx: {{t|gv|doillee}}, {{t|gv|creoi}}, {{t|gv|trome}}, {{t|gv|mooar}}, {{t|gv|neuaashagh}}
* Maori: {{t|mi|whēuaua}}, {{t|mi|uaua}}, {{t|mi|taumaha hārukiruki}} {{qualifier|in the extreme}}
* Mirandese: {{t|mwl|defícel}}
* Mon: {{t|mnw|ဝါတ်|tr=wāt}}
* Mongolian: {{t|mn|хүнд хэцүү|sc=Cyrl}}, {{t+|mn|бэрх|sc=Cyrl}}
* Navajo: {{t|nv|nantłʼah}}
* Norwegian: {{t+|no|vanskelig}}
* Occitan: {{t+|oc|malaisit}}, {{t+|oc|dificil|m}}
* Odia: {{t-needed|or}}
* Old Church Slavonic:
*: Cyrillic: {{t|cu|тѩжькъ|sc=Cyrs}}
* Old English: {{t|ang|earfoþe}}
* Pashto: {{t|ps|ګران|m|tr=grân}}, {{t|ps|مشکل|m|tr=muškil}}
* Persian: {{t+|fa|دشوار|tr=došvâr}}, {{t+|fa|مشکل|tr=moškel}}, {{t+|fa|سخت|tr=saxt|sc=Aran}}, {{t+|fa|صعب|tr=sa'b|sc=Aran}}
* Polish: {{t+|pl|trudny}}, {{t+|pl|ciężki}}
* Portuguese: {{t+|pt|difícil}}
* Punjabi: {{t|pa|ਕਠਿਨ|sc=Guru}}
* Quechua: {{t|qu|sasa}}
* Romanian: {{t+|ro|greu}}, {{t+|ro|dificil}}, {{t+|ro|anevoios}}, {{t+|ro|complicat}}
* Russian: {{t+|ru|тру́дный|m}}, {{t+|ru|тяжёлый}}, {{t+|ru|тя́жкий}}, {{t+|ru|сло́жный}}
* Samoan: {{t-needed|sm}}
* Sanskrit: {{t+|sa|कठिन}}
* Scottish Gaelic: {{t|gd|doirbh}}, {{t|gd|mì-fhurasda}}, {{t|gd|cruaidh}}, {{t|gd|trom}}, {{t|gd|duilich}}
* Serbo-Croatian:
*: Cyrillic: {{t|sh|тежак}}
*: Roman: {{t+|sh|težak}}
* Sinhalese: {{t|si|අමාරු|sc=Sinh}}
* Slovak: {{t|sk|obtiažny}}, {{t|sk|ťažký}}
* Slovene: {{t+|sl|težek}}
* Sorbian:
*: Upper Sorbian: {{t|hsb|ćežki}}
* Spanish: {{t+|es|difícil}}
* Swedish: {{t+|sv|svår}}
* Tagalog: {{t|tl|mahirap}}
* Tajik: {{t+|tg|мушкил|sc=Cyrl}}, {{t|tg|душвор|sc=Cyrl}}, {{t+|tg|сахт|sc=Cyrl}}
* Tamil: {{t+|ta|கடினமான|sc=Taml}}
* Tatar: {{t|tt|кыен|sc=Cyrl}}, {{t+|tt|авыр|sc=Cyrl}}
* Telugu: {{t+|te|కఠినమైన|sc=Telu}}, {{t+|te|కష్టమైన|sc=Telu}}
* Tetum: {{t|tet|susar}}
* Thai: {{t+|th|ยาก}}, {{t+|th|ลำบาก}}
* Tibetan: {{t|bo|ཁག་པོ|sc=Tibt}}, {{t|bo|དཀའ་ལས་ཁག་པོ|sc=Tibt}}, {{t|bo|དཀའ་མོ|sc=Tibt}}, {{t|bo|དཀའ་ངལ་ཅན|sc=Tibt}}
* Tocharian B: {{t|txb|waimene}}, {{t|txb|āmāskai}}
* Tongan: {{t|to|faingataʻa}}
* Turkish: {{t+|tr|zor}}, {{t+|tr|güç}}, {{t+|tr|müşkül}}, {{t+|tr|çetin}}, {{t+|tr|kıyın}}, {{t|tr|düşvar}}, {{t+|tr|ağır}}
* Turkmen: {{t|tk|çatak}}, {{t|tk|çetin}}, {{t|tk|kyn}}, {{t|tk|müçgil}}
* Ukrainian: {{t+|uk|важки́й}}, {{t|uk|трудни́й}}
* Urdu: {{t|ur|مشکل|tr=muśkil}}, {{t|ur|دشوار|tr=duśvār}}, {{t|ur|کٹھن|tr=kaṭhin|sc=Aran}}
* Uyghur: {{t|ug|قىيىن}}, {{t|ug|مۈشكۈل}}
* Uzbek: {{t+|uz|qiyin}}, {{t+|uz|mushkul}}, {{t+|uz|murakkab}}
* Vietnamese: {{t+|vi|khó}}, {{t+|vi|khó khăn}}
* Volapük: {{t-needed|vo}}
* Walloon: {{t-needed|wa}}
* Welsh: {{t+|cy|anodd}}, {{t+|cy|caled}}
* West Frisian: {{t+|fy|swier}}
* White Hmong: {{t-needed|mww}}
* Yiddish: {{t|yi|שווער|sc=Hebr}}, {{t|yi|האַרב|sc=Hebr}}
* Yoruba: {{t-needed|yo}}
* Zhuang: {{t-needed|za}}
{{trans-bottom}}
</nowiki>}}
{{module cat|-|Page generation}}
5z4bann5lllcnsu1v0aigcwisjggqzp
Modul:family tree/doc
828
57767
375837
373775
2026-09-25T07:52:43Z
SNN95
2113
terjemah
375837
wikitext
text/x-wiki
{{shortcut|WT:ANC}}
== Pokok salasilah ==
Modul ini melukis pokok salasilah bagi sesuatu bahasa, keluarga atau bahasa etimologi sahaja daripada data dalam [[Module:languages]], [[Module:families]] dan [[Module:etymology languages]]. Kotak "Family tree" (Pokok salasilah) pada halaman kategori bahasa dan keluarga berasal daripadanya.
{{temp|#invoke:family tree|show|<var>kod</var>}}
* Parameter pertama ialah kod bahasa, keluarga atau bahasa etimologi sahaja.
* {{para|families|show}} memaparkan keluarga bahasa sebagai nod mereka sendiri di atas bahasa protonya. Secara lalai, sebuah keluarga yang mana anak tunggalnya ialah bahasa protonya akan digantikan dengan bahasa proto tersebut, kerana kedua-dua baris tersebut akan menyatakan perkara yang sama.
* {{para|varieties|show}} memaparkan bahasa etimologi sahaja di bawah bahasa induknya. Secara lalai ia disembunyikan, melainkan jika salah satu daripadanya mempunyai turunan yang bukan dari jenis variasi.
* {{para|pair|family-first}} melukis sebuah keluarga dan bahasa protonya sebagai tajuk dua baris, di mana keluarga didahulukan, dengan turunan bahasa proto tersebut berada di bawah kedua-duanya. {{para|pair|proto-first}} meletakkan bahasa proto pada baris pertama. Ini hanya akan berfungsi jika ia diletakkan bersama dengan {{para|families|show}}.
* {{para|collapsed|1}} meruntuhkan (menyembunyikan) pokok di bawah akar secara lalai.
* {{para|lemmas|1}} menambahkan bilangan lema di Wiktionary selepas setiap bahasa.
=== Lencana ===
Setiap nod membawa satu lencana untuk setiap peranan yang dimilikinya:
* {{code||Kb: sebuah keluarga bahasa.
* {{code||P}}: sebuah bahasa proto, iaitu leluhur kepada sebuah keluarga yang telah dibina semula.
* {{code||Kl}}: sebuah variasi, iaitu, bahasa etimologi sahaja, yang dipaparkan di bawah induknya.
Nod yang mempunyai dua peranan akan mendapat dua lencana. Bahasa Proto-Ket ialah variasi bagi bahasa Proto-Yenisei dan juga merupakan bahasa proto bagi bahasa Ket, jadi ia memaparkan {{code||P V}}.
=== Contoh ===
{{#invoke:family tree|show|gem-pro|families=show|varieties=show}}
=== Pokok ===
Had kemasukan templat menghalang halaman ini daripada memaparkan kesemua pokok bahasa semasa di Wiktionary. Di atas ialah contoh pokok bahasa. Apabila membuat perubahan pada data [[Module:languages]], anda mungkin perlu <code><span class='plainlinks'>[https://en.wiktionary.org/w/index.php?title=Module%3Afamily_tree%2Fdocumentation&action=purge&forcelinkupdate menyegarkan semula]</span></code> halaman ini untuk melihat kemas kini.
=== Daripada Lua ===
<syntaxhighlight lang="lua">
require("Module:family tree").render(code, {
families = "show", -- atau "hide" (lalai)
varieties = "show", -- atau "hide" (lalai)
pair = "family-first", -- atau "proto-first", "none" (lalai)
collapsed = false,
lemmas = false,
must_have_descendants = true, -- mengembalikan nil dan bukannya akar tunggal
})
</syntaxhighlight>
Ini akan mengembalikan HTML bagi pokok tersebut. Dengan {{code|lua|must_have_descendants}} ditetapkan, ia sebaliknya akan mengembalikan {{code|lua|nil}} apabila tiada apa-apa untuk dilukis di bawah akar.
=== Lihat juga ===
* [[Module:descendants tree]]
{{module cat|-|Language and script,List}}
q4yf3caf5t25cunzmz0xbzoj5mtktox
375838
375837
2026-09-25T07:53:02Z
SNN95
2113
/* Lencana */
375838
wikitext
text/x-wiki
{{shortcut|WT:ANC}}
== Pokok salasilah ==
Modul ini melukis pokok salasilah bagi sesuatu bahasa, keluarga atau bahasa etimologi sahaja daripada data dalam [[Module:languages]], [[Module:families]] dan [[Module:etymology languages]]. Kotak "Family tree" (Pokok salasilah) pada halaman kategori bahasa dan keluarga berasal daripadanya.
{{temp|#invoke:family tree|show|<var>kod</var>}}
* Parameter pertama ialah kod bahasa, keluarga atau bahasa etimologi sahaja.
* {{para|families|show}} memaparkan keluarga bahasa sebagai nod mereka sendiri di atas bahasa protonya. Secara lalai, sebuah keluarga yang mana anak tunggalnya ialah bahasa protonya akan digantikan dengan bahasa proto tersebut, kerana kedua-dua baris tersebut akan menyatakan perkara yang sama.
* {{para|varieties|show}} memaparkan bahasa etimologi sahaja di bawah bahasa induknya. Secara lalai ia disembunyikan, melainkan jika salah satu daripadanya mempunyai turunan yang bukan dari jenis variasi.
* {{para|pair|family-first}} melukis sebuah keluarga dan bahasa protonya sebagai tajuk dua baris, di mana keluarga didahulukan, dengan turunan bahasa proto tersebut berada di bawah kedua-duanya. {{para|pair|proto-first}} meletakkan bahasa proto pada baris pertama. Ini hanya akan berfungsi jika ia diletakkan bersama dengan {{para|families|show}}.
* {{para|collapsed|1}} meruntuhkan (menyembunyikan) pokok di bawah akar secara lalai.
* {{para|lemmas|1}} menambahkan bilangan lema di Wiktionary selepas setiap bahasa.
=== Lencana ===
Setiap nod membawa satu lencana untuk setiap peranan yang dimilikinya:
* {{code||Kb}}: sebuah keluarga bahasa.
* {{code||P}}: sebuah bahasa proto, iaitu leluhur kepada sebuah keluarga yang telah dibina semula.
* {{code||Kl}}: sebuah variasi, iaitu, bahasa etimologi sahaja, yang dipaparkan di bawah induknya.
Nod yang mempunyai dua peranan akan mendapat dua lencana. Bahasa Proto-Ket ialah variasi bagi bahasa Proto-Yenisei dan juga merupakan bahasa proto bagi bahasa Ket, jadi ia memaparkan {{code||P V}}.
=== Contoh ===
{{#invoke:family tree|show|gem-pro|families=show|varieties=show}}
=== Pokok ===
Had kemasukan templat menghalang halaman ini daripada memaparkan kesemua pokok bahasa semasa di Wiktionary. Di atas ialah contoh pokok bahasa. Apabila membuat perubahan pada data [[Module:languages]], anda mungkin perlu <code><span class='plainlinks'>[https://en.wiktionary.org/w/index.php?title=Module%3Afamily_tree%2Fdocumentation&action=purge&forcelinkupdate menyegarkan semula]</span></code> halaman ini untuk melihat kemas kini.
=== Daripada Lua ===
<syntaxhighlight lang="lua">
require("Module:family tree").render(code, {
families = "show", -- atau "hide" (lalai)
varieties = "show", -- atau "hide" (lalai)
pair = "family-first", -- atau "proto-first", "none" (lalai)
collapsed = false,
lemmas = false,
must_have_descendants = true, -- mengembalikan nil dan bukannya akar tunggal
})
</syntaxhighlight>
Ini akan mengembalikan HTML bagi pokok tersebut. Dengan {{code|lua|must_have_descendants}} ditetapkan, ia sebaliknya akan mengembalikan {{code|lua|nil}} apabila tiada apa-apa untuk dilukis di bawah akar.
=== Lihat juga ===
* [[Module:descendants tree]]
{{module cat|-|Language and script,List}}
6xoog5edyxff6234bwpw8i8amqq9l3d
375839
375838
2026-09-25T07:54:02Z
SNN95
2113
/* Lencana */
375839
wikitext
text/x-wiki
{{shortcut|WT:ANC}}
== Pokok salasilah ==
Modul ini melukis pokok salasilah bagi sesuatu bahasa, keluarga atau bahasa etimologi sahaja daripada data dalam [[Module:languages]], [[Module:families]] dan [[Module:etymology languages]]. Kotak "Family tree" (Pokok salasilah) pada halaman kategori bahasa dan keluarga berasal daripadanya.
{{temp|#invoke:family tree|show|<var>kod</var>}}
* Parameter pertama ialah kod bahasa, keluarga atau bahasa etimologi sahaja.
* {{para|families|show}} memaparkan keluarga bahasa sebagai nod mereka sendiri di atas bahasa protonya. Secara lalai, sebuah keluarga yang mana anak tunggalnya ialah bahasa protonya akan digantikan dengan bahasa proto tersebut, kerana kedua-dua baris tersebut akan menyatakan perkara yang sama.
* {{para|varieties|show}} memaparkan bahasa etimologi sahaja di bawah bahasa induknya. Secara lalai ia disembunyikan, melainkan jika salah satu daripadanya mempunyai turunan yang bukan dari jenis variasi.
* {{para|pair|family-first}} melukis sebuah keluarga dan bahasa protonya sebagai tajuk dua baris, di mana keluarga didahulukan, dengan turunan bahasa proto tersebut berada di bawah kedua-duanya. {{para|pair|proto-first}} meletakkan bahasa proto pada baris pertama. Ini hanya akan berfungsi jika ia diletakkan bersama dengan {{para|families|show}}.
* {{para|collapsed|1}} meruntuhkan (menyembunyikan) pokok di bawah akar secara lalai.
* {{para|lemmas|1}} menambahkan bilangan lema di Wiktionary selepas setiap bahasa.
=== Lencana ===
Setiap nod membawa satu lencana untuk setiap peranan yang dimilikinya:
* {{code||Kb}}: sebuah keluarga bahasa.
* {{code||P}}: sebuah bahasa purba, iaitu leluhur kepada sebuah keluarga yang telah dibina semula.
* {{code||Kl}}: sebuah variasi, iaitu, bahasa etimologi sahaja, yang dipaparkan di bawah induknya.
Nod yang mempunyai dua peranan akan mendapat dua lencana. Bahasa Ket Purba ialah variasi bagi bahasa Yenisei Purba dan juga merupakan bahasa purba bagi bahasa Ket, jadi ia memaparkan {{code||P Kl}}.
=== Contoh ===
{{#invoke:family tree|show|gem-pro|families=show|varieties=show}}
=== Pokok ===
Had kemasukan templat menghalang halaman ini daripada memaparkan kesemua pokok bahasa semasa di Wiktionary. Di atas ialah contoh pokok bahasa. Apabila membuat perubahan pada data [[Module:languages]], anda mungkin perlu <code><span class='plainlinks'>[https://en.wiktionary.org/w/index.php?title=Module%3Afamily_tree%2Fdocumentation&action=purge&forcelinkupdate menyegarkan semula]</span></code> halaman ini untuk melihat kemas kini.
=== Daripada Lua ===
<syntaxhighlight lang="lua">
require("Module:family tree").render(code, {
families = "show", -- atau "hide" (lalai)
varieties = "show", -- atau "hide" (lalai)
pair = "family-first", -- atau "proto-first", "none" (lalai)
collapsed = false,
lemmas = false,
must_have_descendants = true, -- mengembalikan nil dan bukannya akar tunggal
})
</syntaxhighlight>
Ini akan mengembalikan HTML bagi pokok tersebut. Dengan {{code|lua|must_have_descendants}} ditetapkan, ia sebaliknya akan mengembalikan {{code|lua|nil}} apabila tiada apa-apa untuk dilukis di bawah akar.
=== Lihat juga ===
* [[Module:descendants tree]]
{{module cat|-|Language and script,List}}
o6m73vkuxqquhd3qg9kzoyzephk9lk6
Modul:etymon
828
57903
375859
375383
2026-09-25T10:45:12Z
SNN95
2113
terjemah ruang nama
375859
Scribunto
text/plain
--[=[
This module implements the {{etymon}} template for structured etymology data on Wiktionary.
It enables the creation of etymology trees and text by parsing etymon chains,
scraping linked pages for their own {{etymon}} data, and recursively building a tree
of derivational relationships.
Authors:
- Original implementation: [[User:Ioaxxere]]
- Full refactor (September 2025): [[User:Fenakhay]] ([[Special:Diff/86717746]])
Modules:
- [[Module:etymon]]: main module handling parsing, validation, tree building, and page scraping
- [[Module:etymon/data]]: keyword definitions, configuration, and status constants
- [[Module:etymon/tree]]: etymology tree rendering
- [[Module:etymon/text]]: etymology text generation
- [[Module:etymon/categories]]: category generation logic
- [[Module:etymon/tracking]]: tracking
]=]
local export = {}
local __state = {
cached_etymon_args = {},
cached_etymon_pages = {},
cached_descendants_checks = {},
senseid_parent_etymon = {},
available_etymon_ids = {},
single_etymons = {},
entry_title = nil,
entry_lang_code = nil,
current_page_has_inline_etymology = false,
current_page_has_redundant_etymology = false,
used_idless_etymon = false,
toplevel_has_inline_etymology = false,
toplevel_redundant_etymology = false,
toplevel_idless_etymon = false,
has_mismatched_id = false,
linked_page_multiple_etymons_idless = false,
linked_page_partial_etymology_sections = false,
partial_etymology_targets = {},
skip_partial_etymology_category = false,
max_depth_reached = 0,
total_nodes = 0,
language_count = {},
toplevel_keyword_stats = {},
id_stats = nil,
warnings = {},
}
local function reset_invocation_state()
__state.current_page_has_inline_etymology = false
__state.current_page_has_redundant_etymology = false
__state.used_idless_etymon = false
__state.toplevel_has_inline_etymology = false
__state.toplevel_redundant_etymology = false
__state.toplevel_idless_etymon = false
__state.has_mismatched_id = false
__state.linked_page_multiple_etymons_idless = false
__state.linked_page_partial_etymology_sections = false
__state.max_depth_reached = 0
__state.total_nodes = 0
__state.language_count = {}
__state.toplevel_keyword_stats = {}
__state.warnings = {}
end
local M = require("Module:module loader").init({
require = {
data = "Module:etymon/data",
tree = "Module:etymon/tree",
text = "Module:etymon/text",
categories = "Module:etymon/categories",
tracking = "Module:etymon/tracking",
descendants = "Module:etymon/descendants",
anchors = "Module:anchors",
etydate = "Module:etydate",
etymology = "Module:etymology",
families = "Module:families",
languages = "Module:languages",
languages_errorgetby = "Module:languages/errorGetBy",
links = "Module:links",
pages = "Module:pages",
parameters = "Module:parameters",
string_utilities = "Module:string utilities",
template_parser = "Module:template parser",
utilities = "Module:utilities",
debug = "Module:debug",
en_utilities = "Module:en-utilities",
parameter_utilities = "Module:parameter utilities",
parse_utilities = "Module:parse utilities",
template_styles = "Module:TemplateStyles",
script_utilities = "Module:script utilities",
JSON = "Module:JSON",
yesno = "Module:yesno",
},
loadData = {
headword_data = "Module:headword/data",
parameters_data = "Module:parameters/data",
text_allowed = "Module:etymon/data/text_allowed",
},
})
local Util = {}
function Util.format_error(message, preview_only)
if preview_only and not M.pages.is_preview() then
return nil
end
return '<span class="error">' .. message .. '</span>'
end
function Util.add_warning(message, preview_only)
local formatted = Util.format_error(message, preview_only)
if formatted then
table.insert(__state.warnings, formatted)
end
end
function Util.is_text_param_allowed_for_lang(lang)
if not lang or type(lang) ~= "table" then
return false
end
local types = lang.getTypes and lang:getTypes()
if types and types.family then
local code = lang.getCode and lang:getCode()
return code and M.text_allowed.families[code] == true
end
local full_code = lang.getFullCode and lang:getFullCode()
if full_code and M.text_allowed.langs[full_code] then
return true
end
if lang.inFamily then
for family_code in pairs(M.text_allowed.families) do
if lang:inFamily(family_code) then
return true
end
end
end
return false
end
function Util.get_lang(code, no_error)
if no_error then
return M.languages.getByCode(code, nil, true)
end
return M.languages.getByCode(code, nil, true) or M.languages_errorgetby.code(code, true, true)
end
-- Match a term language against a text=:lang stop target (supports etymology-only codes).
function Util.lang_matches_stop_code(term_lang, stop_code)
if not term_lang or not stop_code or stop_code == "" then
return false
end
local stop_lang = Util.get_lang(stop_code, true)
if not stop_lang then
return false
end
if term_lang:getCode() == stop_lang:getCode() then
return true
end
if stop_lang:getFullCode() == stop_lang:getCode() then
return term_lang:getFullCode() == stop_lang:getCode()
end
return false
end
function Util.get_family(code)
return M.families.getByCode(code)
end
function Util.get_lang_exception(lang)
-- Families have no language-specific exceptions
if lang.getTypes and lang:getTypes().family then
return nil
end
local code = lang:getCode()
local lang_exceptions = M.data.config.lang_exceptions
if lang_exceptions[code] then
return lang_exceptions[code]
end
for norm_code, exc in pairs(lang_exceptions) do
if exc.normalize_to and code == exc.normalize_to then
return exc
end
if exc.normalize_from_families then
local should_normalize = false
for _, family in ipairs(exc.normalize_from_families) do
if lang:inFamily(family) then
should_normalize = true
break
end
end
if should_normalize and exc.normalize_exclude_families then
for _, family in ipairs(exc.normalize_exclude_families) do
if lang:inFamily(family) then
should_normalize = false
break
end
end
end
if should_normalize then
local ret = {}
for k, v in pairs(exc) do
ret[k] = v
end
ret.suppress_tr = nil
return ret
end
end
end
return nil
end
function Util.get_norm_lang(lang)
local exc = Util.get_lang_exception(lang)
if exc and exc.normalize_to then
return M.languages.getByCode(exc.normalize_to)
end
return lang
end
function Util.resolve_context_lang(lang, node_args)
if type(node_args) ~= "table" then return lang end
if node_args.status == M.data.STATUS.INLINE then return lang end
if not (lang.hasType and lang:hasType("etymology-only")) then return lang end
local full = lang.getFull and lang:getFull()
if not full or full:getCode() == lang:getCode() then return lang end
if full.hasAncestor and full:hasAncestor(lang) then return lang end
return full
end
-- Add default values for boolean modifiers (e.g., <unc> becomes <unc:1>)
-- This is needed because Module:parse utilities expects boolean modifiers to have explicit values
function Util.add_boolean_defaults(str, param_mods)
local result = str
for name, spec in pairs(param_mods) do
if spec.type == "boolean" then
-- Replace <name> with <name:1> (but not <name:...> which already has a value)
result = result:gsub("<" .. name .. ">", "<" .. name .. ":1>")
end
end
return result
end
local REQUEST_TEMPLATE_PARAM_MODS = {
rfe = M.parameter_utilities.construct_param_mods {
{ param = { "sort", "y", "m", "fragment", "section" } },
{ param = { "nocat", "box", "noes" }, type = "boolean" },
},
etystub = M.parameter_utilities.construct_param_mods {
{ param = "sort" },
{ param = { "nocat", "nocap", "nodot" }, type = "boolean" },
},
}
function Util.expand_request_template(frame, template_name, param_value, lang_code)
local param_mods = REQUEST_TEMPLATE_PARAM_MODS[template_name]
local with_defaults = Util.add_boolean_defaults(param_value, param_mods)
local parsed = M.parse_utilities.parse_inline_modifiers(with_defaults, {
param_mods = param_mods,
generate_obj = function(text)
if M.yesno(text, false) then
return { is_boolean = true }
end
return { text = text }
end,
})
local template_args = { [1] = lang_code }
for name in pairs(param_mods) do
template_args[name] = parsed[name]
end
if not parsed.is_boolean then
template_args[2] = parsed.text
end
return " " .. frame:expandTemplate({
title = template_name,
args = template_args,
})
end
-- Centralized term formatting: handles suppress_term (-), unknown_term (empty/+), and regular terms
function Util.format_term(term, is_toplevel, opts)
opts = opts or {}
-- suppress_term (-) returns nil
if term.suppress_term then
return nil
end
local lang = term.lang
local exc = Util.get_lang_exception(lang)
if is_toplevel then
local display_text = term.alt or term.title or ""
local sc = term.sc or lang:findBestScript(display_text)
local bold_text = tostring(mw.html.create("strong")
:addClass("selflink")
:wikitext(display_text))
return M.script_utilities.tag_text(bold_text, lang, sc, "term")
end
local link_params = { lang = lang }
link_params.term = not term.unknown_term and term.title or nil
link_params.alt = term.alt
link_params.id = (not term.unknown_term and term.id and term.id ~= "") and term.id or nil
if not (exc and exc.suppress_tr) then
link_params.tr = term.tr
link_params.ts = term.ts
else
link_params.suppress_tr = true
end
link_params.lit = (opts.lit ~= "suppress") and term.lit or nil
if opts.gloss ~= "suppress" then
link_params.gloss = term.gloss
end
link_params.genders = term.genders
if opts.pos ~= "suppress" then
link_params.pos = term.pos
link_params.ng = term.ng
link_params.infl = term.infl
end
if exc and exc.suppress_tr then
link_params.lit = nil
end
if opts.tree_ql ~= "suppress" then
if term.q then
link_params.q = term.q
end
if term.qq then
link_params.qq = term.qq
end
if term.l then
link_params.l = term.l
end
if term.ll then
link_params.ll = term.ll
end
link_params.show_decorations = term.q or term.qq or term.l or term.ll
end
return M.links.full_link(link_params, "term")
end
local __is_content_page_cached
function Util.is_content_page()
if __is_content_page_cached == nil then
__is_content_page_cached = M.pages.is_content_page(mw.title.getCurrentTitle())
end
return __is_content_page_cached
end
local __page_data_cached
function Util.get_page_data()
if not __page_data_cached then
__page_data_cached = M.headword_data.page
end
return __page_data_cached
end
-- Extract base keyword from param (without modifiers)
local function get_keyword_base(param)
if type(param) ~= "string" then return nil end
local base = param:match("^:?([^<]+)") or param:gsub("^:", "")
return base
end
local function is_keyword(param, allow_colon_less)
if type(param) ~= "string" then return false end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
if allow_colon_less then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
return false
end
local function get_keyword(param, allow_colon_less)
if type(param) ~= "string" then return nil end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
return get_keyword_base(param)
end
if allow_colon_less then
local base = get_keyword_base(param)
if keywords[base] then
return base
end
end
return nil
end
local function normalize_keyword(keyword)
if keyword:sub(1, 1) == ":" then
return keyword
end
return ":" .. keyword
end
-- Resolve keyword (possibly an alias) to its canonical form. Used only at input boundaries
local function get_canonical_keyword(keyword)
if not keyword then return keyword end
return M.data.keyword_canonical[keyword] or keyword
end
local function is_affix_group_keyword(keyword)
local config = keyword and M.data.keywords[keyword]
return config and config.affix_categories or false
end
local function reject_removed_surf_keyword(param)
local base = get_keyword_base(param)
if base == "surf" then
error("The `:surf` keyword has been removed. Use `<surf>` on a formation keyword instead (e.g. `:af<surf>`, `:bor<surf>`).")
end
end
local function copy_keyword_info(source)
local copy = {}
for k, v in pairs(source) do
copy[k] = v
end
return copy
end
local function lowercase_glossary_display(text)
return text:gsub("(%[%[Appendix:Glossary#[^|]+|)([^%]])([^%]]*)%]%]", function(prefix, first, rest)
return prefix .. mw.ustring.lower(first) .. rest .. "]]"
end)
end
local function surf_should_keep_formation_phrase(base)
if not base.phrase then
return false
end
if base.glossary then
return true
end
return not (base.phrase == "from" and (base.text == "From" or base.text == "from"))
end
-- Runtime overrides when <surf> is present on a keyword.
local function get_effective_keyword_info(keyword, modifiers)
local base = M.data.keywords[keyword]
if not base or not modifiers or not modifiers.surf then
return base
end
local effective = copy_keyword_info(base)
local surf_text = "By [[Appendix:Glossary#surface_analysis|surface analysis]],"
local surf_phrase = "by surface analysis,"
effective.new_sentence = true
effective.invisible = "tree"
if surf_should_keep_formation_phrase(base) then
effective.phrase = surf_phrase .. " " .. base.phrase
if base.text then
effective.text = surf_text .. " " .. lowercase_glossary_display(base.text)
else
effective.text = surf_text .. " " .. base.phrase
end
else
effective.text = surf_text
effective.phrase = surf_phrase
end
return effective
end
-- Build text/phrase for nominalization with <g:code> (uses data module for codes only).
local function get_nominalization_label_for_g(code)
if not code or code == "" then return nil end
local codes = M.data.nominalization_g_codes
local adj = codes[code]
if not adj and #code == 2 then
local gender_adj = codes[code:sub(1, 1)]
local number_adj = codes[code:sub(2, 2)]
if gender_adj and number_adj then
adj = gender_adj .. " " .. number_adj
end
end
if not adj then return nil end
local text = adj:gsub("^%l", function(c) return string.upper(c) end) .. " [[Appendix:Glossary#nominalization|nominalization]] of"
local phrase = M.en_utilities.add_indefinite_article(adj .. " [[Appendix:Glossary#nominalization|nominalization]] of", false)
return { text = text, phrase = phrase }
end
local EtymonParser = {}
-- Keyword modifier definitions
EtymonParser.keyword_param_mods = M.parameter_utilities.construct_param_mods {
{ group = "ref" },
{ param = "conj" }, -- conjunction for alternatives: "and", "or", "and/or", etc.
{ param = { "unc", "surf" }, type = "boolean" },
{ param = "text", restrict = { keywords = { "from", "derived" } } },
{ param = "lit", restrict = { affix_group = true } },
{ param = "g", restrict = { keywords = { "nominalization" } } },
{ param = "senseid", restrict = { keywords = { "semantic loan" } } },
}
-- Term modifier definitions
EtymonParser.etymon_param_mods = M.parameter_utilities.construct_param_mods {
{group = {"link", "q", "l", "ref", "infl"}, exclude = {"sc"}},
{param = {"ety", "postype"}},
{param = "unc", type = "boolean"},
{param = "aftype", restrict = {affix_group = true}},
{param = {"bor", "slbor", "lbor"}, type = "boolean", restrict = {affix_group = true}},
}
local function get_clean_param_mods(param_mods)
local clean = {}
for mod_name, mod_def in pairs(param_mods) do
clean[mod_name] = {}
for key, value in pairs(mod_def) do
if key ~= "restrict" then
clean[mod_name][key] = value
end
end
end
return clean
end
function EtymonParser.check_modifier_restrictions(modifiers, current_keyword, param_mods)
for mod_name, mod_value in pairs(modifiers) do
-- Only check restrictions if the modifier has a non-false/nil value
if mod_value then
local mod_def = param_mods[mod_name]
if mod_def and mod_def.restrict then
if mod_def.restrict.affix_group then
if not is_affix_group_keyword(current_keyword) then
local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">"
error("The modifier `" .. mod_display .. "` is only allowed for affix-group keywords (e.g. `:af`, `:blend`, `:univ`).")
end
elseif mod_def.restrict.keywords then
local allowed_keywords = mod_def.restrict.keywords
local is_allowed = false
for _, allowed_keyword in ipairs(allowed_keywords) do
if current_keyword == allowed_keyword then
is_allowed = true
break
end
end
if not is_allowed then
local keyword_list = {}
for _, kw in ipairs(allowed_keywords) do
table.insert(keyword_list, ":" .. kw)
end
local keyword_str = table.concat(keyword_list, #keyword_list == 2 and " or " or ", ")
if #keyword_list > 2 then
-- Replace last comma with "or"
keyword_str = keyword_str:gsub(", ([^,]+)$", " or %1")
end
local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">"
error("The modifier `" .. mod_display .. "` is only allowed for the keyword" .. (#keyword_list > 1 and "s " or " ") .. keyword_str .. ".")
end
end
end
end
end
end
local TERM_RULE_DISALLOW = {
suppress = { field = "suppress_term", label = "suppressed" },
unknown = { field = "unknown_term", label = "unknown" },
family = { field = "is_family", label = "family" },
}
function EtymonParser.check_etymon_limits(count, limits, label, opts)
if not limits then
return
end
opts = opts or {}
local min_etymons = limits.min_etymons
if min_etymons == nil and not opts.skip_default_min then
min_etymons = 1
end
if min_etymons and count < min_etymons then
if min_etymons > 1 then
error("Detected " .. label .. " group with fewer than " .. min_etymons .. " etymons.")
else
error("Detected " .. label .. " with no etymons.")
end
end
if limits.max_etymons and count > limits.max_etymons then
local unit = (limits.max_etymons == 1) and "etymon" or "etymons"
error("Detected " .. label .. " with more than " .. limits.max_etymons .. " " .. unit .. ".")
end
end
function EtymonParser.check_term_rules(etymon_data, entry_lang, rules, label)
label = label or "term"
if rules and rules.disallow then
local disallowed = {}
for _, typ in ipairs(rules.disallow) do
local spec = TERM_RULE_DISALLOW[typ]
if spec and etymon_data[spec.field] then
table.insert(disallowed, spec.label)
end
end
if #disallowed > 0 then
error(label .. " does not support " ..
mw.text.listToText(disallowed, "or") .. " etymons.")
end
end
if etymon_data.is_family then
if rules and rules.family == "disallowed" then
error(label .. " does not support family codes" .. (rules.family_suffix or "."))
elseif not etymon_data.suppress_term then
error("Family codes require suppressed term (use family:-).")
end
end
if rules then
if rules.require_term and (not etymon_data.term or etymon_data.term == "") then
error(label .. " requires a term for each listed form.")
end
if rules.entry_lang then
if Util.get_norm_lang(etymon_data.lang):getFullCode() ~=
Util.get_norm_lang(entry_lang):getFullCode() then
error(label .. " terms must be in the entry language (" ..
entry_lang:getFullCode() .. "), got '" .. etymon_data.lang:getFullCode() .. "'.")
end
end
if rules.ancestor_check then
M.etymology.check_ancestor(entry_lang, etymon_data.lang)
end
elseif etymon_data.is_family and not etymon_data.suppress_term then
error("Family codes require suppressed term (use family:-).")
end
end
function EtymonParser.check_keyword_term(etymon_data, entry_lang, keyword)
local config = M.data.keywords[keyword]
EtymonParser.check_term_rules(etymon_data, entry_lang, config and config.term_rules, "`:" .. keyword .. "`")
end
function EtymonParser.check_supplement_term(etymon_data, entry_lang, supplement_type)
local config = M.data.supplements[supplement_type]
EtymonParser.check_term_rules(etymon_data, entry_lang, config and config.term_rules, "|" .. supplement_type .. "=")
end
-- Parse keyword with modifiers (e.g., ":bor<unc>" or ":bor<ref:{{R:example}}>")
function EtymonParser.parse_keyword_modifiers(param)
if type(param) ~= "string" then return nil, {} end
local base_keyword = get_keyword_base(param)
if not base_keyword then return nil, {} end
local canonical_keyword = get_canonical_keyword(base_keyword)
-- Check if there are any modifiers
if not param:find("<", 1, true) then
return canonical_keyword, {}
end
-- Parse modifiers using the same mechanism as etymon parsing
local rest_with_defaults = Util.add_boolean_defaults(param, EtymonParser.keyword_param_mods)
local function generate_obj(ignored)
return {}
end
local parsed = M.parse_utilities.parse_inline_modifiers(rest_with_defaults:gsub("^:?[^<]+", ""),
{ param_mods = get_clean_param_mods(EtymonParser.keyword_param_mods), generate_obj = generate_obj })
local modifiers = {
unc = parsed.unc or false,
refs = parsed.refs,
text = parsed.text,
lit = parsed.lit,
conj = parsed.conj,
g = parsed.g,
surf = parsed.surf or false,
senseid = parsed.senseid,
}
-- Validate modifiers against restrictions
EtymonParser.check_modifier_restrictions(modifiers, canonical_keyword, EtymonParser.keyword_param_mods)
return canonical_keyword, modifiers
end
local function normalize_keyword_param(keyword_with_mods)
local trimmed = M.string_utilities.trim(keyword_with_mods)
reject_removed_surf_keyword(trimmed:match("^:") and trimmed or (":" .. trimmed))
local base = get_keyword_base(trimmed)
if not base or not M.data.keywords[base] then
error("Invalid keyword '" .. trimmed .. "' in inline etymology")
end
local canonical_base = get_canonical_keyword(base)
local without_colon = trimmed:gsub("^:", "")
local mods_part = without_colon:sub(#base + 1)
local kw_param = normalize_keyword(canonical_base .. mods_part)
EtymonParser.parse_keyword_modifiers(kw_param)
return kw_param
end
local function get_keyword_mod_names()
local names = {}
for mod_name in pairs(EtymonParser.keyword_param_mods) do
names[mod_name] = true
end
return names
end
local function parse_inline_ety_run(ety_string)
local body = ety_string or ""
if body == "" then
error("Empty inline etymology")
end
local keyword_mod_names = get_keyword_mod_names()
local pos = 1
local len = #body
local function parse_err(msg)
error(msg .. " in inline etymology: '" .. body .. "'")
end
local function peek_double()
return body:sub(pos, pos + 1) == "<<"
end
local function mod_name_from_unwrapped(unwrapped)
return unwrapped:match("^<([^:>]+)")
end
local function is_keyword_mod(unwrapped)
local name = mod_name_from_unwrapped(unwrapped)
return name and keyword_mod_names[name] or false
end
local function read_double_bracket()
if not peek_double() then
return nil
end
local start = pos
pos = pos + 2
while pos <= len - 1 do
if body:sub(pos, pos + 1) == ">>" then
local token = body:sub(start, pos + 1)
pos = pos + 2
return token, token:sub(2, -2)
end
pos = pos + 1
end
parse_err("Unmatched <<")
end
local function read_angle_cell()
if body:sub(pos, pos) ~= "<" or peek_double() then
return nil
end
local open = pos
pos = pos + 1
local depth = 1
local i = pos
while i <= len do
local ch = body:sub(i, i)
if ch == "<" then
depth = depth + 1
elseif ch == ">" then
depth = depth - 1
if depth == 0 then
local inner = body:sub(open + 1, i - 1)
pos = i + 1
return inner
end
end
i = i + 1
end
parse_err("Unmatched <")
end
local function read_bare_run()
local start = pos
while pos <= len and body:sub(pos, pos) ~= "<" do
pos = pos + 1
end
return body:sub(start, pos - 1)
end
local function absorb_double_keyword_mods(keyword_str)
while peek_double() do
local saved = pos
local _, unwrapped = read_double_bracket()
if is_keyword_mod(unwrapped) then
keyword_str = keyword_str .. unwrapped
else
pos = saved
break
end
end
return keyword_str
end
local kw_start = pos
while pos <= len and body:sub(pos, pos) ~= "<" do
pos = pos + 1
end
local keyword = body:sub(kw_start, pos - 1)
if keyword:match("^%s*$") then
parse_err("Missing keyword")
end
keyword = absorb_double_keyword_mods(keyword)
local cells = {}
while pos <= len do
if peek_double() then
local _, unwrapped = read_double_bracket()
if is_keyword_mod(unwrapped) then
parse_err("Unexpected keyword modifier " .. unwrapped .. " outside of a keyword")
end
table.insert(cells, "+" .. unwrapped)
elseif body:sub(pos, pos) == "<" then
local inner = read_angle_cell()
if inner ~= "" then
table.insert(cells, inner)
end
else
local bare = read_bare_run()
if bare ~= "" then
if bare:sub(1, 1) ~= ":" then
parse_err("Unexpected bare text '" .. bare .. "' (use :keyword for nested keywords in inline etymology)")
end
if not is_keyword(bare, true) then
parse_err("Invalid keyword '" .. bare .. "' in inline etymology")
end
table.insert(cells, absorb_double_keyword_mods(bare))
end
end
end
return {
keyword = keyword,
cells = cells,
}
end
function EtymonParser.inline_ety_to_pipe(ety_string)
local run = parse_inline_ety_run(ety_string)
if not run.keyword or run.keyword:match("^%s*$") then
return "|"
end
local pipe_parts = { normalize_keyword_param(M.string_utilities.trim(run.keyword)) }
for _, segment in ipairs(run.cells) do
if is_keyword(segment, true) then
table.insert(pipe_parts, normalize_keyword_param(segment))
else
table.insert(pipe_parts, segment)
end
end
return "|" .. table.concat(pipe_parts, "|") .. "|"
end
function EtymonParser.pipe_to_inline_ety(pipe_string)
local cells = {}
for cell in pipe_string:gmatch("([^|]+)") do
if cell ~= "" then
table.insert(cells, cell)
end
end
if #cells == 0 then
return ""
end
local inline_parts = {}
for index, cell in ipairs(cells) do
local base = get_keyword_base(cell)
if base and M.data.keywords[base] then
local without_colon = cell:gsub("^:", "")
local kw_base, mods = without_colon:match("^([^<]+)(.*)$")
local inline_kw = (kw_base or without_colon) .. (mods or ""):gsub("<([^>]+)>", "<<%1>>")
if index > 1 then
inline_kw = ":" .. inline_kw
end
table.insert(inline_parts, inline_kw)
elseif cell:sub(1, 1) == "+" then
local mod = cell:sub(2)
if mod:match("^<.->$") then
mod = mod:sub(2, -2)
end
table.insert(inline_parts, "<<" .. mod .. ">>")
else
table.insert(inline_parts, "<" .. cell .. ">")
end
end
return table.concat(inline_parts, "")
end
function EtymonParser.parse_inline_ety(ety_string, context_lang)
local run = parse_inline_ety_run(ety_string)
local keyword = M.string_utilities.trim(run.keyword)
reject_removed_surf_keyword(":" .. keyword)
if not is_keyword(keyword, true) then
error("Invalid keyword '" .. keyword .. "' in inline etymology <ety:" .. keyword .. "...>")
end
local args = { context_lang:getCode(), normalize_keyword_param(keyword) }
for _, segment in ipairs(run.cells) do
if is_keyword(segment, true) then
table.insert(args, normalize_keyword_param(segment))
else
table.insert(args, segment)
end
end
return args
end
function EtymonParser.parse_etymon(param, context_lang)
if is_keyword(param) then
return nil
end
if type(param) ~= "string" then
return nil
end
local lang, rest
local is_family = false
local before_bracket = param:match("^([^<]*)") or param
local lang_code, rest_match = before_bracket:match("^([a-zA-Z][a-zA-Z0-9._-]*):(.*)$")
if lang_code then
local potential_lang = Util.get_lang(lang_code, true)
if potential_lang then
lang = potential_lang
rest = param:sub(#lang_code + 2)
else
local potential_family = Util.get_family(lang_code)
if potential_family then
lang = potential_family
rest = param:sub(#lang_code + 2)
is_family = true
else
lang = context_lang
rest = param
end
end
else
lang = context_lang
rest = param
end
M.tracking.track_term(rest)
if rest == "" or rest == "+" then
return {
lang = lang,
term = nil,
unknown_term = true,
is_family = is_family,
}
end
if rest == "-" then
return {
lang = lang,
term = nil,
suppress_term = true,
is_family = is_family,
}
end
if not rest:find("<", 1, true) then
return {
lang = lang,
term = M.string_utilities.trim(rest),
is_family = is_family,
}
end
local term_text = rest:match("^([^<]*)") or ""
local is_unknown = (term_text == "" or term_text == "+")
local is_suppress = (term_text == "-")
local function generate_obj(ignored_term)
return { term = (is_unknown or is_suppress) and nil or M.string_utilities.trim(term_text) }
end
local rest_with_defaults = Util.add_boolean_defaults(rest, EtymonParser.etymon_param_mods)
local parsed_obj = M.parse_utilities.parse_inline_modifiers(rest_with_defaults,
{ param_mods = get_clean_param_mods(EtymonParser.etymon_param_mods), generate_obj = generate_obj })
if parsed_obj.id and parsed_obj.id:match("^!") then
parsed_obj.id = parsed_obj.id:sub(2)
parsed_obj.override = true
end
parsed_obj.lang = lang
parsed_obj.is_family = is_family
if is_unknown then
parsed_obj.unknown_term = true
elseif is_suppress then
parsed_obj.suppress_term = true
end
return parsed_obj
end
function EtymonParser.validate(lang, args, id, title, pos, starts_with_lang_code)
-- id is now optional, so only validate if provided
if id then
if mw.ustring.len(id) < 2 then
error("The `id` parameter must have at least two characters.")
end
if id == title or id == Util.get_page_data().pagename then
error("The `id` parameter must not be the same as the page title.")
end
end
local valid_pos = { prefix = true, suffix = true, interfix = true, infix = true, root = true, word = true }
if pos and not valid_pos[pos] then
error("Unknown value provided for `pos`. Valid values: " .. table.concat(require("Module:table").keysToList(valid_pos), ", ") .. ".")
end
local current_keyword = "from"
local current_keyword_explicit = false
local keyword_etymons = {}
local keywords = M.data.keywords
local function checkKeyword()
local config = keywords[current_keyword]
if current_keyword == "from" and not current_keyword_explicit and #keyword_etymons == 0 then
keyword_etymons = {}
return
end
EtymonParser.check_etymon_limits(#keyword_etymons, config, "`:" .. current_keyword .. "`")
keyword_etymons = {}
end
local start_index = starts_with_lang_code and 2 or 1
for i = start_index, #args do
local param = args[i]
if type(param) ~= "string" then
elseif param:sub(1, 1) == ":" and not is_keyword(param) then
reject_removed_surf_keyword(param)
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif is_keyword(param) then
checkKeyword()
current_keyword = get_canonical_keyword(get_keyword(param))
current_keyword_explicit = true
else
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
table.insert(keyword_etymons, param)
EtymonParser.check_keyword_term(etymon_data, lang, current_keyword)
-- Check modifier restrictions
EtymonParser.check_modifier_restrictions(etymon_data, current_keyword, EtymonParser.etymon_param_mods)
-- postype must be "root" or "word"
local VALID_POSTYPES = { root = true, word = true }
if etymon_data.postype and not VALID_POSTYPES[etymon_data.postype] then
error("Invalid <postype:" .. etymon_data.postype .. ">; must be \"root\" or \"word\".")
end
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
EtymonParser.validate(etymon_data.lang, inline_args, nil, nil, nil, true)
end
else
table.insert(keyword_etymons, param)
end
end
end
checkKeyword()
end
local DataRetriever = {}
local function format_etymon_id_hint(id_data, idx)
local id = type(id_data) == "table" and id_data.id or id_data
local pos = type(id_data) == "table" and id_data.pos
if id and id ~= "" and id ~= "*" then
return '"' .. id .. '"'
end
if pos and pos ~= "" then
return "unnamed (|pos=" .. pos .. "|)"
end
return "etymon #" .. idx .. " (no |id= on page)"
end
local function etymon_target_page_link(page, norm_lang)
return M.links.full_link({
term = page,
lang = norm_lang,
no_generate_alternants = true,
}, "term")
end
-- Summarize {{etymon}} id slots on a linked page for preview warnings.
local function summarize_available_etymon_ids(ids)
local id_list = {}
local all_idless = true
local target_has_idless = false
local any_pos = false
for i, id_data in ipairs(ids) do
local id = type(id_data) == "table" and id_data.id or id_data
local pos = type(id_data) == "table" and id_data.pos
if id and id ~= "" and id ~= "*" then
all_idless = false
else
target_has_idless = true
end
if pos and pos ~= "" then
any_pos = true
end
table.insert(id_list, format_etymon_id_hint(id_data, i))
end
return {
id_list = id_list,
all_idless = all_idless,
target_has_idless = target_has_idless,
any_pos = any_pos,
count = #ids,
options_text = mw.text.listToText(id_list),
}
end
local function ambiguous_etymon_suggestion(page_link, summary)
if summary.all_idless then
if summary.any_pos then
return " None set `|id=` yet; add a unique `|id=` to each on " .. page_link
.. ", then `<id:identifier>` after the term here. Section order / hints: "
.. summary.options_text .. "."
end
return " None set `|id=` yet; add a unique `|id=` to each {{etymon}} in that section from top to bottom, then `<id:identifier>` after the term here (same value as `|id=`)."
end
return " Specify which one with `<id:identifier>` after the term. Options: " .. summary.options_text .. "."
end
local function warn_ambiguous_etymon_link(page, norm_lang, ids, is_toplevel)
local page_link = etymon_target_page_link(page, norm_lang)
local summary = summarize_available_etymon_ids(ids)
if is_toplevel and summary.target_has_idless then
__state.linked_page_multiple_etymons_idless = true
end
local lang_name = norm_lang:getCanonicalName()
local lead = "Etymology link to " .. page_link .. " is ambiguous (" .. summary.count
.. " {{etymon}} templates for " .. lang_name .. ")."
Util.add_warning(lead .. ambiguous_etymon_suggestion(page_link, summary), true)
end
local function is_mismatched_explicit_id(base_key, cached_args, parent_etymon)
return cached_args == M.data.STATUS.MISSING and not parent_etymon
and #(__state.available_etymon_ids[base_key] or {}) > 0
end
local function maybe_flag_partial_etymology_reference(base_key, etymon_data, cached_args, is_toplevel)
if not is_toplevel or __state.skip_partial_etymology_category then
return
end
if not __state.partial_etymology_targets[base_key] then
return
end
if etymon_data.id and type(cached_args) == "table" then
return
end
__state.linked_page_partial_etymology_sections = true
end
local function is_nonlemma_etymon_template(template_args)
return template_args and M.yesno(template_args.nl, false)
end
local function warn_mismatched_explicit_id(page, norm_lang, base_key, etymon_id)
local page_link = etymon_target_page_link(page, norm_lang)
local summary = summarize_available_etymon_ids(__state.available_etymon_ids[base_key] or {})
local lang_name = norm_lang:getCanonicalName()
local lead = "Etymology link to " .. page_link .. " uses `<id:" .. etymon_id
.. ">`, but no {{etymon}} on that page has `|id=" .. etymon_id .. "|` for " .. lang_name .. "."
Util.add_warning(lead .. " Valid IDs: " .. summary.options_text .. ".", true)
end
-- Given an etymon data, scrape its page and cache the result in the global state object.
function DataRetriever.cache_page_etymons(etymon_page, etymon_title, key, etymon_lang, etymon_id, redirected_from, descendants_is_toplevel)
local content = etymon_title:getContent()
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.REDLINK
return
end
-- Check if the linked page is a redirect. If it is, the template parsing
-- code below will be effectively skipped, and `scrape_page` will be called
-- again on the redirect target (see the bottom of this function)
local lang_section_for_descendants = nil
local redirect_target = etymon_title.redirect_target
if not redirect_target then
content = M.pages.get_section(content, etymon_lang:getFullName(), 2)
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
return
end
lang_section_for_descendants = content
end
local etymon_lang_code = etymon_lang:getFullCode()
local lang_page_key = etymon_lang_code .. ":" .. etymon_page
local found_templates_for_lang = {}
local found_ids = {}
local get_node_class = M.template_parser.class_else_type
-- Look for all {{etymon}} templates within the page content using the template parser
-- This way the same page is never parsed more than once
-- Build a map from senseids to their parent etymonids.
local active_etymon_args = nil
local etymology_section_count = 0
local etymology_sections_with_etymon = 0
local current_etymology_has_etymon = false
local current_etymology_has_nonlemma = false
local function finalize_current_etymology_section()
if etymology_section_count == 0 then
return
end
if current_etymology_has_etymon or current_etymology_has_nonlemma then
etymology_sections_with_etymon = etymology_sections_with_etymon + 1
end
current_etymology_has_etymon = false
current_etymology_has_nonlemma = false
end
for node in M.template_parser.parse(content):iterate_nodes() do
local node_class = get_node_class(node)
if node_class == "heading" then
-- A new L2 or etymology section acts as a barrier: an {{etymon}} usage
-- used previously cannot be the parent of any subsequent senseids.
-- Note that we don't have to check for L2s due to the usage of `M.pages.get_section` above.
if node:get_name():find("^Etymology") then
finalize_current_etymology_section()
etymology_section_count = etymology_section_count + 1
active_etymon_args = nil
end
elseif node_class == "template" then
local template_name = node:get_name()
if template_name == "etymon" then
local template_args = node:get_arguments()
-- Check if this etymon is for our language
if template_args[1] == etymon_lang_code then
if is_nonlemma_etymon_template(template_args) then
if etymology_section_count > 0 then
current_etymology_has_nonlemma = true
end
else
if etymology_section_count > 0 then
current_etymology_has_etymon = true
end
table.insert(found_templates_for_lang, template_args)
if template_args.id then
local etymon_key = lang_page_key .. ":" .. template_args.id
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, template_args.id)
active_etymon_args = template_args
else
-- Store idless etymon with default key
local etymon_key = lang_page_key .. ":*"
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, "*")
active_etymon_args = template_args
end
end
end
elseif active_etymon_args and template_name == "senseid" then
local template_args = node:get_arguments()
-- This should always be true for proper usages of {{senseid}}.
if template_args[1] == etymon_lang_code and template_args[2] then
local sense_id_key = lang_page_key .. ":" .. template_args[2]
__state.senseid_parent_etymon[sense_id_key] = active_etymon_args
__state.cached_etymon_pages[sense_id_key] = tostring(etymon_page)
end
end
end
end
finalize_current_etymology_section()
if lang_section_for_descendants
and etymology_section_count > 1
and etymology_sections_with_etymon > 0
and etymology_sections_with_etymon < etymology_section_count
then
__state.partial_etymology_targets[lang_page_key] = true
end
if descendants_is_toplevel and lang_section_for_descendants and #found_templates_for_lang > 0 then
M.descendants.cache_page_checks({
lang_section = lang_section_for_descendants,
etymon_lang_code = etymon_lang_code,
found_templates_for_lang = found_templates_for_lang,
entry_title = __state.entry_title,
entry_lang_code = __state.entry_lang_code,
entry_lang = __state.entry_lang_code and Util.get_lang(__state.entry_lang_code, true) or nil,
cached_descendants_checks = __state.cached_descendants_checks,
lang_page_key = lang_page_key,
redirected_from = redirected_from,
})
end
local id_data_list = {}
for _, args in ipairs(found_templates_for_lang) do
local id = args.id or "*"
table.insert(id_data_list, { id = id, pos = args.pos })
end
__state.available_etymon_ids[lang_page_key] = id_data_list
if #found_templates_for_lang == 1 then
__state.single_etymons[lang_page_key] = found_templates_for_lang[1]
end
if redirected_from and __state.available_etymon_ids[lang_page_key] then
__state.available_etymon_ids[redirected_from] = __state.available_etymon_ids[redirected_from] or {}
for _, id_data in ipairs(__state.available_etymon_ids[lang_page_key]) do
table.insert(__state.available_etymon_ids[redirected_from], id_data)
end
end
if __state.cached_etymon_args[key] ~= nil or __state.senseid_parent_etymon[key] ~= nil then
-- All done!
return
elseif redirect_target and not redirected_from then
-- Try scraping the redirect.
etymon_page = redirect_target.prefixedText
DataRetriever.cache_page_etymons(etymon_page, redirect_target, lang_page_key .. ":" .. etymon_id, etymon_lang, etymon_id, lang_page_key, descendants_is_toplevel)
__state.cached_etymon_args[key] = __state.cached_etymon_args[etymon_lang_code .. ":" .. etymon_page .. ":" .. etymon_id]
else
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
end
end
local function has_linkable_term(etymon_data)
if etymon_data.is_family or etymon_data.suppress_term or etymon_data.unknown_term then
return false
end
local term = etymon_data.term
if term == nil or term == "" then
return false
end
return M.string_utilities.trim(term) ~= ""
end
local function record_term_id_tracking(etymon_data)
if not has_linkable_term(etymon_data) then
return
end
local term_page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
M.tracking.record_term_id_usage(__state.id_stats, etymon_data, term_page)
end
-- Given an etymon object, scrape its page (if necessary) and return its own etymon arguments as well as the page name.
function DataRetriever.get_etymon_args(etymon_data, is_toplevel)
if not has_linkable_term(etymon_data) then
return M.data.STATUS.MISSING, nil, nil, nil
end
local page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
local norm_lang = Util.get_norm_lang(etymon_data.lang)
local base_key = norm_lang:getFullCode() .. ":" .. page
if etymon_data.id then
local key = base_key .. ":" .. etymon_data.id
local cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key]
if cached_args == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, is_toplevel)
end
cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key] -- refresh
-- Get etymon_id from parent if this was resolved via senseid
local parent_etymon = __state.senseid_parent_etymon[key]
local resolved_etymon_id = parent_etymon and parent_etymon.id
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
end
end
local mismatched_id = is_mismatched_explicit_id(base_key, cached_args, parent_etymon)
if mismatched_id and is_toplevel then
__state.has_mismatched_id = true
M.tracking.record_mismatched_id_usage(__state.id_stats, norm_lang, page, etymon_data.id)
warn_mismatched_explicit_id(page, norm_lang, base_key, etymon_data.id)
end
maybe_flag_partial_etymology_reference(base_key, etymon_data, cached_args, is_toplevel)
return cached_args, __state.cached_etymon_pages[key], resolved_etymon_id, descendants_check
else
__state.used_idless_etymon = true
if is_toplevel then
__state.toplevel_idless_etymon = true
end
if __state.available_etymon_ids[base_key] == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, is_toplevel)
end
local ids = __state.available_etymon_ids[base_key] or {}
local count = #ids
-- Try to filter by postype if available and we have multiple candidates
if count > 1 and etymon_data.postype then
local matching_ids = {}
for _, id_data in ipairs(ids) do
if id_data.pos == etymon_data.postype then
table.insert(matching_ids, id_data)
end
end
if #matching_ids == 1 then
local matched_id = matching_ids[1].id
local matched_key = base_key .. ":" .. matched_id
M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "postype")
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id = matched_id },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id = matched_id },
})
end
end
local matched_args = __state.cached_etymon_args[matched_key]
maybe_flag_partial_etymology_reference(base_key, etymon_data, matched_args, is_toplevel)
return matched_args, __state.cached_etymon_pages[matched_key], nil, descendants_check
end
end
if count == 1 then
local only_id_data = ids[1]
local only_id = (type(only_id_data) == "table" and only_id_data.id) or only_id_data or "*"
M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "single")
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id_data = only_id_data },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id_data = only_id_data },
})
end
end
local single_args = __state.single_etymons[base_key]
maybe_flag_partial_etymology_reference(base_key, etymon_data, single_args, is_toplevel)
return single_args, __state.cached_etymon_pages[base_key .. ":" .. only_id], nil, descendants_check
elseif count > 1 then
M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "ambiguous")
warn_ambiguous_etymon_link(page, norm_lang, ids, is_toplevel)
maybe_flag_partial_etymology_reference(base_key, etymon_data, M.data.STATUS.AMBIGUOUS, is_toplevel)
return M.data.STATUS.AMBIGUOUS, nil, nil, nil
else
M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "missing")
maybe_flag_partial_etymology_reference(base_key, etymon_data, M.data.STATUS.MISSING, is_toplevel)
return M.data.STATUS.MISSING, nil, nil, nil
end
end
end
local function keyword_invisible_in_tree(keyword_info)
if not keyword_info then
return false
end
local inv = keyword_info.invisible
return inv == "all" or inv == true or inv == "tree"
end
-- True when the node has at least one top-level child container visible in the tree.
local function node_has_visible_tree_children(node)
for _, container in ipairs(node.children or {}) do
if not keyword_invisible_in_tree(container.keyword_info) then
return true
end
end
return false
end
-- Count visible term nodes in the tree.
local function get_visible_tree_depth(node, skip_child_rendering)
local max_depth = 1
if skip_child_rendering or not node then
return max_depth
end
for _, container in ipairs(node.children or {}) do
local keyword_info = container.keyword_info
if not keyword_invisible_in_tree(keyword_info) then
local skip_grandchildren = keyword_info and keyword_info.no_child_categories
for _, term in ipairs(container.terms or {}) do
if term.is_duplicate then
if term.original_has_children then
max_depth = math.max(max_depth, 2)
end
else
max_depth = math.max(max_depth, 1 + get_visible_tree_depth(term, skip_grandchildren))
end
end
end
end
return max_depth
end
local function as_param_list(val)
if val == nil then
return {}
end
if type(val) == "table" then
return val
end
if type(val) == "string" and val ~= "" then
return { val }
end
return {}
end
local TreeBuilder = {}
-- Build a unique key for deduplication in the seen table
function TreeBuilder.build_key(lang, title, args)
local norm_lang_code = Util.get_norm_lang(lang):getFullCode()
local is_table = type(args) == "table"
local id = (is_table and args.id) or ""
if title then
return norm_lang_code .. ":" .. M.links.get_link_page(title, lang) .. ":" .. id
end
if is_table and args.status == M.data.STATUS.INLINE then
local content_parts = {}
for i = 1, #args do
content_parts[i] = tostring(args[i])
end
return norm_lang_code .. ":*:" .. id .. "\0" .. table.concat(content_parts, "\0")
end
return norm_lang_code .. ":*:" .. id
end
-- Copy parsed etymon modifiers onto a tree/supplement term node.
function TreeBuilder.apply_etymon_fields(term, etymon_data)
term.id = etymon_data.id
term.gloss = etymon_data.gloss
term.tr = etymon_data.tr
term.ts = etymon_data.ts
term.alt = etymon_data.alt
term.genders = etymon_data.genders
term.pos = etymon_data.pos
term.ng = etymon_data.ng
term.infl = etymon_data.infl
term.refs = etymon_data.refs
term.is_uncertain = etymon_data.unc
term.lit = etymon_data.lit
term.q = etymon_data.q
term.qq = etymon_data.qq
term.l = etymon_data.l
term.ll = etymon_data.ll
term.suppress_term = etymon_data.suppress_term
term.unknown_term = etymon_data.unknown_term
term.is_family = etymon_data.is_family
term.override = etymon_data.override
term.aftype = etymon_data.aftype
term.postype = etymon_data.postype
term.bor = etymon_data.bor
term.lbor = etymon_data.lbor
term.slbor = etymon_data.slbor
end
function TreeBuilder.build_supplement_term(etymon_data, entry_lang, supplement_type)
EtymonParser.check_supplement_term(etymon_data, entry_lang, supplement_type)
local term = {
lang = etymon_data.lang,
title = etymon_data.term,
children = {},
status = M.data.STATUS.OK,
}
TreeBuilder.apply_etymon_fields(term, etymon_data)
return term
end
function TreeBuilder.build_supplement_terms(entry_lang, supplement_type, param_value)
local terms = {}
for _, term_param in ipairs(as_param_list(param_value)) do
if type(term_param) == "string" and term_param ~= "" then
local etymon_data = EtymonParser.parse_etymon(term_param, entry_lang)
if etymon_data then
table.insert(terms, TreeBuilder.build_supplement_term(etymon_data, entry_lang, supplement_type))
end
end
end
return terms
end
-- Attach a |param= supplement defined in etymon_data.supplements (e.g. doublet=).
function TreeBuilder.append_term_supplement(data_tree, entry_lang, supplement_type, param_value)
local config = M.data.supplements[supplement_type]
if not config then
error("Unknown supplement '" .. tostring(supplement_type) .. "'.")
end
local terms = TreeBuilder.build_supplement_terms(entry_lang, supplement_type, param_value)
if #terms == 0 then
return
end
data_tree.supplements = data_tree.supplements or {}
table.insert(data_tree.supplements, {
type = supplement_type,
config = config,
terms = terms,
})
M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, supplement_type, entry_lang, entry_lang, true)
end
function TreeBuilder.build(lang, title, args, seen, depth, stop_recursion)
seen = seen or {}
depth = depth or 0
local is_toplevel = (depth == 0)
if depth > __state.max_depth_reached then
__state.max_depth_reached = depth
end
__state.total_nodes = __state.total_nodes + 1
local lang_code = lang:getCode()
__state.language_count[lang_code] = (__state.language_count[lang_code] or 0) + 1
local current_id = (type(args) == "table" and args.id) or ""
local key = TreeBuilder.build_key(lang, title, args)
local node = { lang = lang, title = title, id = current_id, args = args, children = {}, status = M.data.STATUS.OK }
if type(args) ~= "table" or seen[key] then
node.status = args or M.data.STATUS.MISSING
-- Mark as duplicate if we've seen this node before
if seen[key] then
node.is_duplicate = true
node.duplicate_key = key
local original_node = seen[key]
if type(original_node) == "table" and original_node.children and #original_node.children > 0 then
node.original_has_children = true
end
end
return node
end
node.status = args.status or M.data.STATUS.OK
seen[key] = node
-- If stop_recursion is set, skip parsing children but check for visible children
if stop_recursion then
local keywords = M.data.keywords
local has_visible_children = false
for i = 2, #args do
local param = args[i]
if type(param) == "string" then
local keyword_base = get_keyword_base(param)
if keyword_base and keywords[keyword_base] then
local _, kw_modifiers = EtymonParser.parse_keyword_modifiers(param:sub(1, 1) == ":" and param or (":" .. param))
if not keyword_invisible_in_tree(get_effective_keyword_info(keyword_base, kw_modifiers)) then
has_visible_children = true
break
end
elseif param:sub(1, 1) ~= ":" then
-- It's a term (not a keyword), so there are visible children
has_visible_children = true
break
end
end
end
node.has_visible_children = has_visible_children
return node
end
-- Parse args into keyword containers
local current_keyword = "from"
local current_keyword_modifiers = {}
local current_container = nil
local function ensure_container()
if not current_container or current_container.keyword ~= current_keyword then
local keyword_info = get_effective_keyword_info(current_keyword, current_keyword_modifiers)
current_container = {
keyword = current_keyword,
keyword_info = keyword_info,
keyword_modifiers = current_keyword_modifiers,
terms = {},
}
table.insert(node.children, current_container)
-- Override keyword text/phrase for nominalization with <g:code>
if current_keyword_modifiers.g and current_keyword == "nominalization" then
local labels = get_nominalization_label_for_g(current_keyword_modifiers.g)
if not labels then
local codes = {}
for c in pairs(M.data.nominalization_g_codes) do table.insert(codes, c) end
table.sort(codes)
error("Invalid <g:" .. tostring(current_keyword_modifiers.g) .. ">. Supported codes for nominalization: " .. table.concat(codes, ", "))
end
current_container.keyword_info = copy_keyword_info(keyword_info)
current_container.keyword_info.text = labels.text
current_container.keyword_info.phrase = labels.phrase
end
end
return current_container
end
local parse_context_lang = Util.resolve_context_lang(lang, args)
for i = 2, #args do
local param = args[i]
if is_keyword(param) then
local keyword, modifiers = EtymonParser.parse_keyword_modifiers(param)
if not keyword then
error("Invalid keyword '" .. param .. "'.")
end
current_keyword = keyword
current_keyword_modifiers = modifiers
current_container = nil -- Force new container for new keyword
elseif type(param) == "string" and param:sub(1, 1) == ":" then
reject_removed_surf_keyword(param)
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif type(param) == "string" then
local etymon_data = EtymonParser.parse_etymon(param, parse_context_lang)
if etymon_data then
-- Track keyword usage at top level
M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, current_keyword, lang, etymon_data.lang, is_toplevel)
local term_node = {}
local container
-- Handle suppress_term (-) and unknown_term (empty or +) directly
if etymon_data.suppress_term or etymon_data.unknown_term then
container = ensure_container()
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
term_node = TreeBuilder.build(etymon_data.lang, nil, inline_args, seen, depth + 1)
else
term_node = {
lang = etymon_data.lang,
children = {},
status = M.data.STATUS.OK,
}
end
TreeBuilder.apply_etymon_fields(term_node, etymon_data)
else
-- Regular term: fetch arguments from page
record_term_id_tracking(etymon_data)
local etymon_args, page_of, resolved_etymon_id, descendants_check =
DataRetriever.get_etymon_args(etymon_data, is_toplevel)
-- Check for <ety> inline parameter doesn't override the scraped arguments, unless the latter are missing
if etymon_data.ety then
if etymon_args == M.data.STATUS.REDLINK or etymon_args == M.data.STATUS.MISSING then
__state.current_page_has_inline_etymology = true
if is_toplevel then
__state.toplevel_has_inline_etymology = true
end
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
-- Track inline ety keywords too
local inline_keyword = get_keyword(inline_args[2], true)
if inline_keyword and #inline_args >= 3 then
local inline_etymon = EtymonParser.parse_etymon(inline_args[3], etymon_data.lang)
if inline_etymon then
M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, inline_keyword, etymon_data.lang, inline_etymon.lang, is_toplevel)
end
end
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
etymon_args = inline_args
term_node.page_of = __state.cached_etymon_pages[key] -- term node is on the same page as the parent
else
-- Scraped arguments exist, <ety> is redundant and ignored
__state.current_page_has_redundant_etymology = true
if is_toplevel then
__state.toplevel_redundant_etymology = true
end
end
end
-- Ensure container exists before checking keyword info
container = ensure_container()
-- Check if current keyword has no_child_categories - if so, stop recursion
local keyword_info = container.keyword_info
local should_stop_recursion = (stop_recursion or (keyword_info and keyword_info.no_child_categories))
term_node = TreeBuilder.build(etymon_data.lang, etymon_data.term, etymon_args, seen, depth + 1, should_stop_recursion)
term_node.target_key = Util.get_norm_lang(etymon_data.lang):getFullCode() ..
":" .. M.links.get_link_page(etymon_data.term, etymon_data.lang)
term_node.etymon_id = resolved_etymon_id -- The actual etymon id when resolved via senseid
term_node.page_of = page_of
TreeBuilder.apply_etymon_fields(term_node, etymon_data)
term_node.missing_descendants_header, term_node.missing_descendants_entry =
M.descendants.get_term_sync_flags(current_keyword, term_node.status, descendants_check)
end
table.insert(container.terms, term_node)
end
end
end
return node
end
-- Convert etymology tree to JSON-serializable table
local function tree_to_json(node)
local obj = {
term = node.title,
lang = node.lang:getCode(),
lang_name = node.lang:getCanonicalName(),
id = (node.id and node.id ~= "") and node.id or nil,
status = node.status,
is_uncertain = node.is_uncertain or nil,
is_duplicate = node.is_duplicate or nil,
gloss = node.gloss,
transliteration = node.tr,
transcription = node.ts,
alt = node.alt,
g = node.genders,
pos = node.pos,
ng = node.ng,
infl = node.infl,
children = {},
}
for _, container in ipairs(node.children or {}) do
local keyword_info = container.keyword_info
if keyword_info then
local container_obj = {
keyword = container.keyword,
keyword_label = keyword_info.text,
keyword_abbrev = keyword_info.abbrev,
is_group = keyword_info.is_group or nil,
is_invisible = keyword_info.invisible or nil,
is_uncertain = (container.keyword_modifiers and container.keyword_modifiers.unc) or nil,
terms = {},
}
for _, term in ipairs(container.terms or {}) do
table.insert(container_obj.terms, tree_to_json(term))
end
table.insert(obj.children, container_obj)
end
end
return obj
end
-- Build and return the etymology data tree for a given term.
function export.get_tree(lang, title, args, options)
options = options or {}
__state.entry_title = title
__state.entry_lang_code = lang:getCode()
__state.id_stats = M.tracking.new_id_stats()
__state.skip_partial_etymology_category = options.skip_partial_etymology_category == true
if options.validate then
EtymonParser.validate(lang, args, options.id, title, options.pos, false)
end
local lang_code = lang:getCode()
local start_index = (args[1] == lang_code) and 2 or 1
local tree_args = { [1] = lang_code, id = options.id or args.id }
for i = start_index, #args do
table.insert(tree_args, args[i])
end
__state.cached_etymon_args[lang_code .. ":" .. title .. ":" .. (tree_args.id or "")] = tree_args
local ety_data_tree = TreeBuilder.build(lang, title, tree_args)
if options.json then
return M.JSON.toJSON(tree_to_json(ety_data_tree))
end
return ety_data_tree
end
-- Given a language code, page name and optionally the id= parameter,
-- render the tree and only the etymology tree for the relevant page.
-- Fetches and parses the corresponding {{etymon}} from the requested page,
-- and any further pages needed to render the tree.
-- Parameters can be passed either through the #invoke or as
-- template parameters *through* an #invoke.
function export.render_tree_for_etymon_on_page(frame)
local frame_args = frame.args
local parent_args = frame:getParent().args
local langcode = frame_args[1] or parent_args[1]
local pagename = frame_args[2] or parent_args[2]
local id = frame_args["id"] or parent_args["id"]
local display_title = frame_args["title"] or parent_args["title"]
local parsed_title = mw.title.new(pagename, 0)
local title
if parsed_title.namespace == 0 then
title = M.pages.safe_page_name(parsed_title)
elseif parsed_title.namespace == 118 then
title = "*" .. M.pages.safe_page_name(parsed_title)
else
error("Unsupported namespace for render_tree_for_etymon_on_page: " .. parsed_title.namespace)
end
local lang = Util.get_lang(langcode)
__state.entry_title = title
__state.entry_lang_code = lang:getCode()
__state.id_stats = M.tracking.new_id_stats()
-- Construct etymon_data for DataRetriever.get_args.
local etymon_data = {
lang = lang,
term = title,
id = id
}
local args, pagename = DataRetriever.get_etymon_args(etymon_data, true)
if args == M.data.STATUS.MISSING then
error("The etymon template was not found (language " ..
langcode ..
", title '" ..
title ..
"'" ..
(id and ", ID '" .. id .. "'" or ", no ID given") .. "). Page contents may have changed in the interim.")
end
local tree_title = display_title or title
if lang:stripDiacritics(M.links.remove_links(tree_title)) ~= lang:stripDiacritics(M.links.remove_links(title)) then
M.tracking.track_title_pagename_mismatch(lang)
end
reset_invocation_state()
local ety_data_tree = export.get_tree(lang, tree_title, args, {
validate = true,
id = id,
})
local output = {}
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
return table.concat(output)
end
function export.main(frame)
local parent_args = frame:getParent().args
local args = M.parameters.process(parent_args, M.parameters_data.etymon)
local lang = args[1]
local etymon_args = args[2]
local id = args.id
local title = args.title
local text = args.text
local tree = args.tree
local etydate = args.etydate
local doublet = args.doublet
local rfe = args.rfe
local etystub = args.etystub
local is_nonlemma = M.yesno(args.nl, false)
local page_data = Util.get_page_data()
if not title then
title = page_data.pagename
if page_data.namespace == "Rekonstruksi" then title = "*" .. title end
end
local entry_pagename = page_data.pagename
if page_data.namespace == "Rekonstruksi" then
entry_pagename = "*" .. entry_pagename
end
if lang:stripDiacritics(M.links.remove_links(title)) ~= lang:stripDiacritics(M.links.remove_links(entry_pagename)) then
M.tracking.track_title_pagename_mismatch(lang)
end
local current_L2 = M.pages.get_current_L2()
if current_L2 then
local norm_lang = Util.get_norm_lang(lang)
local norm_name = norm_lang:getCanonicalName()
if current_L2 ~= norm_name then
local lang_desc = lang:getCode() .. " (" .. lang:getCanonicalName() .. ")"
if norm_lang:getCode() ~= lang:getCode() then
lang_desc = lang_desc .. ", normalized to " .. norm_lang:getCode() .. " (" .. norm_name .. ")"
end
error("Language '" .. lang_desc .. "' does not match the L2 header (" .. current_L2 .. ").")
end
end
reset_invocation_state()
local ety_data_tree = export.get_tree(lang, title, etymon_args, {
validate = true,
pos = args.pos,
id = id,
json = args.json,
skip_partial_etymology_category = is_nonlemma,
})
if args.json then
return ety_data_tree
end
local output = {}
local text_allowlist_mode = M.text_allowed.default_mode or "off"
if text and text_allowlist_mode ~= "off" and not Util.is_text_param_allowed_for_lang(lang) then
local msg = "Etymology texts (parameter <code>text=</code>) are not allowed for " .. lang:getFullName() ..
"; see [[Template:etymon#Text allowlist|Template:etymon § Text allowlist]] for the list of languages that may use the <code>text=</code> parameter."
if text_allowlist_mode == "error" then
error(msg)
else
Util.add_warning(msg, true)
end
end
local lang_exc = Util.get_lang_exception(lang)
if lang_exc and lang_exc.disallow then
local disallow = lang_exc.disallow
local error_text = " for " .. lang:getFullName()
if disallow.ref then
error_text = error_text .. "; see " .. disallow.ref
else
error_text = error_text .. "."
end
if tree and disallow.tree then
error("Etymology trees are not allowed" .. error_text)
end
if text and disallow.text then
error("Etymology texts are not allowed" .. error_text)
end
end
if etydate then
local etydate_param_mods = M.parameter_utilities.construct_param_mods {
{ group = "ref" },
{ param = "refn" },
{ param = "nocap", type = "boolean" },
}
local function generate_etydate_obj(etydate_text)
local etydate_specs = {}
for spec in etydate_text:gmatch("[^,]+") do
table.insert(etydate_specs, mw.text.trim(spec))
end
return { [1] = etydate_specs }
end
local parsed_etydate = M.parse_utilities.parse_inline_modifiers(etydate, { param_mods = etydate_param_mods, generate_obj = generate_etydate_obj })
local etydate_args = {
[1] = parsed_etydate[1],
nocap = parsed_etydate.nocap or false,
}
ety_data_tree.supplements = ety_data_tree.supplements or {}
table.insert(ety_data_tree.supplements, {
type = "etydate",
etydate_text = M.etydate.format_etydate(etydate_args, { omit_refs = true }),
etydate_refs = (parsed_etydate.refs and #parsed_etydate.refs > 0) and parsed_etydate.refs or nil,
})
end
TreeBuilder.append_term_supplement(ety_data_tree, lang, "doublet", doublet)
local has_visible_children = node_has_visible_tree_children(ety_data_tree)
-- Suppress trees for multiword entries and one-step chains
local visible_tree_depth = get_visible_tree_depth(ety_data_tree)
local is_trivial_tree = visible_tree_depth <= 2
local is_multiword = title:find("%s") ~= nil or title:find("_") ~= nil
if tree and (is_multiword or is_trivial_tree) then
tree = false
end
if tree then
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
end
local tree_disallowed = lang_exc and lang_exc.disallow and lang_exc.disallow.tree
local ety_tree_json = M.JSON.toJSON(tree_to_json(ety_data_tree))
local anchor = M.anchors.etymonid(lang, id, {
no_tree = args.notree,
title = title,
empty_tree = (not has_visible_children) or tree_disallowed,
ety_tree_json = ety_tree_json,
})
table.insert(output, anchor)
local text_stop_lang_missing = nil
if text then
local max_depth, stop_at_blue_link, stop_at_lang, stop_at_lang_or_bluelink
if text == "++" then
max_depth, stop_at_blue_link = false, false
elseif text == "+" then
max_depth, stop_at_blue_link = 1, false
elseif text == "*" then
max_depth, stop_at_blue_link = false, true
elseif text:match("^:[^*]+%*$") then
-- Stop at a specific language OR first bluelink after it, e.g., ":ota*"
-- If the target language is a redlink, continue to the first bluelink
local lang_code = text:match("^:([^*]+)%*$")
if lang_code and lang_code ~= "" then
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang_or_bluelink = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
elseif text:sub(1, 1) == ":" then
-- Stop at a specific language, e.g., ":ar" stops at first Arabic term
local lang_code = text:sub(2)
if lang_code ~= "" then
-- Validate the language code
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
local num = tonumber(text)
if num and num >= 1 then
max_depth, stop_at_blue_link = num, false
else
error('Invalid text value "' ..
text .. '". Valid values are: "++" (full chain), "+" (first step only), "*" (until first blue link), a number (max steps), ":lang" (stop at language), or ":lang*" (stop at language or first bluelink if redlink)')
end
end
local text_output, text_render_meta = M.text.render({
data_tree = ety_data_tree,
format_term_func = Util.format_term,
lang_matches_stop_code = Util.lang_matches_stop_code,
max_depth = max_depth,
stop_at_blue_link = stop_at_blue_link,
curr_page = page_data.pagename,
nodot = args.nodot,
dot = args.dot,
stop_at_lang = stop_at_lang,
stop_at_lang_or_bluelink = stop_at_lang_or_bluelink,
})
table.insert(output, text_output)
if stop_at_lang and text_render_meta and not text_render_meta.stop_lang_reached then
M.tracking.track_text_stop_lang_missing(lang, stop_at_lang)
text_stop_lang_missing = stop_at_lang
end
end
if rfe then
table.insert(output, Util.expand_request_template(frame, "rfe", rfe, lang:getCode()))
end
if etystub then
table.insert(output, Util.expand_request_template(frame, "etystub", etystub, lang:getCode()))
end
if is_nonlemma then
table.insert(output, " " .. frame:expandTemplate({
title = "nonlemma",
args = {},
}))
end
local categories = {}
if Util.is_content_page() then
M.tracking.track_tree_metrics({
max_depth_reached = __state.max_depth_reached,
total_nodes = __state.total_nodes,
language_count = __state.language_count,
lang = lang,
})
categories = M.categories.build({
data_tree = ety_data_tree,
page_lang = lang,
available_etymon_ids = __state.available_etymon_ids,
senseid_parent_etymon = __state.senseid_parent_etymon,
get_norm_lang_func = Util.get_norm_lang,
lang_exc = lang_exc,
suppress_categories = lang_exc and lang_exc.suppress_categories,
nocat = args.nocat,
tree = tree,
text = text,
exnihilo = args.exnihilo,
toplevel_has_inline_etymology = __state.toplevel_has_inline_etymology,
toplevel_redundant_etymology = __state.toplevel_redundant_etymology,
toplevel_idless_etymon = __state.toplevel_idless_etymon,
has_mismatched_id = __state.has_mismatched_id,
linked_page_multiple_etymons_idless = __state.linked_page_multiple_etymons_idless,
linked_page_partial_etymology_sections = __state.linked_page_partial_etymology_sections,
text_stop_lang_missing = text_stop_lang_missing,
})
M.tracking.track_keywords(__state.toplevel_keyword_stats, lang)
M.tracking.track_page_id(lang, id)
M.tracking.track_ids(__state.id_stats, lang)
end
if #categories > 0 then
table.insert(output, M.categories.format(categories, lang))
end
if __state.warnings then
for i, warning in ipairs(__state.warnings) do
table.insert(output, (i == 1 and "\n" or "") .. warning .. "\n")
end
end
return table.concat(output)
end
return export
7qwzuob4k9obymq2ejknfo0emdwmooz
MediaWiki:Gadget-LanguagesAndScripts.css
8
77816
375856
227608
2026-09-25T09:37:52Z
PeaceSeekers
3334
Kemaskini
375856
css
text/css
/*
Support for scripts. See [[Wiktionary:Scripts]].
*/
/* Default style for mentions outside of "form of" definitions.
See [[Wiktionary:Votes/2007-10/style for mentioned terms]] */
.mention {
font-style: italic;
}
.mention i, i .mention {
font-style: normal;
}
/* The following scripts do not support italic text, but do support bold text.
Note: we repeat the first class selector twice to increase CSS 'specificity'. */
.Aghb.Aghb, .Aghb *,
.Ahom.Ahom, .Ahom *,
.Armi.Armi, .Armi *,
.Armn.Armn, .Armn *,
.Avst.Avst, .Avst *,
.Bali.Bali, .Bali *,
.Bamu.Bamu, .Bamu *,
.Batk.Batk, .Batk *,
.Beng.Beng, .Beng *,
.as-Beng.as-Beng, .as-Beng *,
.Bopo.Bopo, .Bopo *,
.Brai.Brai, .Brai *,
.Bugi.Bugi, .Bugi *,
.Buhd.Buhd, .Buhd *,
.Cakm.Cakm, .Cakm *,
.Cans.Cans, .Cans *,
.Cari.Cari, .Cari *,
.Cham.Cham, .Cham *,
.Cher.Cher, .Cher *,
.Copt.Copt, .Copt *,
.Cprt.Cprt, .Cprt *,
.Cyrl.Cyrl, /* .Cyrl *, */
.Cyrs.Cyrs, .Cyrs *,
.Deva.Deva, .Deva *,
.Dsrt.Dsrt, .Dsrt *,
.Egyp.Egyp, .Egyp *,
.Ethi.Ethi, .Ethi *,
.Geok.Geok, .Geok *,
.Geor.Geor, .Geor *,
.Glag.Glag, .Glag *,
.Goth.Goth, .Goth *,
.Grek.Grek, /* .Grek *, */
.Gujr.Gujr, .Gujr *,
.Guru.Guru, .Guru *,
.Hang.Hang, .Hang *,
.Hani.Hani, .Hani *,
.Hano.Hano, .Hano *,
.Hans.Hans, .Hans *,
.Hant.Hant, .Hant *,
.Hira.Hira, .Hira *,
.Hluw.Hluw, .Hluw *,
.IPA.IPA, .IPA *,
.Ipach.Ipach, .Ipach *,
.Ital.Ital, .Ital *,
.Java.Java, .Java *,
.Jpan.Jpan, .Jpan *,
.Kali.Kali, .Kali *,
.Kana.Kana, .Kana *,
.Khar.Khar, .Khar *,
.Khmr.Khmr, .Khmr *,
.Knda.Knda, .Knda *,
.Kore.Kore, .Kore *,
.Kthi.Kthi, .Kthi *,
.Lana.Lana, .Lana *,
.Laoo.Laoo, .Laoo *,
.Lepc.Lepc, .Lepc *,
.Limb.Limb, .Limb *,
.Lisu.Lisu, .Lisu *,
.Lyci.Lyci, .Lyci *,
.Lydi.Lydi, .Lydi *,
.Mand.Mand, .Mand *,
.Merc.Merc, .Merc *,
.Mero.Mero, .Mero *,
.Mlym.Mlym, .Mlym *,
.Modi.Modi, .Modi *,
.Mtei.Mtei, .Mtei *,
.Music.Music, .Music *,
.Narb.Narb, .Narb *,
.Nkoo.Nkoo, .Nkoo *,
.Olck.Olck, .Olck *,
.Orya.Orya, .Orya *,
.Osma.Osma, .Osma *,
.Phag.Phag, .Phag *,
.Phli.Phli, .Phli *,
.Phlv.Phlv, .Phlv *,
.Phnx.Phnx, .Phnx *,
.Plrd.Plrd, .Plrd *,
.Polyt.Polyt, .Polyt *,
.Prti.Prti, .Prti *,
.Rjng.Rjng, .Rjng *,
.Samr.Samr, .Samr *,
.Sarb.Sarb, .Sarb *,
.Saur.Saur, .Saur *,
.Sgnw.Sgnw, .Sgnw *,
.Shaw.Shaw, .Shaw *,
.Shrd.Shrd, .Shrd *,
.Sinh.Sinh, .Sinh *,
.Sora.Sora, .Sora *,
.Sund.Sund, .Sund *,
.Sylo.Sylo, .Sylo *,
.Tagb.Tagb, .Tagb *,
.Tale.Tale, .Tale *,
.Talu.Talu, .Talu *,
.Taml.Taml, .Taml *,
.Tavt.Tavt, .Tavt *,
.Tayo.Tayo, .Tayo *,
.Telu.Telu, .Telu *,
.Tfng.Tfng, .Tfng *,
.Tglg.Tglg, .Tglg *,
.Thaa.Thaa, .Thaa *,
.Thai.Thai, .Thai *,
.Tibt.Tibt, .Tibt *,
.Ugar.Ugar, .Ugar *,
.Vaii.Vaii, .Vaii *,
.Xpeo.Xpeo, .Xpeo *,
.Yiii.Yiii, .Yiii *,
.Zmth.Zmth, .Zmth *,
.Zname.Zname, .Zname *,
.Zsym.Zsym, .Zsym * {
font-style: normal;
}
/* The following scripts do not support neither bold nor italic text.
Note: we repeat the first class selector twice to increase CSS 'specificity'. */
.Arab.Arab, .Arab *,
.Aran.Aran, .Aran *,
.Brah.Brah, .Brah *,
.Elba.Elba, .Elba *,
.Hebr.Hebr, .Hebr *,
.Lina.Lina, .Lina *,
.Linb.Linb, .Linb *,
.Mani.Mani, .Mani *,
.Mymr.Mymr, .Mymr *,
.Mong.Mong, .Mong *,
.mnc-Mong.mnc-Mong, .mnc-Mong *,
.sjo-Mong.sjo-Mong, .sjo-Mong *,
.xwo-Mong.xwo-Mong, .xwo-Mong *,
.Ogam.Ogam, .Ogam *,
.Orkh.Orkh, .Orkh *,
.Ougr.Ougr, .Ougr *,
.Runr.Runr, .Runr *,
.Sogd.Sogd, .Sogd *,
.Syrc.Syrc, .Syrc *,
.Tang.Tang, .Tang *,
.Vith.Vith, .Vith *,
.Xsux.Xsux, .Xsux * {
font-style: normal;
font-weight: normal;
}
/* For scripts where bolding is not used or can be hard to see, the
quoted word is highlighted in pink. */
.Arab.e-example b, .Arab.e-quotation b, .Arab.e-normalization b,
.Aran.e-example b, .Aran.e-quotation b, .Aran.e-normalization b,
.Brah.e-example b, .Brah.e-quotation b, .Brah.e-normalization b,
.Deva.e-example b, .Deva.e-quotation b, .Deva.e-normalization b,
.Elba.e-example b, .Elba.e-quotation b, .Elba.e-normalization b,
.Hebr.e-example b, .Hebr.e-quotation b, .Hebr.e-normalization b,
.Lina.e-example b, .Lina.e-quotation b, .Lina.e-normalization b,
.Linb.e-example b, .Linb.e-quotation b, .Linb.e-normalization b,
.Mani.e-example b, .Mani.e-quotation b, .Mani.e-normalization b,
.Mong.e-example b, .Mong.e-quotation b, .Mong.e-normalization b,
.Ogam.e-example b, .Ogam.e-quotation b, .Ogam.e-normalization b,
.Orkh.e-example b, .Orkh.e-quotation b, .Orkh.e-normalization b,
.Ougr.e-example b, .Ougr.e-quotation b, .Ougr.e-normalization b,
.Runr.e-example b, .Runr.e-quotation b, .Runr.e-normalization b,
.Sogd.e-example b, .Sogd.e-quotation b, .Sogd.e-normalization b,
.Syrc.e-example b, .Syrc.e-quotation b, .Syrc.e-normalization b,
.Ugar.e-example b, .Ugar.e-quotation b, .Ugar.e-normalization b,
.Vith.e-example b, .Vith.e-quotation b, .Vith.e-normalization b,
.Xpeo.e-example b, .Xpeo.e-quotation b, .Xpeo.e-normalization b,
.Xsux.e-example b, .Xsux.e-quotation b, .Xsux.e-normalization b {
background-color: var(--wikt-palette-pink, #ffe0f0);
font-size: inherit;
}
/* Latin (.Latn)
This is the default script, so it has no definitions. */
/* This is needed to overcome the "smartness" of certain web browsers */
.Latn:lang(ja),
.Latn:lang(ko),
.Latn:lang(zh) {
font-family: inherit;
}
/* Latin Fraktur */
.Latf {
font-family: UnifrakturMaguntia, UnifrakturCook, Unifraktur, sans-serif;
font-size: 125%;
}
/* Navajo */
.nv-Latn {
font-family: Calibri, 'Aboriginal Sans', 'DejaVu Sans', 'Arial Unicode MS', sans-serif !important;
}
/* Pitjantjatjara (ḻ ṉ ṟ ṯ and capitals) */
.pjt-Latn {
font-family: Arial, 'Microsoft Sans Serif', Tahoma, sans-serif;
}
/* Latin extended */
/* no styles for .Latnx, .Latinx, .unicode, .Unicode */
/* Adlam */
.Adlm {
font-family: 'Noto Sans Adlam', sans-serif;
}
/* Aghwan */
.Aghb {
font-family: OptimaModoki, 'Noto Sans Caucasian Albanian', sans-serif;
}
/* Ahom */
.Ahom {
font-family: AhomUnicode, 'Noto Serif Ahom', 'Unifont Upper', sans-serif;
}
/* Arabic */
.Arab,
.Aran {
font-family: 'Noto Naskh Arabic', 'Iranian Sans', Tahoma, 'Microsoft Sans Serif', 'Arial Unicode MS', sans-serif;
font-size: 133%;
direction: rtl;
unicode-bidi: isolate;
}
/* former fa-Arab */
.Arab:lang(az), /* Azerbaijani */
.Arab:lang(fa), /* Persian */
.Arab:lang(tg), /* Tajik */
.Arab:lang(uz), /* Uzbek */
.Arab:lang(aee), /* Northeast Pashayi */
.Arab:lang(avd), /* Alviri-Vidari */
.Arab:lang(awa), /* Awadhi */
.Arab:lang(bal), /* Baluchi */
.Arab:lang(bft), /* Balti */
.Arab:lang(bhh), /* Bukhari */
.Arab:lang(bqi), /* Bakhtiari */
.Arab:lang(bsg), /* Bashkardi */
.Arab:lang(cpg), /* Cappadocian Greek */
.Arab:lang(deh), /* Dehwari */
.Arab:lang(doi), /* Dogri */
.Arab:lang(esh), /* Eshtehardi */
.Arab:lang(glh), /* Northwest Pashayi */
.Arab:lang(glk), /* Gilaki */
.Arab:lang(hrz), /* Harzani */
.Arab:lang(kaa), /* Karakalpak */
.Arab:lang(kgn), /* Karingani */
.Arab:lang(klj), /* Khalaj */
.Arab:lang(kok), /* Konkani */
.Arab:lang(lki), /* Laki */
.Arab:lang(lrc), /* Northern Luri */
.Arab:lang(lrl), /* Larestani */
.Arab:lang(luz), /* Southern Luri */
.Arab:lang(mhj), /* Mogholi */
.Arab:lang(oru), /* Ormuri */
.Arab:lang(psh), /* Southwest Pashayi */
.Arab:lang(psi), /* Southeast Pashayi */
.Arab:lang(sgh), /* Shughni */
.Arab:lang(shm), /* Shahrudi */
.Arab:lang(siy), /* Sivandi */
.Arab:lang(smy), /* Semnani */
.Arab:lang(srz), /* Shahmirzadi */
.Arab:lang(tly), /* Talysh */
.Arab:lang(ttt), /* Tat */
.Arab:lang(xnr), /* Kangri */
.Arab:lang(xme-ker), /* Kermanic */
.Arab:lang(xme-taf), /* Tafreshi */
/* .Arab:lang(xme-klt), Kilit; there's a comment saying "and fa-Arab?" */
.Arab:lang(xme-ott) { /* Old Tati */
font-family: 'Iranian Sans', 'Geeza Pro', Tahoma, sans-serif;
}
/* former ks-Arab */
.Arab:lang(ks), /* Kashmiri */
.Arab:lang(kls) { /* Kalasha */
font-family: 'Noto Naskh Arabic', Amiri, Mirza, 'Scheherazade New', Scheherazade, 'Geeza Pro', Tahoma, sans-serif;
}
/* former ku-Arab */
.Arab:lang(ckb), /* Central Kurdish */
.Arab:lang(hac), /* Gurani */
.Arab:lang(kmr), /* Northern Kurdish */
.Arab:lang(sdh) { /* Southern Kurdish */
font-family: 'Noto Naskh Arabic', Tahoma, 'Arial Unicode MS', 'UT Cairo', 'UT Naskh', sans-serif;
}
/* former pa-Arab and ur-Arab */
.Aran { /* Nastaliq */
font-family: 'Noto Nastaliq Urdu', Tahoma, 'Arial Unicode MS', 'UT Cairo', 'UT Naskh', sans-serif;
/* 'Awami Nastaliq' is possibly the best Nastaliq font currently, but only Firefox can display it correctly */
}
/* former ps-Arab */
.Arab:lang(ps), /* Pashto */
.Arab:lang(wne) { /* Waneci */
font-family: Pokhto, 'Pashto Kror Asiatype', 'Scheherazade New', Scheherazade, sans-serif;
}
/* former sd-Arab */
.Arab:lang(kfr), /* Kachchi */
.Arab:lang(sd) { /* Sindhi */
font-family: 'MB Sindhi', Tahoma, 'MBSarang Sattar', 'MB Bhitai Sattar', 'MB Lateefi', 'Ayaz Gul', sans-serif;
}
/* former ug-Arab */
.Arab:lang(srh), /* Sarikoli */
.Arab:lang(ug) { /* Uyghur */
font-family: 'UKIJ Tuz', 'Microsoft Uighur', 'Scheherazade New', Scheherazade, sans-serif;
}
/* Imperial Aramaic */
.Armi {
font-family: 'Segoe UI Historic', 'Aramaic Imperial Yeb', 'Noto Sans Imperial Aramaic', sans-serif;
direction: rtl;
unicode-bidi: isolate;
}
/* Armenian */
.Armn {
font-family: Mshtakan, Arial, 'Segoe UI', Tahoma, 'Arian AMU', 'DejaVu Sans', sans-serif;
}
/* Avestan */
.Avst {
font-family: Avestan, Ahuramzda, 'Noto Sans Avestan', sans-serif;
font-size: 125%;
direction: rtl;
unicode-bidi: isolate;
font-variant-ligatures: none; /* Avestan language */
}
.Avst:lang(pal) { /* Pazend (Middle Persian in Avestan script) */
font-variant-ligatures: normal;
}
/* Balinese */
.Bali {
font-family: 'Noto Sans Balinese', sans-serif;
}
/* Bamum */
.Bamu {
font-family: 'Noto Sans Bamum', sans-serif;
}
/* Bassa */
.Bass {
font-family: 'Noto Sans Bassa Vah', sans-serif;
}
/* Batak */
.Batk {
font-family: Batak-Unicode, 'Noto Sans Batak', sans-serif;
}
/* Bengali, Assamese */
.Beng,
.as-Beng {
font-family: 'Bangla Sangam MN', UniBangla, 'Code2000', Likhan, 'UT Bengali Dhaka', Vrinda, 'Noto Sans Bengali', sans-serif;
font-size: 130%;
}
/* Bhaiksuki */
.Bhks {
font-family: 'Noto Sans Bhaiksuki', sans-serif;
}
/* Zhuyin (Bopomofo) */
.Bopo {
font-family: MOESongUN, DFKai-SB, 'Microsoft Yahei', 'Microsoft Jhenghei', 'Source Han Sans TC', 'Source Han Sans TW', 'Noto Sans CJK TC', sans-serif;
}
/* Brahmi */
.Brah {
font-family: 'Segoe UI Historic', 'Noto Sans Brahmi', sans-serif;
}
/* Braille */
.Brai {
font-size: 150%;
}
/* Buginese */
.Bugi {
font-family: Saweri, 'Noto Sans Buginese', sans-serif;
}
/* Buhid [[Wiktionary:Grease pit/2025/February#Buhid]] */
.Buhd {
font-family: 'Noto Sans Buhid', Quivira, sans-serif;
font-size: 110%;
}
/* Chakma */
.Cakm {
font-family: RibengUni, 'Noto Sans Chakma', sans-serif;
}
/* Unified Canadian Aboriginal Syllabics */
.Cans {
font-family: 'Euphemia UCAS', Euphemia, 'Aboriginal Sans', OskiBlackfoot, 'Code2000', 'Everson Mono Unicode', 'Noto Sans Canadian Aboriginal', sans-serif;
font-size: 110%;
}
/* Carian */
.Cari {
font-family: 'Segoe UI Historic', Aegean, 'Noto Sans Carian', sans-serif;
font-size: 125%;
}
/* Cham */
.Cham {
font-family: 'Code2000', 'JG ChamCambodia', 'Noto Sans Cham', sans-serif;
font-size: 110%;
}
/* Cherokee */
.Cher {
font-family: Digohweli, 'Aboriginal Sans', 'Code2000', Marin, 'Rotinonhsonni Sans', 'Everson Mono Unicode', 'Noto Sans Cherokee', sans-serif;
font-size: 110%;
}
/* Khwarezmian */
.Chrs {
font-family: 'Noto Sans Chorasmian', sans-serif;
}
/* Coptic */
.Copt {
font-family: Quivira, Antinoou, 'New Athena Unicode', Analecta, FreeSerifAvvaShenouda, 'Arial Coptic', 'Sophia Nubian', 'Code2000', 'Noto Sans Coptic', sans-serif;
font-size: 130%;
}
/* Cypro-Minoan */
.Cpmn {
font-family: 'Noto Sans Cypro Minoan', sans-serif;
}
/* Cypriot */
.Cprt {
font-family: 'Segoe UI Historic', Aegean, 'Noto Sans Cypriot', sans-serif;
font-size: 115%;
direction: rtl;
unicode-bidi: isolate;
}
/* Cyrillic */
.Cyrl {
font-family: Helvetica, Geneva, 'Arial Unicode MS', 'Lucida Sans Unicode', sans-serif;
}
/* Old Cyrillic (Old Church Slavonic, Old East Slavic) */
.Cyrs {
font-family: Monomakh, 'Monomakh Unicode', 'Monomakh Unicode TT', Menaion, 'Menaion Unicode', 'Menaion Unicode TT', Ponomar, 'Ponomar Unicode', 'Ponomar Unicode TT', Fedorovsk, 'Fedorovsk Unicode', 'Fedorovsk Unicode TT', Pochaevsk, 'Pochaevsk Unicode', 'Pochaevsk Unicode TT', Triodion, 'Triodion Unicode', 'Triodion Unicode TT', 'Acathist', 'Shafarik', Vilnius, BukyVede, 'Kliment Std', 'RomanCyrillic Std', 'Monomachus', 'Old Standard', 'Old Standard TT', Dilyana, 'Hirmos Ponomar', 'Hirmos Ponomar TT', 'Menaion Medieval', Lazov, 'DejaVu Sans', 'Lucida Grande', 'Arial Unicode MS', 'Lucida Sans Unicode', sans-serif;
font-size: 125%;
}
/* Devanagari */
.Deva {
font-family: 'Tiro Devanagari Sanskrit', 'Sanskrit Text', 'Adobe Devanagari', 'Noto Serif Devanagari', Utsaah, 'Devanagari MT', Raghu, Gargi, JanaSanskrit, JanaHindi, Siddhanta, 'Noto Sans Devanagari', sans-serif;
font-size: 120%;
}
/* Dhives Akuru */
.Diak {
font-family: 'Noto Serif Dives Akuru', serif;
}
/* Dogra */
.Dogr {
font-family: 'Noto Serif Dogra', sans-serif;
}
/* Deseret */
.Dsrt {
font-family: 'Segoe UI Symbol', 'Code2001', 'MPH 2B Damase', 'Everson Mono', sans-serif;
}
/* Duployan */
.Dupl {
font-family: 'Noto Sans Duployan', sans-serif;
}
/* Demotic (Egyptian) */
.Egyd, .Egyd * {
font-style: normal;
direction: rtl;
}
/* Egyptian hieroglyphs */
.Egyp {
font-family: 'NewGardiner', 'Noto Sans Egyptian Hieroglyphs', Abydos, Aegyptus, 'Segoe UI Historic', sans-serif;
font-size: 150%;
}
/* Elbasan */
.Elba {
font-family: 'Noto Sans Elbasan', sans-serif;
}
/* Elymaic */
.Elym {
font-family: 'Noto Sans Elymaic', sans-serif;
}
/* Ethiopic (Ge'ez) */
.Ethi {
font-family: 'Abyssinica SIL', Nyala, 'Code2000', 'Ethiopia Jiret', 'GF Zemen Unicode', 'TITUS Cyberbit Basic', 'Visual Geez Unicode', 'Visual Geez Unicode Agazian', 'Visual Geez Unicode Title', 'Noto Sans Ethiopic', sans-serif;
font-size: 120%;
}
/* Georgian */
.Geor {
font-family: 'DejaVu Sans', 'Arial Unicode MS', Sylfaen, sans-serif;
}
/* Glagolitic */
.Glag {
font-family: Shafarik, Menaion, 'Menaion Unicode', 'Menaion Unicode TT', 'Segoe UI Historic', BukyVede, Dilyana, 'Noto Sans Glagolitic', sans-serif;
font-size: 150%;
}
/* Gunjala Gondi */
.Gong {
font-family: 'Noto Sans Gunjala Gondi', sans-serif;
}
/* Masaram Gondi */
.Gonm {
font-family: 'Noto Sans Masaram Gondi', sans-serif;
}
/* Gothic */
.Goth {
font-family: 'Segoe UI Historic', 'Code2001', Skeirs, 'MPH 2B Damase', 'Noto Sans Gothic', sans-serif;
}
b.Goth,
strong.Goth {
font-size: larger;
}
/* Grantha */
.Gran {
font-family: 'Noto Sans Grantha', sans-serif;
}
/* Greek (.Grek) removed - [[Wiktionary:Grease pit/2025/February#Sans-serif Greek font]] */
/* Albanian, Pontic, Tsakonian, Turkish and Aromanian in Greek script */
.Grek:lang(sq),
.Grek:lang(pnt),
.Grek:lang(tsd),
.Grek:lang(ota),
.Grek:lang(rup) {
font-family: 'Gentium', 'Gentium Plus', 'Lucida Sans Unicode', sans-serif;
}
/* Polytonic Greek */
.Polyt {
font-family: 'SBL Greek', 'SBL BibLit', 'New Athena Unicode', 'DejaVu Sans', Athena, Gentium, 'Gentium Plus', 'Palatino Linotype', Menaion, Times, 'Arial Unicode MS', 'Lucida Sans Unicode', 'Lucida Grande', sans-serif;
}
/* Messapic */
.Polyt:lang(cms) {
font-family: 'Noto Serif', 'Lucida Sans Unicode', 'Gentium', 'Gentium Plus', 'Noto Sans', sans-serif;
}
/* Gujarati */
.Gujr {
font-family: 'Noto Sans Gujarati', 'Gujarati Sangam MN', 'Gujarati MT', Shruti, 'Lucida Grande', 'Arial Unicode MS', 'Lucida Sans Unicode', 'Code2000', 'TITUS Cyberbit Basic', 'Chrysanthi Unicode', 'Bitstream Cyberbit', 'Bitstream CyberBase', 'Bitstream Vera', 'Visual Geez Unicode', sans-serif;
font-size: 125%;
}
/* Gurmukhi */
.Guru {
font-family: 'Gurmukhi MN', 'Gurmukhi MT', 'UT Punjabi Amritsar', 'Lucida Grande', 'Arial Unicode MS', 'Lucida Sans Unicode', 'Code2000', 'TITUS Cyberbit Basic', 'Chrysanthi Unicode', 'Bitstream Cyberbit', 'Bitstream CyberBase', 'Bitstream Vera', 'Visual Geez Unicode', 'Noto Sans Gurmukhi', sans-serif;
font-size: 125%;
}
/* Chinese (Han)
.Hani = generic
.Hans = simplified
.Hant = traditional
*/
.Hans {
font-family: 'PingFang SC', DengXian, 'Source Han Sans SC', 'Source Han Sans CN', 'Noto Sans CJK SC', 'Microsoft Yahei', SimHei, SimSun, NSimSun, SimSun-ExtB, Song, 'Heiti SC', HanaMinA, HanaMinB, sans-serif;
}
.Hani,
.Hant {
font-family: 'PingFang TC', 'Source Han Sans TC', 'Source Han Sans TW', 'Noto Sans CJK TC', 'Microsoft Jhenghei', MOESongUN, PMingLiU, PMingLiU-ExtB, MingLiU, MingLiU-ExtB, Ming, 'Heiti TC', HanaMinA, HanaMinB, sans-serif;
}
.Hani,
.Hans,
.Hant {
font-size: 120%;
line-height: 1;
}
.Hani:lang(vi),
.Hani:lang(tyz) {
font-family: 'Nom Na Tong', 'HAN NOM A', 'HAN NOM B', Sun-ExtA, Sun-ExtB, Ming-Lt-HKSCS-UNI-H, Ming-Lt-HKSCS-ExtB, HanaMinA, HanaMinB, HanaMin, 'PingFang TC', MingLiU, MingLiU-ExtB, 'MingLiU_HKSCS', 'MingLiU_HKSCS-ExtB', SimSun, SimSun-ExtB, 'Arial Unicode MS', 'TITUS Cyberbit Basic', sans-serif;
/* CJK Unified Ideographs Extension C and Extension D (U+2A700..U+2B734, U+2B740..U+2B81F)
font-family: Sun-ExtB, 'MingLiU_HKSCS-ExtB', Ming-Lt-HKSCS-ExtB, HanaMinB, sans-serif;
**/
}
/* Hanunoo */
.Hano {
font-family: 'Noto Sans Hanunoo', Quivira, 'MPH 2B Damase', sans-serif;
font-size: 110%;
}
/* Hatran */
.Hatr {
font-family: 'Noto Sans Hatran', sans-serif;
direction: rtl;
}
/* Hebrew */
.Hebr {
font-family: 'SBL Hebrew', 'SBL BibLit', 'Taamey David CLM', 'Taamey Frank CLM', Alef, 'Noto Sans Hebrew', Narkisim, Miriam, Kinryu, 'Arial Hebrew', Arial, 'Adobe Hebrew', serif;
font-size: 133%;
direction: rtl;
unicode-bidi: isolate;
}
/* Hiragana: see .Jpan */
/* Anatolian Hieroglyphs */
.Hluw {
font-family: 'Noto Sans Anatolian Hieroglyphs', Anatolian, sans-serif;
font-size: 200%;
}
/* Pahawh Hmong */
.Hmng {
font-family: 'Noto Sans Pahawh Hmong', sans-serif;
}
/* Nyiakeng Puachue Hmong */
.Hmnp {
font-family: 'Noto Serif Nyiakeng Puachue Hmong', serif;
}
/* Old Hungarian */
.Hung {
font-family: 'Noto Sans Old Hungarian', sans-serif;
}
/* Italic */
.Ital {
font-family: 'Segoe UI Historic', Aegean, 'Noto Sans Old Italic', sans-serif;
font-size: 125%;
}
/* Javanese */
.Java {
font-family: 'Tuladha Jejeg', 'Javanese Text', adjisaka, 'Noto Sans Javanese', sans-serif;
}
/* Japanese scripts
.Hira = Hiragana
.Kana = Katakana (used for Ainu language)
.Jpan = Hiragana + Katakana + Kanji
*/
.Hira,
.Jpan,
.Kana {
font-family: 'Hiragino Kaku Gothic Pro', Osaka, 'Yu Gothic', Meiryo, 'Source Han Sans J', 'Source Han Sans JP', 'Noto Sans CJK JP', 'Droid Sans Japanese', 'MS PGothic', 'MS Gothic', 'MS PMincho', 'MS Mincho', HanaMinA, HanaMinB, sans-serif;
font-size: 120%;
line-height: 1;
}
.Jpan ruby rt {
font-family: 'Yu Gothic UI', 'Meiryo UI', 'MS UI Gothic', sans-serif;
font-size: 60%;
}
/* Kayah Li */
.Kali {
font-family: 'Code2000', 'Noto Sans Kayah Li', sans-serif;
}
/* Katakana: see .Jpan */
/* Kharoshthi */
.Khar {
font-family: 'Segoe UI Historic', 'MPH 2B Damase', 'Noto Sans Kharoshthi', sans-serif;
font-size: 110%;
direction: rtl;
unicode-bidi: isolate;
}
/* Khmer */
.Khmr {
font-family: 'Khmer OS', 'Khmer OS Content', 'Leelawadee UI', 'Noto Sans Khmer', 'Code2000', 'Khmer Mondulkiri U OT ls', sans-serif;
font-size: 125%;
}
/* Khojki */
.Khoj {
font-family: 'Noto Sans Khojki', sans-serif;
}
/* Khitan small script */
.Kits {
font-family: 'Noto Serif Khitan Small Script', serif;
}
/* Kannada */
.Knda {
font-family: 'Kannada Sangam MN', JanaKannada, Tunga, Kedage, RaghuKannada, Sampige, 'Arial Unicode MS', 'Code2000', 'Bitstream Cyberbit', 'Bitstream CyberBase', 'Noto Sans Kannada', sans-serif;
font-size: 125%;
}
/* Korean */
.Kore,
.Hang {
font-family: 'Apple SD Gothic Neo', 'Malgun Gothic', Dotum, Gulim, 'NanumBarunGothic YetHangul', NanumBarunGothic, UnDotum, 'Source Han Sans K', 'Source Han Sans KR', 'Noto Sans CJK KR', NanumGothic, 'NanumMyeongjo YetHangul', NanumMyeongjo, Batang, UnBatang, sans-serif;
font-size: 120%;
line-height: 1;
}
/* Kaithi */
.Kthi {
font-family: 'Noto Sans Kaithi', sans-serif;
}
/* Lanna */
.Lana {
font-family: 'Lanna Alif', 'Noto Sans Tai Tham', sans-serif;
}
/* Lao */
.Laoo {
font-family: 'Phetsarath OT', 'Saysettha OT', 'JG Basic Lao Opentype', 'JG Basic2 Lao Opentype', 'JG LaoTimes Opentype', 'Phagnoum Lao Unicode Opentype', 'JG Lao Old Arial Opentype', DokChampa, 'Code2000', 'JG Lao Classic Opentype', 'Alice0 Unicode', 'Alice1 Unicode', 'Alice2 Unicode', 'Alice3 Unicode', 'Alice4 Unicode', 'Alice5 Unicode', 'Noto Sans Lao', 'Noto Sans Lao Looped', sans-serif;
font-size: 125%;
}
/* Lepcha */
.Lepc {
font-family: 'Noto Sans Lepcha', Mingzat, sans-serif;
}
/* Limbu */
.Limb {
font-family: 'Code2000', 'MPH 2B Damase', 'Noto Sans Limbu', sans-serif;
}
/* Linear A */
.Lina {
font-family: Aegean, 'Noto Sans Linear A', sans-serif;
font-size: 125%;
}
/* Linear B */
.Linb {
font-family: Aegean, 'Noto Sans Linear B', sans-serif;
font-size: 125%;
}
/* Lisu */
.Lisu {
font-family: 'Noto Sans Lisu', Quivira, sans-serif;
}
/* Lycian */
.Lyci {
font-family: 'Segoe UI Historic', Aegean, 'Noto Sans Lycian', sans-serif;
font-size: 125%;
}
/* Lydian */
.Lydi {
font-family: 'Segoe UI Historic', Aegean, 'Noto Sans Lydian', sans-serif;
font-size: 125%;
}
/* Mahajani */
.Mahj {
font-family: 'Noto Sans Mahajani', sans-serif;
font-size: 125%;
}
/* Makasar */
.Maka {
font-family: 'Noto Serif Makasar', serif;
}
/* Mandaic */
.Mand {
font-family: 'Noto Sans Mandaic', sans-serif;
direction: rtl;
unicode-bidi: isolate;
}
/* Manichaean */
.Mani {
font-family: 'Noto Sans Manichaean', sans-serif;
font-size: 133%;
direction: rtl;
unicode-bidi: isolate;
}
/* Marchen */
.Marc {
font-family: 'Noto Sans Marchen', sans-serif;
}
/* Medefaidrin */
.Medf {
font-family: 'Noto Sans Medefaidrin', sans-serif;
}
/* Mende */
.Mend {
font-family: 'Noto Sans Mende Kikakui', sans-serif;
}
/* Meroitic cursive */
.Merc {
font-family: 'Segoe UI Historic', Aegyptus, 'Noto Sans Meroitic', sans-serif;
font-size: 150%;
}
/* Meroitic hieroglyphs */
.Mero {
font-family: Aegyptus, 'Noto Sans Meroitic', sans-serif;
font-size: 150%;
}
/* Malayalam */
.Mlym {
font-family: 'Malayalam Sangam MN', Kartika, 'Code2000', 'Lucida Grande', 'Arial Unicode MS', 'Lucida Sans Unicode', 'TITUS Cyberbit Basic', 'Chrysanthi Unicode', 'Bitstream Cyberbit', 'Bitstream CyberBase', 'Bitstream Vera', 'Visual Geez Unicode', 'Noto Sans Malayalam', sans-serif;
font-size: 125%;
}
/* Modi */
.Modi {
font-family: 'Noto Sans Modi', sans-serif;
}
/* Classical Mongolian, Manchu, Xibe, Clear Script */
.Mong,
.mnc-Mong, /* Manchu */
.sjo-Mong, /* Xibe */
.xwo-Mong /* Clear Script, Todo */ {
font-family: 'Mongolian Baiti', 'Noto Sans Mongolian', sans-serif;
font-size: 140%;
-webkit-writing-mode: vertical-lr;
-moz-writing-mode: vertical-lr;
writing-mode: vertical-lr;
}
/* Mro */
.Mroo {
font-family: 'Noto Sans Mro', sans-serif;
}
/* Meitei Mayek */
.Mtei {
font-family: 'Eeyek Unicode', 'Noto Sans Meetei Mayek', sans-serif;
}
/* Multani */
.Mult {
font-family: 'Noto Sans Multani', sans-serif;
}
/* Burmese */
.Mymr {
font-family: TharLon, Padauk, 'Myanmar Text', 'Myanmar3', 'Myanmar2', 'Myanmar1', ParabaikSans, MyMyanmar, 'Noto Sans Myanmar', sans-serif;
font-size: 130%;
}
big.Mymr,
strong.Mymr,
b.Mymr {
font-size: 162%;
}
.Mymr b {
font-size: 130%;
}
/* Nandinagari */
.Nand {
font-family: 'Noto Sans Nandinagari', sans-serif;
}
/* North Arabian */
.Narb {
font-family: 'Noto Sans Old North Arabian', sans-serif;
}
/* Newa */
.Newa {
font-family: 'Noto Sans Newa', sans-serif;
}
/* N'Ko */
.Nkoo {
font-family: "Noto Sans N'Ko", sans-serif;
}
/* Nüshu or Nushu */
.Nshu {
font-family: 'Noto Sans Nushu', 'Noto Traditional Nushu', 'Unicode Nushu', sans-serif;
font-size: 130%;
}
/* Ogham */
.Ogam {
font-family: 'Segoe UI Historic', Beth-Luis-Nion, Pollach, 'Maigh Nuad', 'Craobh Ruadh', 'Everson Mono Ogham', Cog, Crosta, 'TITUS Ogham', 'Ragnarok Ogham', 'Noto Sans Ogham', sans-serif;
font-size: 125%;
}
big.Ogam, strong.Ogam, b.Ogam, b .Ogam {
font-size: 156%;
}
.Ogam b {
font-size: 125%;
}
/* Ol Chiki */
.Olck {
font-family: 'Code2000', 'Noto Sans Ol Chiki', sans-serif;
}
/* Orkhon Runes */
.Orkh {
font-family: 'Segoe UI Historic', Orkun, 'Old Turkic', 'Noto Sans Old Turkic', sans-serif;
}
/* Odia */
.Orya {
font-family: 'Oriya Sangam MN', 'Noto Sans Oriya', sans-serif;
font-size: 125%;
}
/* Osage */
.Osge {
font-family: 'Noto Sans Osage', sans-serif;
}
/* Osmanya */
.Osma {
font-family: 'Noto Sans Osmanya', sans-serif;
}
/* Old Uyghur */
.Ougr {
font-family: 'Noto Serif Old Uyghur', serif;
font-size: 133%;
direction: rtl;
unicode-bidi: isolate;
}
/* Palmyrene */
.Palm {
font-family: 'Noto Sans Palmyrene', sans-serif;
}
/* Pau Cin Hau */
.Pauc {
font-family: 'Noto Sans Pau Cin Hau', sans-serif;
}
/* Old Permic */
.Perm {
font-family: 'Noto Sans Old Permic', sans-serif;
}
/* Phags-pa */
.Phag {
font-family: 'BabelStone Phags-pa Book', 'Microsoft PhagsPa', 'Noto Sans Phags-pa', sans-serif;
-webkit-writing-mode: vertical-lr;
-moz-writing-mode: vertical-lr;
writing-mode: vertical-lr;
layout-flow: vertical-ideographic;
}
/* Inscriptional Pahlavi */
.Phli {
font-family: 'Segoe UI Historic', 'Noto Sans Inscriptional Pahlavi', Shapour, 'ZH Mono', sans-serif;
direction: rtl;
unicode-bidi: isolate;
}
/* Psalter Pahlavi */
.Phlp {
font-family: 'Noto Sans Psalter Pahlavi', sans-serif;
}
/* Phoenician */
.Phnx {
font-family: 'Segoe UI Historic', 'ALPHABETUM Unicode', 'MPH 2B Damase', Aegean, 'Code2001', 'Free Sans', 'Noto Sans Phoenician', sans-serif;
font-size: 125%;
direction: rtl;
unicode-bidi: isolate;
}
/* Pollard */
.Plrd {
font-family: 'Noto Sans Miao', sans-serif;
}
/* Inscriptional Parthian */
.Prti {
font-family: 'Segoe UI Historic', 'Noto Sans Inscriptional Parthian', 'ZH Mono', sans-serif;
}
/* Rejang */
.Rjng {
font-family: 'Noto Sans Rejang', 'Code2000', sans-serif;
}
/* Hanifi Rohingya */
.Rohg {
font-family: 'Noto Sans Hanifi Rohingya', sans-serif;
}
/* Runic (Germanic) */
.Runr {
font-family: 'Segoe UI Historic', Junicode, 'Free Mono', 'Caslon Roman', 'Segoe UI Symbol', 'Code2000', 'Everson Mono', 'TITUS Cyberbit Basic', 'Noto Sans Runic', sans-serif;
font-size: 130%;
}
/* Samaritan */
.Samr {
font-family: 'Noto Sans Samaritan', Quivira, 'Everson Mono', Unifont, sans-serif;
font-size: 133%;
direction: rtl;
unicode-bidi: isolate;
}
/* South Arabian */
.Sarb {
font-family: 'Segoe UI Historic', Qataban, 'Noto Sans Old South Arabian', sans-serif;
}
/* Saurashtra */
.Saur {
font-family: Pagul, 'Noto Sans Saurashtra', sans-serif;
}
/* SignWriting */
.Sgnw {
font-family: 'Noto Sans SignWriting', sans-serif;
}
/* Shavian */
.Shaw {
font-family: 'Segoe UI Historic', 'Code2001', 'Everson Mono', 'Noto Sans Shavian', sans-serif;
}
/* Sharada */
.Shrd {
font-family: 'Noto Sans Sharada', sans-serif;
font-size: 125%;
}
/* Siddham */
.Sidd {
font-family: 'Noto Sans Siddham', sans-serif;
}
/* Sinhalese */
.Sinh {
font-family: 'Sinhala Sangam MN', KaputaUnicode, KandyUnicode, Dinamina, DinaminaUniWeb, Potha, Madhura, 'Noto Sans Sinhala', sans-serif;
font-size: 125%;
}
/* Sogdian */
.Sogd {
font-family: 'Noto Sans Sogdian', sans-serif;
}
/* Old Sogdian */
.Sogo {
font-family: 'Noto Sans Old Sogdian', sans-serif;
}
/* Sorang Sompeng */
.Sora {
font-family: 'Noto Sans Sora Sompeng', sans-serif;
}
/* Soyombo */
.Soyo {
font-family: 'Noto Sans Soyombo', sans-serif;
}
/* Sundanese */
.Sund {
font-family: 'Sundanese Unicode', 'Noto Sans Sundanese', sans-serif;
}
/* Sunuwar */
.Sunu {
font-family: 'Noto Sans Sunuwar', sans-serif;
}
/* Syloti Nagri */
.Sylo {
font-family: 'Noto Sans Syloti Nagri', sans-serif;
}
/* Syriac */
.Syrc {
font-family: 'Estrangelo Edessa', 'San Francisco', 'Code2000', 'Bitstream Cyberbit', 'Bitstream CyberBase', 'Bitstream Vera', 'TITUS Cyberbit Basic', sans-serif;
font-size: 130%;
direction: rtl;
unicode-bidi: isolate;
}
/* Serto or Western form of Syriac */
.Syrc:lang(tru),
.Syrc:lang(amw) {
font-family: 'Noto Sans Syriac Western', 'Serto Jerusalem', 'Noto Sans Syriac', sans-serif;
}
/* Madnhaya or Eastern form of Syriac */
.Syrc:lang(aii) {
font-family: 'Noto Sans Syriac Eastern', 'Noto Sans Syriac', sans-serif;
}
/* Tagbanwa */
.Tagb {
font-family: Quivira, Tagbanwa, 'Noto Sans Tagbanwa', sans-serif;
font-size: 110%;
}
/* Takri */
.Takr {
font-family: 'Noto Sans Takri', sans-serif;
}
/* Tai Le */
.Tale {
font-family: 'Microsoft Tai Le', 'Tai Le Valentinium', 'MPH 2B Damase', 'Noto Sans Tai Le', sans-serif;
}
/* New Tai Lue */
.Talu {
font-family: 'Noto Sans New Tai Lue', 'Microsoft New Tai Lue', 'Dai Banna SIL Book', 'Husb_New_Tai_Lue_WB', sans-serif;
}
/* Tamil */
.Taml {
font-family: 'Tamil Sangam MN', InaiMathi, Vijaya, Akshar, JanaTamil, 'Code2000', ETTamilNew, 'Lohit Tamil', 'Arial Unicode MS', 'Free Serif', Latha, 'Noto Sans Tamil', 'Noto Sans Tamil Supplement', sans-serif;
font-size: 125%;
}
/* Tangut */
.Tang {
font-family: 'BabelStone Tangut Wenhai', 'Tangut N4694', 'Tangut Yinchuan', 'New Tangut', 'New Tangut Std', 'Tangut TWU', 'Babelstone Tangut Wenhai', sans-serif;
font-size: 120%;
text-orientation: upright;
-webkit-text-orientation: upright
}
/* Tai Viet */
.Tavt {
font-family: 'Tai Heritage Pro', 'Noto Sans Tai Viet', sans-serif;
}
/* Lai Tay */
.Tayo {
-webkit-writing-mode: vertical-rl;
-moz-writing-mode: vertical-rl;
writing-mode: vertical-rl;
layout-flow: vertical-ideographic;
}
/* Telugu */
.Telu {
font-family: 'Telugu Sangam MN', 'Noto Sans Telugu', sans-serif;
font-size: 125%;
}
/* Tifinagh */
.Tfng {
font-family: 'Hapax Berbère', Ebrima, 'Code2000', DejaVu, 'Noto Sans Tifinagh', sans-serif;
font-size: 120%;
}
/* Baybayin */
.Tglg {
font-family: 'Noto Sans Tagalog', 'Tagalog Stylized', 'Baybayin Lopez', 'Tagalog Doctrina 1593', Quivira, sans-serif;
}
.Tglg:lang(bcl) {
font-family: 'Bikol Mintz', 'Noto Sans Tagalog', 'Tagalog Stylized', 'Baybayin Lopez', 'Tagalog Doctrina 1593', Quivira, sans-serif;
}
.Tglg:lang(ceb) {
font-family: 'Bisaya Hervas', 'Noto Sans Tagalog', 'Tagalog Stylized', 'Baybayin Lopez', 'Tagalog Doctrina 1593', Quivira, sans-serif;
}
/* Thaana */
.Thaa {
font-family: 'Noto Sans Thaana', sans-serif;
}
/* Thai */
.Thai {
font-family: 'Leelawadee UI', Leelawadee, 'Arial Unicode MS', 'Code2000', 'Noto Sans Thai', 'Noto Sans Thai Looped', sans-serif;
font-size: 125%;
}
/* Tibetan */
.Tibt {
font-family: 'Noto Serif Tibetan', 'Noto Sans Tibetan', Jomolhari-ID, 'Tibetan Machine Uni', 'Tibetan Machine Web', Jomolhari, 'Microsoft Himalaya', sans-serif;
font-size: 130%;
}
/* Tirhuta */
.Tirh {
font-family: 'Noto Sans Tirhuta', sans-serif;
}
/* Toto */
.Toto {
font-family: 'Noto Serif Toto', serif;
}
/* Ugaritic */
.Ugar {
font-family: 'Oxford Ugaritic', 'Segoe UI Historic', Aegean, 'Code2001', 'Noto Sans Ugaritic', sans-serif;
}
/* Vai */
.Vaii {
font-family: Ebrima, 'Code2000', 'Noto Sans Vai', sans-serif;
font-size: 110%;
}
/* Vithkuqi */
.Vith {
font-family: 'Noto Serif Vithkuqi', 'Noto Sans Vithkuqi', sans-serif;
}
/* Varang Kshiti */
.Wara {
font-family: 'Noto Sans Warang Citi', sans-serif;
}
/* Wancho */
.Wcho {
font-family: 'Noto Sans Wancho', sans-serif;
}
/* Old Persian cuneiform */
.Xpeo {
font-family: 'Segoe UI Historic', Aegean, Xerxes, 'Code2001', 'Noto Sans Old Persian', sans-serif;
}
/* Sumero-Akkadian cuneiform */
.Xsux {
font-family: Akkadian, FreeIdgSerif, CuneiformComposite, 'Segoe UI Historic', sans-serif; /* Neo-Assyrian form */
/* font-family: FreeIdgSerif, Akkadian, CuneiformComposite, 'Segoe UI Historic', sans-serif; */ /* Old Assyrian form */
/* font-family: Akkadian, CuneiformComposite, FreeIdgSerif, 'Segoe UI Historic', sans-serif; /* /* Neo-Sumerian form */
/* font-family: Akkadian, CuneiformComposite, FreeIdgSerif, 'Segoe UI Historic', sans-serif; /* /* Classical Sumerian form */
font-size: 125%;
}
/* Specify Hittite fonts */
.Xsux:lang(hit) {
font-family: UllikummiA, Akkadian, FreeIdgSerif, CuneiformComposite, 'Segoe UI Historic', sans-serif;
}
/* Specify Sumerian fonts */
.Xsux:lang(sux) {
font-family: CuneiformComposite, Akkadian, 'Segoe UI Historic', sans-serif;
}
/* Yezidi */
.Yezi {
font-family: 'Noto Serif Yezidi', serif;
}
/* Yi */
.Yiii {
font-family: 'Noto Sans Yi', sans-serif;
}
/* Zanabazar Square */
.Zanb {
font-family: 'Noto Sans Zanabazar Square', sans-serif;
}
/* Symbols */
.Zsym {
font-family: Symbola, sans-serif;
font-size: 150%;
}
/* Rumi numerals; see <https://en.wikipedia.org/wiki/Rumi_Numeral_Symbols> */
.Rumin {
font-size: 120%;
}
/*
Multilingual writing systems and notations
*/
/* English Phonemic Representation (AHD) */
.enPR {
font-family: 'Arial Unicode MS', 'Lucida Grande', Gentium, 'Gentium Plus', 'Gentium Alternative', 'TITUS Cyberbit Basic', 'Lucida Sans Unicode', sans-serif;
}
/* International Phonetic Alphabet */
.IPA, .Ipach {
font-family: Gentium, 'Gentium Plus', GentiumAlt, 'DejaVu Sans', 'Segoe UI', 'Lucida Grande', 'Charis SIL', 'Doulos SIL', 'TITUS Cyberbit Basic', 'Code2000', 'Lucida Sans Unicode', sans-serif;
font-size: 110%;
font-variant-ligatures: no-common-ligatures;
}
/* Musical notation */
.Music {
font-family: Musica, 'Musical Symbols', Euterpe, 'Noto Music', sans-serif;
font-size: 140%;
}
/* Znamenny musical notation */
.Zname {
font-family: 'Mezenets Unicode', Slavonic, Voskresensky, Smolensky, Symbola, 'Noto Znamenny Musical Notation', sans-serif;
}
4r0lmqn9d127lgks1d0d2r5n573g2ga
Modul:etymon/data/text allowed
828
118630
375835
373465
2026-09-25T07:13:29Z
SNN95
2113
kemaskini
375835
Scribunto
text/plain
--[=[
Languages and families that may use the {{etymon}} `text=` parameter (language-community consensus).
]=]
return {
-- Mode: "off" = disabled, "warn" = warn only, "error" = enforce.
default_mode = "warn",
langs = {
["ak"] = true,
["amf"] = true,
["bg"] = true,
["bnt-sab-pro"] = true,
["cs"] = true,
["en"] = true,
["eo"] = true,
["es"] = true,
["ext"] = true,
["fa"] = true,
["gmw-msc"] = true,
["hsb"] = true,
["iir-pro"] = true,
["jbo"] = true,
["jdt"] = true,
["la"] = true,
["mul"] = true,
["ota"] = true,
["pap"] = true,
["ps"] = true,
["ro"] = true,
["sco"] = true,
["sk"] = true,
["sw"] = true,
["tg"] = true,
["tl"] = true,
["tr"] = true,
["uk"] = true,
["uz"] = true,
["sl"] = true,
["rsk"] = true,
["zlw-ocs"] = true,
["zlw-osk"] = true,
["zle-ono"] = true,
["zle-ort"] = true,
["sla-pro"] = true,
["eu"] = true,
-- Austronesian
["map-pro"] = true,
["map-ata-pro"] = true,
["poz-pro"] = true,
["poz-btk-pro"] = true,
["poz-cet-pro"] = true,
["pqe-pro"] = true,
["poz-hce-pro"] = true,
["poz-oce-pro"] = true,
["poz-pol-pro"] = true,
["poz-pnp-pro"] = true,
["poz-pep-pro"] = true,
["poz-mic-pro"] = true,
["poz-lgx-pro"] = true,
["poz-msa-pro"] = true,
["poz-mcm-pro"] = true,
["cmc-pro"] = true,
["poz-mly-pro"] = true,
["poz-swa-pro"] = true,
["btk-pro"] = true,
["phi-pro"] = true,
["phi-kal-pro"] = true,
["poz-ssw-pro"] = true,
["dru-pro"] = true,
["crp-slb"] = true,
},
families = {
["ber"] = true, -- Berber
["dra"] = true, -- Dravidian
["inc"] = true, -- Indo-Aryan
["iir-nur"] = true, -- Nuristani
["mun"] = true, -- Munda
["roa-gap"] = true, -- Galician-Portuguese
["sem-ara"] = true, -- Aramaic
["sem-arb"] = true, -- Arabic
["tup"] = true, -- Tupian
["bnt-sab"] = true, -- Lechitic
["zlw-lch"] = true, -- Lechitic
},
}
d2t0jck7ixvesghw7yzk9gdr2q8prgn
kirkification
0
144614
375855
373585
2026-09-25T09:33:59Z
SNN95
2113
kemaskini
375855
wikitext
text/x-wiki
== Bahasa Inggeris ==
[[File:Mona Lisa Kirkification.jpg|thumb|alt=Mona Lisa dengan wajah digantikan dengan wajah Charlie Kirk|'''''Kirkification''' of the [[Mona Lisa]]'' (Kirkifikasi ''Mona Lisa'')]]
===Etimologi===
{{ety|en|:af|Kirk<alt:(Charlie) Kirk><id:kata nama khas asal>|-ification|tree=1}}
Gabungan {{suffix|en|Kirk|ification}}.
===Sebutan===
* {{AFA|en|/kɜːkɪ.fɪˈkeɪ.ʃən/|a=RP}}
* {{AFA|en|/kɜɹkɪ.fɪˈkeɪ.ʃən/|a=GA}}
* {{audio|en|En-us-kirkification.ogg|a=US}}
* {{rhymes|en|eɪʃən|s=5}}
===Kata nama===
{{en-kn}}
# Perbuatan menyunting or atau mengganti muka pada gambar seseorang dengan gambar aktivis politik Amerika Syarikat, [[w:Charlie Kirk|Charlie Kirk]].
#* {{quote-journal|en|author=Cade Savoy|date=2025-11-17|title=AI ‘Kirkification’ prompts important questions about digital grief|work=[[w:The Reveille (newspaper)|The Reveille]]|location=Baton Rouge, Louisiana|publisher=LSU|archiveurl=https://web.archive.org/web/20251213174921/https://lsureveille.com/270296/opinion/opinion-ai-kirkification-prompts-important-questions-about-digital-grief/
|text=That was until I logged into my X account and saw Kirk’s face grafted onto ex-Syrian dictator Bashar al-Assad.{{pb}}Assad isn’t alone: “'''Kirkification'''” — the practice of using AI to superimpose Kirk’s face onto another person — has taken over the internet, primarily as a means of poking fun at the dead man’s supporters.|translation= Itu berlaku sehinggalah saya log masuk ke akaun X saya dan melihat wajah Kirk dicantumkan pada bekas diktator Syria Bashar al-Assad.{{pb}}Assad tidak keseorangan: “'''Kirkification'''” — amalan menggunakan AI untuk meletakkan wajah Kirk pada orang lain — telah menguasai internet, terutamanya sebagai cara untuk mempersendakan penyokong si mati.}}
#* {{quote-journal|en|title=Kirkification: Gen Z and sardonic online culture|article_series=CT Viewpoints|author=Jada King|date=2026-3-18|work=w:CT Mirror|location=Hartford, Connecticut|archiveurl=https://web.archive.org/web/20260724113734/https://ctmirror.org/2026/03/18/kirkification-gen-z-and-sardonic-online-culture/
|text=Fear, anger, and doubt keep us online, and when being exposed to atrocity after atrocity, numbness is inevitable.{{pb}}In this way, '''Kirkification''' is extremely brilliant; it effectively closes the gap between politics and comedy, acting as a form of humorous political subversion.|translation= Ketakutan, kemarahan, dan keraguan membuatkan kita terus berada dalam talian, dan apabila terdedah kepada kekejaman demi kekejaman, kebas tidak dapat dielakkan.{{pb}}Dengan cara ini, '''Kirkifikasi''' sangat bijak; ia berkesan merapatkan jurang antara politik dan komedi, bertindak sebagai satu bentuk subversi politik yang lucu.}}
#* {{RQ:New Yorker|Brady Brickner-Wood|The Kirkification of Our Troubled Times|date=2026-4-29|archiveurl=https://web.archive.org/web/20260429180802/https://www.newyorker.com/culture/infinite-scroll/the-kirkification-of-our-troubled-times
|text='''Kirkification''' began as a process of nihilistic disenchantment: churning out content that captures the confusion and cynicism of a generation trying to make sense of, and detach from, the brutal realities of contemporary political life. |translation= '''Kirkifikasi''' bermula sebagai proses kekecewaan nihilistik: menghasilkan kandungan yang menggambarkan kekeliruan dan sinisme generasi yang cuba memahami, dan memisahkan diri daripada, realiti kejam kehidupan politik kontemporari.}}
# {{lb|en|linguistik}} Perbuatan mengubah sesebuah perkataan untuk menyertakan ''Kirk'' dalam ejaan atau sebutannya, merujuk kepada aktivis politik Amerika Syarikat, Charlie Kirk.
tg1igfsnlrf1movk4nfo0gzm1ua0aej
Modul:inflection-table
828
145812
375797
2026-09-24T13:33:26Z
Hakimi97
2668
Mencipta laman baru dengan kandungan 'local export = {} local M = require("Module:module loader").init({ require = { parameters = "Module:parameters", }, loadData = { headword_data = "Module:headword/data", }, }) local concat = table.concat local insert = table.insert local WRAPPER_CLASS = "inflection-table-wrapper" local TABLE_CLASS = "inflection-table" local TITLE_CLASS = "inflection-table-title" local NO_TITLE_CLASS = "inflection-table-no-title" local NOTES_CLASS = "inflectio...'
375797
Scribunto
text/plain
local export = {}
local M = require("Module:module loader").init({
require = {
parameters = "Module:parameters",
},
loadData = {
headword_data = "Module:headword/data",
},
})
local concat = table.concat
local insert = table.insert
local WRAPPER_CLASS = "inflection-table-wrapper"
local TABLE_CLASS = "inflection-table"
local TITLE_CLASS = "inflection-table-title"
local NO_TITLE_CLASS = "inflection-table-no-title"
local NOTES_CLASS = "inflection-table-notes"
local COLLAPSIBLE_CLASSES = "inflection-table-collapsible inflection-table-collapsed no-vc"
local VS_SWITCHER_CLASS = "vsSwitcher"
local VS_TOGGLE_CLASS = "vsToggleElement"
local BOX_CLASS = "inflection-box"
local BOX_CONTENT_CLASS = "inflection-box-content"
local BOX_NARROW_CLASS = "inflection-table-narrow"
local TR_ALONGSIDE_CLASS = "tr-alongside"
local FLOW_VERTICAL_CLASS = "flow-vertical"
local NO_TITLE_MARKER = "-"
local DEFAULT_PALETTE = "grey"
local DEFAULT_TOGGLE_CATEGORY = "inflection"
local DEFAULT_TITLE_FORMAT = "Inflection of ''%s''"
local VERTICAL_FLOW = "vertical"
local BASE_STYLE = "width: fit-content"
-- Palettes recognized by [[MediaWiki:Gadget-Palette.css]].
local PALETTES = {
red = true,
scarlet = true,
orange = true,
amber = true,
yellow = true,
lime = true,
green = true,
teal = true,
cyan = true,
blue = true,
indigo = true,
purple = true,
magenta = true,
rose = true,
grey = true,
brown = true,
}
local top_params = {
title = true,
tall = true,
class = true,
["min-width"] = true,
palette = true,
category = true,
lang = true,
["vs-category"] = true,
vsToggleElement = true,
}
local bottom_params = {
notes = true,
}
local box_top_params = {
title = true,
flow = true,
category = true,
}
local function nonempty(value)
return value ~= nil and value ~= "" and value or nil
end
-- Builds the opening <div> and the wikitable header. The markup is deliberately
-- unbalanced -- {{inflection-table-bottom}} closes both -- so this is assembled
-- as text rather than through mw.html, which cannot emit an unclosed node.
function export.make_top(args)
local palette = args.palette or DEFAULT_PALETTE
if not PALETTES[palette] then
error(("Unrecognized palette '%s'; see [[MediaWiki:Gadget-Palette.css]] for the recognized palettes.")
:format(palette))
end
local no_title = args.title == NO_TITLE_MARKER
local wrapper_classes = { WRAPPER_CLASS }
if no_title then
insert(wrapper_classes, NO_TITLE_CLASS)
end
if nonempty(args.tall) then
insert(wrapper_classes, COLLAPSIBLE_CLASSES)
end
if nonempty(args.class) then
insert(wrapper_classes, args.class)
end
local style = BASE_STYLE
local min_width = nonempty(args["min-width"])
if min_width then
style = ("%s; min-width: %s"):format(style, min_width)
end
local vs_category = nonempty(args["vs-category"])
local table_classes = { TABLE_CLASS }
if nonempty(args.lang) then
insert(table_classes, ("%s-%s"):format(TABLE_CLASS, args.lang))
end
if vs_category then
insert(table_classes, VS_SWITCHER_CLASS)
end
local lines = {
('<div class="%s" style="%s" data-wikt-palette-p="%s" data-toggle-category="%s">'):format(
concat(wrapper_classes, " "),
style,
palette,
args.category or DEFAULT_TOGGLE_CATEGORY
),
('{| class="%s"%s'):format(
concat(table_classes, " "),
vs_category and (' data-toggle-category="%s"'):format(vs_category) or ""
),
}
if not no_title then
insert(lines, ('|+ class="%s%s" | %s'):format(
TITLE_CLASS,
nonempty(args.vsToggleElement) and " " .. VS_TOGGLE_CLASS or "",
args.title or DEFAULT_TITLE_FORMAT:format(M.headword_data.pagename)
))
insert(lines, "|-")
end
return concat(lines, "\n") .. "\n"
end
-- Builds the opening markup for {{inflection-box-top}}, a wrapper holding
-- several complete inflection tables side by side. Boxes always place
-- transliterations alongside, so that class is fixed. No palette is emitted:
-- each table inside the box carries its own.
-- {{inflection-table-bottom}} closes this too.
function export.make_box_top(args)
local no_title = args.title == NO_TITLE_MARKER
local classes = {
WRAPPER_CLASS,
BOX_CLASS,
BOX_NARROW_CLASS,
}
if no_title then
insert(classes, NO_TITLE_CLASS) -- zeroes the wrapper's border and padding
else
insert(classes, COLLAPSIBLE_CLASSES)
end
insert(classes, TR_ALONGSIDE_CLASS)
if args.flow == VERTICAL_FLOW then
insert(classes, FLOW_VERTICAL_CLASS)
end
local style, table_style = BASE_STYLE, ""
if no_title then
style = style .. "; background: none"
table_style = ' style="background: none"'
end
local lines = {
('<div class="%s" style="%s" data-toggle-category="%s">'):format(
concat(classes, " "),
style,
args.category or DEFAULT_TOGGLE_CATEGORY
),
('{| class="%s"%s'):format(BOX_CONTENT_CLASS, table_style),
}
if not no_title then
insert(lines, ('|+ class="%s" | %s'):format(
TITLE_CLASS,
args.title or DEFAULT_TITLE_FORMAT:format(M.headword_data.pagename)
))
end
insert(lines, "|-")
insert(lines, "|")
return concat(lines, "\n") .. "\n"
end
-- Closes the wikitable and the wrapper opened by make_top, with the notes block
-- in between.
function export.make_bottom(args)
local notes = args and nonempty(args.notes)
if not notes then
-- The blank line is what the template emitted; kept so the parser sees
-- byte-identical wikitext.
return "|}\n\n</div>"
end
return ('|}\n<div class="%s">\n%s\n</div>\n</div>'):format(NOTES_CLASS, notes)
end
local function split_args(args)
local top_args, bottom_args = {}, {}
for k, v in pairs(args or {}) do
if bottom_params[k] then
bottom_args[k] = v
else
top_args[k] = v
end
end
return top_args, bottom_args
end
-- Builds a complete table around `contents`.
function export.make_table(args, contents)
local top_args, bottom_args = split_args(args)
return export.make_top(top_args) .. contents .. export.make_bottom(bottom_args)
end
-- As export.make_table, for boxes.
function export.make_box(args, contents)
local top_args, bottom_args = split_args(args)
return export.make_box_top(top_args) .. contents .. export.make_bottom(bottom_args)
end
-- Implementation of {{inflection-table-top}}. Other modules should call
-- make_top directly instead.
function export.top(frame)
local args = M.parameters.process(frame:getParent().args, top_params, nil, "inflection-table", "top")
return export.make_top(args)
end
-- Implementation of {{inflection-table-bottom}}. See the note on export.top.
function export.bottom(frame)
local args = M.parameters.process(frame:getParent().args, bottom_params, nil, "inflection-table", "bottom")
return export.make_bottom(args)
end
-- Implementation of {{inflection-box-top}}. See the note on export.top.
function export.box_top(frame)
local args = M.parameters.process(frame:getParent().args, box_top_params, nil, "inflection-table", "box_top")
return export.make_box_top(args)
end
-- Boxes close exactly like plain tables, so these are aliases rather than
-- separate implementations.
export.make_box_bottom = export.make_bottom
export.box_bottom = export.bottom
return export
mas35tbdkeuyo7w6frl7ehgr6r1c24b
Wikikamus:bdr/nudai
4
145813
375800
2026-09-24T13:55:48Z
Sipatung
10839
Membuat terjemahan baru
375800
wikitext
text/x-wiki
==Bahasa {{bahasa|bdr}}==
===Kata kerja===
{{inti|bdr|kata kerja}}
# {{label|1=bdr|2=dialek|3=Sabah}} menyikat {{cp|bdr|Yang toh '''nudai''' bu' tekok empu ni.|Nenek '''[[menyikat]]''' rambut cucunya.}}
gbqdc2yqx34sgejbddv8spvipz3oa6z
Modul:pa-Aran-translit
828
145814
375811
2026-09-24T14:49:31Z
Hakimi97
2668
Mencipta laman baru dengan kandungan 'local m_str_utils = require("Module:string utilities") local U = m_str_utils.char local gsub = m_str_utils.gsub local export = {} local zabar = U(0x64E) local zer = U(0x650) local pesh = U(0x64F) local tashdid = U(0x651) -- also called shadda local jazm = "ْ" local he = "ہ" local ain = 'ع' local alif = 'ا' local ye = 'ی' local ye2 = 'ئ' local ye3 = "ے" local vao = "و" local aspirate = 'ھ' local nasal = 'ں' local consonants = "بٻبپت...'
375811
Scribunto
text/plain
local m_str_utils = require("Module:string utilities")
local U = m_str_utils.char
local gsub = m_str_utils.gsub
local export = {}
local zabar = U(0x64E)
local zer = U(0x650)
local pesh = U(0x64F)
local tashdid = U(0x651) -- also called shadda
local jazm = "ْ"
local he = "ہ"
local ain = 'ع'
local alif = 'ا'
local ye = 'ی'
local ye2 = 'ئ'
local ye3 = "ے"
local vao = "و"
local aspirate = 'ھ'
local nasal = 'ں'
local consonants = "بٻبپتثجڄڄچحخدݙذرزژسشصضطظعغفقکڳگلࣇمنںݨوہھٹڈڑ"
local consonantS = "بٻبپتثجڃڄچحخدݙذرزژسشصضطظعغفقکگڳلࣇمنݨہھٹڈڑ"
local consonantS2 = "یٻببپتثجڃڄچحخدݙذرزژسشصضطظعغفقکڳگلࣇمنݨوہھٹڈڑ"
local vowels = "اآیئےۓوؤ"
local hes = "ہح"
local diacritics = "َُِّْٰ"
local ZZP = "َُِ"
local mapping = {
["آ"] = 'ā', ["ب"] = 'b', ["ٻ"] = 'ḇ', ["پ"] = 'p', ["ت"] = 't', ["ٹ"] = 'ṭ', ["ث"] = 's̱',
["ج"] = 'j', ["ڄ"] = 'ǰ', ["چ"] = 'c', ["ح"] = 'ḥ', ["خ"] = 'x',
["د"] = 'd', ["ڈ"] = 'ḍ', ["ݙ"] = 'ḏ', ["ذ"] = 'ẕ', ["ر"] = 'r', ['ڑ'] = "ṛ", ["ز"] = 'z', ["ژ"] = 'ž',
["س"] = 's', ["ش"] = 'ś', ["ص"] = 'ṣ', ["ض"] = 'ẓ',
["ط"] = 't̤', ["ظ"] = 'z̤', ["ع"] = 'ʻ', ["غ"] = 'ġ', ["ف"] = 'f', ["ق"] = 'q',
["ک"] = 'k', ["گ"] = 'g', ["ڳ"] = 'g̈', ["ݨ"] = 'ṇ', ["ࣇ"] = 'ḷ',
["ل"] = 'l', ["م"] = 'm', ["ن"] = 'n', ["و"] = 'v', ["ہ"] = 'h', ["ی"] = 'y', ["۔"] = ".", ["ں"] = 'ṉ',
["ھ"] = "h",
["ؤ"] = "'o",
-- diacritics
[zabar] = "a",
[zer] = "i",
[pesh] = "u",
[jazm] = "", -- also sukun - no vowel
[U(0x200C)] = "-", -- ZWNJ (zero-width non-joiner)
-- ligatures
["ﻻ"] = "lā",
["ﷲ"] = "allāh",
-- kashida
["ـ"] = "-", -- kashida, no sound
-- numerals
["۱"] = "1", ["۲"] = "2", ["۳"] = "3", ["۴"] = "4", ["۵"] = "5",
["۶"] = "6", ["۷"] = "7", ["۸"] = "8", ["۹"] = "9", ["۰"] = "0",
-- punctuation (leave on separate lines)
["؟"] = "?", -- question mark
["،"] = ",", -- comma
["؛"] = ";", -- semicolon
["«"] = '“', -- quotation mark
["»"] = '”', -- quotation mark
["٪"] = "%", -- percent
["؉"] = "‰", -- per mille
["٫"] = ".", -- decimals
["٬"] = ",", -- thousand
["ۓ"] = "-ye",
["ۀ"] = "h-e" -- he ye (in izafat)
}
function export.tr(text, lang, sc)
-- EXCEPTIONS - leave as they are, unless they have been sorted out elsewhere
text = gsub(text, '([' .. consonants .. '])' .. ye .. vao .. nasal, "%1eyoṉ") -- needs to be fixed
text = gsub(text, '([' .. consonants .. '])' .. ye .. vao, "%1eyo") -- needs to be fixed
text = gsub(text, '([' .. consonants .. '])' .. ye .. '([' .. consonants .. '])' , "%1e%2") -- needs to be fixed
text = gsub(text, '([' .. consonants .. '])' .. ye .. alif, "%1eyā") -- needs to be fixed
text = gsub(text, zabar .. aspirate .. "(ی)" , "hai") -- needs to be fixed
text = gsub(text, zabar .. aspirate .. "(و)" , "hau") -- needs to be fixed
text = gsub(text, "ئے", "'e")
text = gsub(text, "َے", "ai")
text = gsub(text, "ے", "e")
text = gsub(text, "ہہ", "h")
text = gsub(text, "اے", "e")
text = gsub(text, "اَے", "ai")
text = gsub(text, "ن٘", "ṉ")
text = gsub(text, "اللہ", "allāh")
text = gsub(text, "ؤ" .. pesh, "ū") -- needs to be fixed
--text = gsub(text, "ُھوِیں", "vīṉ")
text = gsub(text, "([" .. pesh .. aspirate .. "])" .. "وِیں", "%1vīṉ")
-- diacritics
text = gsub(text, "([" .. consonants .. "])" .. zer .. ye .. alif, "%1īyā")
text = gsub(text, pesh .. vao .. jazm .. "", "ū")
text = gsub(text, "([" .. consonants .. "])" .. zabar .. vao, "%1au")
text = gsub(text, "([" .. consonants .. "])" .. zabar .. ye, "%1ai")
text = gsub(text, "([" .. alif .. consonants .. "])" .. zabar .. ye3, "%1ai")
text = gsub(text, "([" .. consonants .. "])" .. zer .. ye, "%1ī")
text = gsub(text, jazm .. alif, "ā")
text = gsub(text, ye2 .. zer .. ye, "'ī")
text = gsub(text, "" .. alif .. ye2 .. "([" .. consonants .. "])", "ā'i%1") -- needs to be fixed
-- Initial alif
text = gsub(text, alif .. zer, "ī")
text = gsub(text, alif .. zabar .. '([' .. consonantS .. '])', "a%1")
text = gsub(text, alif .. zabar .. vao .. jazm .. "", "au")
text = gsub(text, alif .. vao .. jazm .. "", "o")
text = gsub(text, alif .. zabar .. ye .. jazm .. "", "ai")
text = gsub(text, alif .. ye .. jazm .. "", "e")
text = gsub(text, alif .. pesh .. vao, "ū")
text = gsub(text, alif .. pesh .. vao .. jazm .. "", "ū")
text = gsub(text, alif .. pesh, "u")
-- do-chashme-he zabar, zer, pesh / no need to mess about
--- works for short vowels
text = gsub(text, "([" .. ZZP .. "])" .. aspirate, "h%1")
text = gsub(text, pesh .. aspirate .. vao .. jazm .. "", "ū")
text = gsub(text, zabar .. aspirate .. vao .. jazm .. "", "ai")
text = gsub(text, '([' .. consonants .. '])' .. aspirate .. ye .. jazm .. "", "%1he")
-- Tashdeed
text = gsub(text, '([' .. consonantS2 .. '])' .. tashdid, "%1%1")
text = gsub(text, '([' .. consonantS2 .. '])' .. tashdid .. '([' .. ZZP .. '])', "%1%1%2")
text = gsub(text, '([' .. ZZP .. '])' .. ye .. '([' .. ZZP .. '])' .. tashdid, "%1yy%2")
text = gsub(text, '([' .. ZZP .. '])' .. vao .. '([' .. ZZP .. '])' .. tashdid, "%1vv%2")
-- For some reason the tashdeed gets pushed after the other diacritics, so this line is necessary for tashdeed to work with other diacritics
text = gsub(text, '([' .. consonants .. '])' .. '([' .. ZZP .. '])' .. tashdid, "%1%1%2")
-- tanween diacritic / no need to mess about
text = gsub(text, '([' .. consonants .. '])' .. 'ً' .. alif, "%1an")
text = gsub(text, alif .. 'ً', "an")
text = gsub(text, '([' .. consonants .. '])' .. 'ً', "%1an")
-- khari zabar -- / no need to mess about
text = gsub(text, '([' .. vowels .. '])' .. 'ٰ', "á")
text = gsub(text, '([' .. consonants .. '])' .. 'ٰ' .. '([' .. vowels .. '])', "%1á")
-- ‘ain
text = gsub(text, alif .. ain , "ā‘")
text = gsub(text, ain .. alif .. '([' .. consonants .. '])', "ʻā%1")
text = gsub(text, '([' .. consonants .. '])' .. ain .. he, "%1ʻa")
text = gsub(text, '([' .. consonants .. '])' .. '([' .. zer .. pesh .. ']?)' .. ain, "%1%2ʻ")
text = gsub(text, ain .. zabar .. vao .. '([' .. consonants .. '])', "‘au%1")
text = gsub(text, ain .. zabar .. ye .. '([' .. consonants .. '])', "‘ai%1")
text = gsub(text, ain .. zer .. '([' .. consonants .. '])', "ʻi%1")
text = gsub(text, ain .. pesh .. '([' .. consonants .. '])', "ʻu%1")
text = gsub(text, ain .. zer .. ye .. '([' .. consonants .. '])', "ʻī%1")
text = gsub(text, ain .. pesh .. vao .. '([' .. consonantS .. '])', "ʻū%1")
--- alif
text = gsub(text, '([' .. consonants .. '])' .. zabar .. alif, "%1ā")
text = gsub(text, '([' .. consonants .. '])' .. alif, "%1ā")
text = gsub(text, '([' .. consonants .. '])' .. tashdid .. alif, "%1%1ā")
-- Vao
text = gsub(text, vao .. '([' .. ZZP .. '])', "v%1")
text = gsub(text, 'ُو', "ū")
text = gsub(text, '([' .. consonants .. '])' .. zabar .. vao .. alif, "%1avā")
-- medial/final consonants
--- (e) -- works
text = gsub(text, '([' .. consonants .. '])' .. ye .. jazm .. '([' .. consonants .. '])', "%1e%2")
text = gsub(text, '([' .. consonants .. '])' .. ye3, "%1e")
text = gsub(text, '([' .. consonants .. '])' .. zabar .. ye3, "%1ai")
--- izafat
text = gsub(text, '([' .. consonants .. '])' .. zer .. " ", "%1-e ")
--- he
text = gsub(text, zabar .. he .. zer .. ye, "ahī")
text = gsub(text, zabar .. he .. alif, "ahā")
text = gsub(text, zabar .. he .. '([' .. consonants .. vowels .. '])', "ah%1")
--- vao
text = gsub(text, '([' .. consonants .. '])' .. vao, "%1o")
text = gsub(text, '([' .. consonants .. '])' .. tashdid .. vao, "%1%1o")
text = gsub(text, '([' .. consonants .. '])' .. tashdid .. zer .. ye .. jazm .. alif, "%1%1īyā")
text = gsub(text, zer .. ye .. jazm .. alif, "īyā")
text = gsub(text, zer .. ye .. alif, "iyā")
--- ye
text = gsub(text, ye .. zabar .. alif, "yā")
text = gsub(text, '([' .. consonants .. zer .. '])' .. ye, "%1ī")
text = gsub(text, "ۂ", "a-e")
text = gsub(text, '.', mapping)
-- Changed these to 'iy(*)', because they will be used for with ی, which are normally written as 'iy'
text = gsub(text, 'īā', "iyā")
text = gsub(text, 'īa', "iya")
text = gsub(text, 'aa', "ā")
--
text = gsub(text, 'ئy', "'ī")
text = gsub(text, "" .. 'ئے', "'ye")
text = gsub(text, "īے", "iye")
text = gsub(text, "iīe", "iye")
text = gsub(text, "īe", "iye")
text = gsub(text, "iīv", "iyo")
text = gsub(text, "ئiy", "'ī")
return text
end
return export
g2qwa29pqqhy4aulplbxklfaag3a1la
Modul:R:it:Sapere
828
145815
375830
2026-09-25T04:18:23Z
Hakimi97
2668
Mencipta laman baru dengan kandungan 'local export = {} function export.create(frame) local params = { [1] = {default = mw.loadData("Module:headword/data").pagename}, [2] = {}, ["sense"] = {}, } local args = require("Module:parameters").process(frame:getParent().args, params) local u = mw.ustring.char -- https://en.wikipedia.org/wiki/Unicode_subscripts_and_superscripts#Superscripts_and_subscripts_block local superscripts = { u(0xb9), u(0xb2), u(0xb3), u(0x2074), u(...'
375830
Scribunto
text/plain
local export = {}
function export.create(frame)
local params = {
[1] = {default = mw.loadData("Module:headword/data").pagename},
[2] = {},
["sense"] = {},
}
local args = require("Module:parameters").process(frame:getParent().args, params)
local u = mw.ustring.char
-- https://en.wikipedia.org/wiki/Unicode_subscripts_and_superscripts#Superscripts_and_subscripts_block
local superscripts = {
u(0xb9),
u(0xb2),
u(0xb3),
u(0x2074),
u(0x2075),
u(0x2076),
u(0x2077),
u(0x2078),
u(0x2079),
}
local term = mw.uri.encode(args[1])
local sense = tonumber(args["sense"] or '')
if sense and sense >= 1 and sense <= 9 then
term = term .. superscripts[sense]
end
local title = (args[2] or args[1]):gsub('_', ' ')
local link = "[https://www.sapere.it/enciclopedia/" .. term .. ".html "
.. title
.. (sense and ('<sup>' .. sense .. '</sup>') or '')
.. "] " ..
"in ''sapere.it'' – De Agostini Editore"
return link
end
return export
2nf2jz931is0adsmr5xnz80qidubs8q
Templat:R:it:Sapere
10
145816
375831
2026-09-25T04:18:43Z
Hakimi97
2668
Mencipta laman baru dengan kandungan '{{#invoke:R:it:Sapere|create}}<noinclude>{{documentation}}</noinclude>'
375831
wikitext
text/x-wiki
{{#invoke:R:it:Sapere|create}}<noinclude>{{documentation}}</noinclude>
m57hhem8w82tqejiclk35re2gb26439
Templat:jeda perenggan
10
145817
375846
2026-09-25T09:12:34Z
SNN95
2113
Mencipta laman baru dengan kandungan '<div class="paragraphbreak" style="margin-top:0.5em"></div><noinclude>{{documentation}}</noinclude>'
375846
wikitext
text/x-wiki
<div class="paragraphbreak" style="margin-top:0.5em"></div><noinclude>{{documentation}}</noinclude>
r8flwbox6t79tbhxblur1xf3iyaqaec
375847
375846
2026-09-25T09:13:26Z
SNN95
2113
SNN95 telah memindahkan laman [[Templat:pb]] ke [[Templat:jeda perenggan]]: BMkan
375846
wikitext
text/x-wiki
<div class="paragraphbreak" style="margin-top:0.5em"></div><noinclude>{{documentation}}</noinclude>
r8flwbox6t79tbhxblur1xf3iyaqaec
Templat:pb
10
145818
375848
2026-09-25T09:13:26Z
SNN95
2113
SNN95 telah memindahkan laman [[Templat:pb]] ke [[Templat:jeda perenggan]]: BMkan
375848
wikitext
text/x-wiki
#LENCONG [[Templat:jeda perenggan]]
ft8szevwg3ogj2jfvytrpm1qidkjpko
Templat:RQ:New Yorker
10
145819
375850
2026-09-25T09:17:14Z
SNN95
2113
Mencipta laman baru dengan kandungan '{{#invoke:quote|call_quote_template |template=quote-journal |en |author = {{{author|{{{1|}}}}}} |title = {{#if:{{{title|{{{2|}}}}}} | {{#invoke:string/templates|replace|{{{title|{{{2|}}}}}}|'|’}} | {{maintenance line|sila nyatakan tajuk artikel}} }} |series = {{#if:{{{section|}}} | Seksyen {{{section}}} }} |newspaper = w:The New Yorker |location = New York, N.Y. |publisher = w:Condé Nast Publications |year...'
375850
wikitext
text/x-wiki
{{#invoke:quote|call_quote_template
|template=quote-journal
|en
|author = {{{author|{{{1|}}}}}}
|title = {{#if:{{{title|{{{2|}}}}}}
| {{#invoke:string/templates|replace|{{{title|{{{2|}}}}}}|'|’}}
| {{maintenance line|sila nyatakan tajuk artikel}}
}}
|series = {{#if:{{{section|}}}
| Seksyen {{{section}}}
}}
|newspaper = w:The New Yorker
|location = New York, N.Y.
|publisher = w:Condé Nast Publications
|year = {{{year|{{{3|}}}}}}
|issn = 0028-792X
|oclc = 320541675
|textparam = 4
|footer = {{#if:{{{footer|}}}|{{small|{{{footer}}}}}}}{{#if:{{{url|}}}{{{titleurl|}}}|{{#if:{{{archiveurl|}}}||{{#if:{{{footer|}}}|. }}{{maintenance line|Bolehkah kita [[Wikikamus:Petikan/Sumber#Cara mengarkibkan laman web|arkib]] [[URL]] ini?}}}}}}
|propagateparams = author2,author3,author4,author5,authorlink,authorlink2,authorlink3,authorlink4,authorlink5,quotee,url,archiveurl,archivedate,accessdate,date,date_published,month,volume,issue,page,pageurl,pages,column,termlang
|allowparams = author,1,title,2,year,3,section
}}<noinclude>{{documentation}}</noinclude>
m3wu4emum87kfqb8ikl3277wmx4t9ft
375853
375850
2026-09-25T09:30:54Z
SNN95
2113
375853
wikitext
text/x-wiki
{{#invoke:quote|call_quote_template
|template=quote-journal
|en
|author = {{{author|{{{1|}}}}}}
|title = {{#if:{{{title|{{{2|}}}}}}
| {{#invoke:string/templates|replace|{{{title|{{{2|}}}}}}|'|’}}
| {{maintenance line|sila nyatakan tajuk artikel}}
}}
|series = {{#if:{{{section|}}}
| Seksyen {{{section}}}
}}
|newspaper = w:The New Yorker
|location = New York, N.Y.
|publisher = w:Condé Nast Publications
|year = {{{year|{{{3|}}}}}}
|issn = 0028-792X
|oclc = 320541675
|textparam = 4
|footer = {{#if:{{{footer|}}}|{{small|{{{footer}}}}}}}{{#if:{{{url|}}}{{{titleurl|}}}|{{#if:{{{archiveurl|}}}||{{#if:{{{footer|}}}|. }}{{maintenance line|Bolehkah kita [[Wikikamus:Petikan/Sumber#Cara mengarkibkan laman web|arkib]] [[URL]] ini?}}}}}}
|propagateparams = author2,author3,author4,author5,authorlink,authorlink2,authorlink3,authorlink4,authorlink5,quotee,url,archiveurl,archivedate,accessdate,date,date_published,month,volume,issue,page,pageurl,pages,column,termlang
|allowparams = author,1,title,2,year,3,section,translation
}}<noinclude>{{documentation}}</noinclude>
6cyq9y83nidx9a7qh9n245nddn79qw3
375854
375853
2026-09-25T09:33:34Z
SNN95
2113
375854
wikitext
text/x-wiki
{{#invoke:quote|call_quote_template
|template=quote-journal
|en
|author = {{{pengarang|{{{penulis|{{{author|{{{1|}}}}}}}}}}}}
|title = {{#if:{{{title|{{{2|}}}}}}
| {{#invoke:string/templates|replace|{{{tajuk|{{{title|{{{2|}}}}}}}}}|'|’}}
| {{maintenance line|sila nyatakan tajuk artikel}}
}}
|series = {{#if:{{{section|}}}{{{seksyen|}}}
| Seksyen {{{seksyen|{{{section}}}}}}
}}
|newspaper = w:The New Yorker
|location = New York, N.Y.
|publisher = w:Condé Nast Publications
|year = {{{tahun|{{{year|{{{3|}}}}}}}}}
|issn = 0028-792X
|oclc = 320541675
|textparam = 4
|translation = {{{translation|{{{translate|}}}}}}
|footer = {{#if:{{{footer|}}}|{{small|{{{footer}}}}}}}{{#if:{{{url|}}}{{{titleurl|}}}|{{#if:{{{archiveurl|}}}||{{#if:{{{footer|}}}|. }}{{maintenance line|Bolehkah kita [[Wikikamus:Petikan/Sumber#Cara mengarkibkan laman web|arkib]] [[URL]] ini?}}}}}}
|propagateparams = author2,author3,author4,author5,authorlink,authorlink2,authorlink3,authorlink4,authorlink5,quotee,url,archiveurl,archivedate,accessdate,date,date_published,month,volume,issue,page,pageurl,pages,column,termlang
|allowparams = author,1,title,2,year,3,section,translation
}}<noinclude>{{documentation}}</noinclude>
rcsvkduxt97693wfnayjd24cbb9vaqo
MediaWiki:Gadget-EtytreeZoom.js
8
145820
375857
2026-09-25T09:38:32Z
PeaceSeekers
3334
Mencipta laman baru dengan kandungan '// <nowiki> const elem = s => document.createRange().createContextualFragment(s).firstChild; // helper mw.util.addCSS(` .etytree-scale { position: absolute; right: 0; padding: 0.5em; z-index: 100; } .etytree { position: relative; } .etytree-body { transform-origin: top left; } `); function zoomOut(tree) { let navContent = tree.parentElement; let scale = navContent.clientWidth / tree.scrollWidth; tree.style.transform = `scale($...'
375857
javascript
text/javascript
// <nowiki>
const elem = s => document.createRange().createContextualFragment(s).firstChild; // helper
mw.util.addCSS(`
.etytree-scale {
position: absolute;
right: 0;
padding: 0.5em;
z-index: 100;
}
.etytree {
position: relative;
}
.etytree-body {
transform-origin: top left;
}
`);
function zoomOut(tree) {
let navContent = tree.parentElement;
let scale = navContent.clientWidth / tree.scrollWidth;
tree.style.transform = `scale(${scale})`;
navContent.style.overflow = "hidden"; // hide scrollbars
navContent.scrollLeft = 0;
navContent.style.height = `${tree.scrollHeight * scale}px`;
}
function resetZoom(tree) {
let navContent = tree.parentElement;
tree.style.transform = "";
navContent.style.overflow = "auto";
navContent.style.height = "";
}
mw.hook("wikipage.content").add(function($content) {
for (let etytree of $(".etytree", $content).toArray()) {
let navContent = etytree.querySelector(".NavContent");
let tree = navContent.firstElementChild;
let etytreeScale = elem(`<div class="etytree-scale"><button class="etytree-scale-toggle cdx-button">Zoom out</button></div>`.trim());
let toggle = etytreeScale.querySelector(".etytree-scale-toggle");
navContent.prepend(etytreeScale);
etytree.treeZoomedOut = false;
toggle.addEventListener("click", () => {
if (toggle.textContent === "Zoom out") {
zoomOut(tree);
etytree.treeZoomedOut = true;
toggle.textContent = "Zoom in";
} else {
resetZoom(tree);
etytree.treeZoomedOut = false;
toggle.textContent = "Zoom out";
}
});
// The button is hidden if there's no horizontal scrollbar.
etytreeScale.style.display = navContent.scrollWidth > navContent.clientWidth ? "" : "none";
(new ResizeObserver(() => {
for (let etytree of $(".etytree", $content).toArray()) {
if (etytree.treeZoomedOut) // redo the zoom out
zoomOut(etytree.querySelector(".etytree-body"));
let etytreeScale = etytree.querySelector(".etytree-scale");
etytreeScale.style.display = navContent.scrollWidth > navContent.clientWidth ? "" : "none";
}
})).observe(navContent);
}
});
// </nowiki>
fvrbia4vf9yzsk2lm2lidjrgrmhnqvb
MediaWiki:Gadget-TreeDescs-pagestyles.css
8
145821
375858
2026-09-25T09:39:03Z
PeaceSeekers
3334
Mencipta laman baru dengan kandungan '/* code forked from [[w:Template:Tree list/styles.css]] */ .treeview ul { padding: 0; margin: 0; } .treeview li li { margin: 0; list-style-type: none; list-style-image: none; background: url("https://upload.wikimedia.org/wikipedia/commons/f/f2/Treeview-grey-line.png") no-repeat 0 -2979.5px; /* @noflip */ padding: 0 0 0 21px; text-indent: 0.3em; } .treeview li li:last-child { background-position: 0 -5969.5px } /* first line here deals with...'
375858
css
text/css
/* code forked from [[w:Template:Tree list/styles.css]] */
.treeview ul {
padding: 0;
margin: 0;
}
.treeview li li {
margin: 0;
list-style-type: none;
list-style-image: none;
background: url("https://upload.wikimedia.org/wikipedia/commons/f/f2/Treeview-grey-line.png") no-repeat 0 -2979.5px;
/* @noflip */
padding: 0 0 0 21px;
text-indent: 0.3em;
}
.treeview li li:last-child {
background-position: 0 -5969.5px
}
/* first line here deals with new situation after RemexHTML switch,
* where emptyline cause the first child to become the 2nd child. Such a mess
*/
.treeview li.emptyline > ul > .mw-empty-elt:first-child + .emptyline,
.treeview li.emptyline > ul > li:first-child {
background-position: 0 9px
}
/* additional code for Wiktionary */
.treeview li dd {
margin: 0;
}
.treeview li dl {
margin: 0;
padding-left: 21px;
}
.treeview li dl:not(:last-child) {
background: url("https://upload.wikimedia.org/wikipedia/commons/f/f2/Treeview-grey-line.png") no-repeat 0 -2px;
/* @noflip */
padding-left: 21px;
text-indent: 0.3em;
}
.treeview .desc-arr {
color: var(--wikt-palette-grey-9, grey);
}
.treeview li li .desc-arr {
vertical-align: top;
}
lz282qet5qzkmuxcslusjro6a7akd85
Modul:en-headword/testcases
828
145822
375860
2026-09-25T11:20:30Z
SNN95
2113
Mencipta laman baru dengan kandungan 'local export = {} local pos_functions = {} --[==[ Author from 2020 on: mostly Benwing2, with significant contributions from Theknightwho. Based on a prior version by Rua (by now mostly rewritten), with contributions from Erutuon and others (see history for full attribution). ]==] local force_cat = false -- for testing; if true, categories appear in non-mainspace pages local require = require local require_when_needed = require("Module:require when n...'
375860
Scribunto
text/plain
local export = {}
local pos_functions = {}
--[==[
Author from 2020 on: mostly Benwing2, with significant contributions from Theknightwho. Based on a prior version by Rua
(by now mostly rewritten), with contributions from Erutuon and others (see history for full attribution).
]==]
local force_cat = false -- for testing; if true, categories appear in non-mainspace pages
local require = require
local require_when_needed = require("Module:require when needed")
local en_utilities_module = "Module:en-utilities"
local headword_utilities_module = "Module:headword utilities"
local headword_module = "Module:headword"
local inflection_utilities_module = "Module:inflection utilities"
local parse_utilities_module = "Module:parse utilities"
local JSON_module = "Module:JSON"
local labels_module = "Module:labels"
local links_module = "Module:links"
local parameters_module = "Module:parameters"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local utilities_module = "Module:utilities"
local yesno_module = "Module:yesno"
local iut = require_when_needed(inflection_utilities_module)
local put = require_when_needed(parse_utilities_module)
local m_headword_utilities = require_when_needed(headword_utilities_module)
local add_links_to_multiword_term = require_when_needed(headword_utilities_module, "add_links_to_multiword_term")
local add_suffix = require_when_needed(en_utilities_module, "add_suffix")
local apply_link_modifiers = require_when_needed(headword_utilities_module, "apply_link_modifiers")
local concat = table.concat
local deepEquals = require_when_needed(table_module, "deepEquals")
local dump = mw.dumpObject
local format_categories = require_when_needed(utilities_module, "format_categories")
local full_headword = require_when_needed(headword_module, "full_headword")
local get_label_info = require_when_needed(labels_module, "get_label_info")
local get_link_page = require_when_needed(links_module, "get_link_page")
local glossary_link = require_when_needed(headword_utilities_module, "glossary_link")
local insert = table.insert
local insertIfNot = require_when_needed(table_module, "insertIfNot")
local ipairs = ipairs
local is_regular_plural = require_when_needed(en_utilities_module, "is_regular_plural")
local list_to_set = require_when_needed(table_module, "listToSet")
local pairs = pairs
local process_params = require_when_needed(parameters_module, "process")
local remove = table.remove
local remove_links = require_when_needed(links_module, "remove_links")
local replacement_escape = require_when_needed(string_utilities_module, "replacement_escape")
local shallowCopy = require_when_needed(table_module, "shallowCopy")
local singularize = require_when_needed(en_utilities_module, "singularize")
local split = require_when_needed(string_utilities_module, "split")
local toJSON = require_when_needed(JSON_module, "toJSON")
local toNFD = mw.ustring.toNFD
local type = type
local ulen = require_when_needed(string_utilities_module, "len")
local ulower = require_when_needed(string_utilities_module, "lower")
local umatch = require_when_needed(string_utilities_module, "match")
local u = require_when_needed(string_utilities_module, "char")
local ugsub = require_when_needed(string_utilities_module, "gsub")
local lang = require("Module:languages").getByCode("en")
local langname = lang:getCanonicalName()
local list_param = {list = true, disallow_holes = true}
local list_allow_holes = {list = true, allow_holes = true}
local boolean_param = {type = "boolean"}
local function ine(val)
if val == "" then return nil else return val end
end
local function track(page)
require("Module:debug/track")("en-headword/" .. page)
return true
end
------------------------------------------- UTILITY FUNCTIONS ------------------------------------------
-- Parse and return an inflection not requiring additional processing. The raw arguments come from `args[field]`, which
-- is parsed for inline modifiers.
local function parse_inflection(args, field, is_head)
local argfield = field
if type(argfield) == "table" then
argfield = argfield[1]
end
return m_headword_utilities.parse_term_list_with_modifiers {
paramname = field,
forms = args[argfield],
splitchar = ",",
is_head = is_head,
}
end
-- Insert the parsed inflections in `terms` (as parsed by `parse_inflection`) into `data.inflections`, with label
-- `label` and optional accelerator spec `accel`.
local function insert_inflection(data, terms, label, accel, no_label)
for _, termobj in ipairs(terms) do
m_headword_utilities.remove_termobj_field_modifiers(termobj)
end
m_headword_utilities.insert_inflection {
headdata = data,
terms = terms,
label = label,
no_label = no_label,
accel = accel and {form = accel} or nil,
}
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. `label` is the label that the inflections are given;
-- `accel` is the accelerator form, or nil.
local function parse_and_insert_inflection(data, args, field, label, accel)
m_headword_utilities.parse_and_insert_inflection {
headdata = data,
forms = args[field],
paramname = field,
splitchar = ",",
label = label,
accel = accel and {form = accel} or nil,
}
end
-- These functions are used directly in the <> format as well as in the utility functions #2 below.
local function compute_double_last_cons_stem(term)
local last_cons = term:match("([bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])$")
if not last_cons then
error("Verb stem '" .. term .. "' must end in a consonant to use ++")
end
return term .. last_cons
end
local function compute_plusplus_s_form(term, default_s_form)
if term:find("[szx]$") then
-- regas -> regasses, derez -> derezzes
return compute_double_last_cons_stem(term) .. "es"
else
return default_s_form
end
end
-- The main entry point.
-- This is the only function that can be invoked from a template.
function export.show(frame)
local iparams = {
[1] = true,
}
local iargs = require("Module:parameters").process(frame.args, iparams)
local parargs = frame:getParent().args
local poscat = iargs[1]
local pos_in_1 = not poscat
if pos_in_1 then
poscat = ine(parargs[1]) or
mw.title.getCurrentTitle().fullText == "Template:en-head" and "interjection" or
error("Part of speech must be specified in 1=")
poscat = require(headword_module).canonicalize_pos(poscat)
end
local indexing_poscat = pos_in_1 and "head" or poscat
local params = {
["head"] = list_param,
["id"] = true,
["json"] = boolean_param,
["sort"] = true,
["splithyph"] = boolean_param,
["nosplithyph"] = boolean_param,
["hyphspace"] = boolean_param,
["nolink"] = boolean_param,
["nolinkhead"] = {type = "boolean_param", alias_of = "nolink"},
["suffix"] = boolean_param,
["nosuffix"] = boolean_param,
["nomultiwordcat"] = boolean_param,
["abbr"] = list_param,
["the"] = true,
["def"] = {alias_of = "the"},
["pagename"] = true, -- for testing
}
if pos_in_1 then
params[1] = {required = true} -- required but ignored as already processed above
end
local pos_data = pos_functions[indexing_poscat]
local pos_func
if pos_data then
local pos_params = pos_data.params
if pos_params then
for key, val in pairs(pos_params) do
params[key] = val
end
end
pos_func = pos_data.func
end
local args = process_params(parargs, params)
-- Account for unsupported titles, e.g. 'C|N>K' instead of 'Unsupported titles/C through N to K'.
local pagename = args.pagename or mw.loadData("Module:headword/data").pagename
local user_specified_heads = parse_inflection(args, "head", "is_head")
local heads = user_specified_heads
local autohead
if args.nolink or not pagename:find("[ '%-]") then
autohead = pagename
else
local en_no_split_apostrophe_words = list_to_set {
"one's",
"someone's",
"he's",
"she's",
"it's",
}
local en_include_hyphen_prefixes = list_to_set {
-- We don't include things that are also words even though they are often (perhaps mostly) prefixes, e.g.
-- "be", "counter", "cross", "extra", "half", "mid", "over", "pan", "under".
"acro",
"acousto",
"Afro",
"agro",
"anarcho",
"angio",
"Anglo",
"ante",
"anti",
"arch",
"auto",
"bi",
"bio",
"cis",
"co",
"cryo",
"crypto",
"de",
"demi",
"eco",
"electro",
"Euro",
"ex",
"Greco",
"hemi",
"hydro",
"hyper",
"hypo",
"infra",
"Indo",
"inter",
"intra",
"Judeo",
"macro",
"meta",
"micro",
"mini",
"multi",
"neo",
"neuro",
"non",
"para",
"peri",
"post",
"pre",
"pro",
"proto",
"pseudo",
"re",
"semi",
"sub",
"super",
"trans",
"un",
"vice",
}
local function is_english(term)
local title = mw.title.new(term)
if title and title.exists then
local content = title:getContent()
if content and content:find("==English==\n") then
return true
end
end
return false
end
local function en_split_hyphen_when_space(word)
if not word:find("-", nil, true) then
return nil
end
if args.hyphspace then
return "[[" .. word:gsub("%-+", " ") .. "|" .. word .. "]]"
end
if args.nosplithyph then
return "[[" .. word .. "]]"
end
if not args.splithyph then
local space_word = word:gsub("%-+", " ")
if is_english(space_word) then
return "[[" .. space_word .. "|" .. word .. "]]"
end
if is_english(word) then
return "[[" .. word .. "]]"
end
end
return nil
end
local function en_split_apostrophe(word)
local base = word:match("^(.*)'s$")
if base then
return "[[" .. base .. "]][[-'s|'s]]"
end
-- Only treat final apostrophe as possessive if preceded by something that looks like a plural ending in /z/.
-- In particular we don't want to do it for words like [[truckin']].
base = word:match("^(.*[sxz])'$")
if base then
if base:find("s$") then
local sg = singularize(base)
if is_english(sg) then
return "[[" .. sg .. "|" .. base .. "]][[-'|']]"
end
end
return "[[" .. base .. "]][[-'|']]"
end
return "[[" .. word .. "]]"
end
autohead = add_links_to_multiword_term(pagename, {
split_hyphen_when_space = en_split_hyphen_when_space,
split_apostrophe = en_split_apostrophe,
no_split_apostrophe_words = en_no_split_apostrophe_words,
include_hyphen_prefixes = en_include_hyphen_prefixes,
})
end
if not heads[1] then
heads = {{term = autohead}}
else
for _, headobj in ipairs(heads) do
local head = headobj.term
if head:find("^~") then
head = apply_link_modifiers(autohead, head:sub(2), lang)
headobj.term = head
elseif head:find("^[!?]$") then
-- If explicit head= just consists of ! or ?, add it to the end of the default head.
headobj.term = autohead .. head
end
if head == autohead then
track("redundant-head")
end
end
end
-- handle the=/def=
if args.the == "~" then
local newheads = {}
for _, headobj in ipairs(heads) do
local barehead = shallowCopy(headobj)
insert(newheads, barehead)
headobj.term = "the " .. headobj.term
insert(newheads, headobj)
end
heads = newheads
elseif args.the then
local the = require(yesno_module)(args.the)
if the then
for _, headobj in ipairs(heads) do
headobj.term = "the " .. headobj.term
end
end
end
local data = {
lang = lang,
pos_category = poscat,
categories = {},
heads = heads,
user_specified_heads = user_specified_heads,
-- We use our own splitting algorithm so the redundant head cat will be inaccurate.
no_redundant_head_cat = true,
inflections = {},
nomultiwordcat = args.nomultiwordcat,
sort_key = args.sort,
pagename = pagename,
id = args.id,
force_cat_output = force_cat,
}
local function inscat(cat)
insert(data.categories, langname .. " " .. cat)
end
local is_suffix = false
if args.suffix or not args.nosuffix and pagename:find("^%-") and not pagename:find("^%-%-") and poscat ~= "suffix forms" then
is_suffix = true
data.pos_category = "suffixes"
local singular_poscat = singularize(poscat)
inscat(singular_poscat .. "-forming suffixes")
insert(data.inflections, {label = singular_poscat .. "-forming suffix"})
end
if pos_func then
pos_func(args, data, is_suffix)
end
local extra_categories = {}
if pagename:find("[Qq]") then
-- Check for q not followed by u. We want to exclude things like [[13q deletion syndrome]] and [[BFOQ]] that
-- don't have a lowercase letter on either side, as well as things like [[& seq.]] and [[acq.]] that are
-- abbreviations for words containing a following u.
--
-- Approximate range of combining diacritics; we want to remove them so the checks below for
-- a lowercase letter next to the q aren't tripped up by diacritics on the letter.
local u300 = u(0x0300)
local u36F = u(0x036F)
local pagename_no_diacritics = ugsub(toNFD(pagename), "[" .. u300 .. "-" .. u36F .. "]", "")
if pagename_no_diacritics:find("[Qq][a-tv-z]") or pagename_no_diacritics:find("[a-z]q[^u.]") or
pagename_no_diacritics:find("[a-z]q$") then
inscat("words containing Q not followed by U")
end
end
-- toNFD performs decomposition, so letters that decompose to an ASCII
-- vowel and a diacritic, such as é, are counted as vowels and do not do not
-- need to be included in the pattern.
if not umatch(ulower(toNFD(pagename)), "[aeiouyæœøəªºαεηιουω]") then
inscat("words spelled without vowels")
end
if pagename:find("yre$") then
inscat('words ending in "-yre"')
end
if not pagename:find(" ") and ulen(pagename) >= 25 then
insert(extra_categories, "Long " .. langname .. " words")
end
if pagename:find("^[^aeiou ]*a[^aeiou ]*e[^aeiou ]*i[^aeiou ]*o[^aeiou ]*u[^aeiou ]*$") then
inscat("words that use all vowels in alphabetical order")
end
parse_and_insert_inflection(data, args, "abbr", "abbreviation")
if args.json then
return toJSON(data)
end
return full_headword(data)
.. (extra_categories[1]
and format_categories(extra_categories, lang, args.sort)
or "")
end
local function make_default_comparative(word)
if word == "good" or word == "well" then
return {"better"}
elseif word == "bad" or word == "badly" then
return {"worse"}
elseif word == "far" then
return {"further", "farther"}
else
return {add_suffix(word, "r")}
end
end
local function make_default_superlative(word)
if word == "good" or word == "well" then
return {"best"}
elseif word == "bad" or word == "badly" then
return {"worst"}
elseif word == "far" then
return {"furthest", "farthest"}
else
return {add_suffix(word, "st.superlative")}
end
end
-- This function does the common work between adjectives and adverbs.
local function process_comparative_args(data, args, plpos)
local pagename = data.pagename
local comps = parse_inflection(args, 1)
local sups = parse_inflection(args, "sup")
local outcomps, outsups
if args.componly then
if comps[1] then
error("Can't specify comparatives of comparative-only " .. plpos)
end
insert(data.inflections, {label = glossary_link("comparative") .. " form only"})
insert(data.categories, langname .. " comparative-only " .. plpos)
-- Set to empty list so we don't get any comparatives output, but process superlatives if specified.
outcomps = {}
if not sups[1] then
-- Set to empty list so we don't get any superlatives output unless explicitly given.
outsups = {}
end
elseif args.suponly then
if comps[1] or sups[1] then
error("Can't specify comparatives or superlatives of or superlative-only " .. plpos)
end
insert(data.inflections, {label = glossary_link("superlative") .. " form only"})
insert(data.categories, langname .. " superlative-only " .. plpos)
return
end
-- If the first parameter is ?, then don't show anything, just return.
if comps[1] and comps[1].term == "?" then
if comps[2] then
error("Can't specify additional comparatives along with '?'")
end
if sups[1] then
error("Can't specify superlatives along with '?' for the comparative")
end
return
end
if comps[1] and comps[1].term == "-" then
local hyphencomp = remove(comps, 1) -- Remove the "-" but retain for decorations.
-- Not (generally) comparable; may occasionally have a comparative
if comps[1] then
insert_fixed_inflection(data, "not generally <<comparable>>", hyphencomp)
elseif not sups[1] then
insert_fixed_inflection(data, "not <<comparable>>", hyphencomp)
insert(data.categories, langname .. " uncomparable " .. plpos)
return
else
-- No comparative, but a superlative. insert_inflection() will correctly generate 'no comparative' if we
-- pass in "-" as the value.
outcomps = {hyphencomp}
end
elseif not comps[1] then
comps = {{term = "more"}}
end
if not outcomps then -- not if we set `outcomps` to "-" above or processed a comparative-only term
outcomps = {}
-- Go over each parameter given and create a comparative and superlative form.
for _, compobj in ipairs(comps) do
local comp = compobj.term
if comp == "-" then
error("Comparative of '-' only allowed as first comparative")
end
if comp == "+" then
comp = "+more"
elseif comp == "more" and pagename ~= "many" and pagename ~= "much" then
comp = "+more"
elseif comp == "further" and pagename ~= "far" then
comp = "+further"
elseif comp == "better" and pagename ~= "good" and pagename ~= "well" then
comp = "+better"
elseif comp:find("~") then
comp = comp:gsub("~", replacement_escape(pagename))
end
compobj.origterm = comp
if comp == "+more" then
comp = "more [[" .. pagename .. "]]"
elseif comp == "+further" then
comp = {"further [[" .. pagename .. "]]", "farther [[" .. pagename .. "]]"}
elseif comp == "+better" then
comp = "better [[" .. pagename .. "]]"
elseif comp == "er" then
-- Add -er.
comp = add_suffix(pagename, "r")
elseif comp == "ier" then
if pagename:sub(-1) ~= "y" then
error("Can't specify 'ier' comparative unless the term ends with 'y': " .. pagename)
end
comp = pagename:gsub("e?y$", "ier")
elseif comp:find("^%+") then
local special = m_headword_utilities.get_special_indicator(comp, "noerror")
if special then
comp = m_headword_utilities.handle_multiword(pagename, special, make_default_comparative)
end
end
if type(comp) == "table" and not comp[2] then
comp = comp[1]
end
if type(comp) == "table" then
for i = 1, #comp - 1 do
local outobj = shallowCopy(compobj)
outobj.term = comp[i]
insert(outcomps, outobj)
end
compobj.term = comp[#comp]
insert(outcomps, compobj)
else
compobj.term = comp
insert(outcomps, compobj)
end
end
end
if sups[1] and sups[1].term == "-" then
if sups[2] then
error("Can't specify '-' as superlative followed by further values")
end
-- No superlative. insert_inflection() will correctly generate 'no superlative' if we pass in "-" as the value.
outsups = sups
else
if not sups[1] then
sups = {{term = "+"}}
end
end
-- `outsups` will be set if we set `outsups` to "-" above or processed a comparative-only term without superlatives.
if not outsups then
outsups = {}
local function process_sup(sup, special, supobj, compobj)
if special then
sup = m_headword_utilities.handle_multiword(pagename, special, make_default_superlative)
elseif sup == "-" or sup == "+" then
error(("Internal error: Superlative value of '%s' should have been handled earlier"):format(sup))
elseif sup == "+most" then
sup = "most [[" .. pagename .. "]]"
elseif sup == "+furthest" then
sup = {"furthest [[" .. pagename .. "]]", "farthest [[" .. pagename .. "]]"}
elseif sup == "+best" then
sup = "best [[" .. pagename .. "]]"
elseif sup == "est" then
-- Add -est.
sup = add_suffix(pagename, "st.superlative")
elseif sup == "iest" then
if pagename:sub(-1) ~= "y" then
error("Can't specify 'iest' superlative unless the term ends with 'y': " .. pagename)
end
sup = pagename:gsub("e?y$", "iest")
end
if type(sup) == "table" and not sup[2] then
sup = sup[1]
end
if compobj then
supobj = shallowCopy(supobj)
supobj = m_headword_utilities.combine_termobj_decorations(supobj, compobj)
end
if type(sup) == "table" then
for i = 1, #sup - 1 do
local outobj = shallowCopy(supobj)
outobj.term = sup[i]
insert(outsups, outobj)
end
supobj.term = sup[#sup]
insert(outsups, supobj)
else
supobj.term = sup
insert(outsups, supobj)
end
end
for _, supobj in ipairs(sups) do
local sup = supobj.term
if sup == "-" then
error("Superlative of '-' only allowed as first superlative")
end
if sup == "+" then
if not comps[1] then
error("Superlative of '+' can't be specified when there are no comparatives")
end
for _, compobj in ipairs(comps) do
local comp = compobj.origterm
local special
if comp == "+more" then
sup = "+most"
elseif comp == "+further" then
sup = "+furthest"
elseif comp == "+better" then
sup = "+best"
elseif comp == "er" then
sup = "est"
elseif comp == "ier" then
sup = "iest"
else
if comp:find("^%+") then
special = m_headword_utilities.get_special_indicator(comp, "noerror")
end
if not special then
-- If the full comparative was given, then derive the superlative by replacing -er with
-- -est.
if comp:sub(-2) == "er" then
sup = comp:sub(1, -3) .. "est"
else
error(("The superlative cannot be derived automatically from comparative '%s' because it doesn't end in -er"):format(comp))
end
end
end
process_sup(sup, special, supobj, compobj)
end
else
local special = m_headword_utilities.get_special_indicator(sup, "noerror")
-- Do some work here rather than in process_sup() so we don't end up double-processing a term with a '~'
-- in it or a term that happens to be 'most' or similar after substitution of ~ in the comparative.
if not special then
if sup == "most" and pagename ~= "many" and pagename ~= "much" then
sup = "+most"
elseif sup == "furthest" and pagename ~= "far" then
sup = "+furthest"
elseif sup == "best" and pagename ~= "good" and pagename ~= "well" then
sup = "+best"
elseif sup:find("~") then
sup = sup:gsub("~", replacement_escape(pagename))
end
end
process_sup(sup, special, supobj)
end
end
end
insert_inflection(data, outcomps, "<<comparative>>", "comparative")
insert_inflection(data, outsups, "<<superlative>>", "superlative")
end
pos_functions["adjectives"] = {
params = {
[1] = list_param,
["comp_qual"] = {list = "comp\1_qual", allow_holes = true, replaced_by = false,
instead = "use <l:...> or <q:...> inline modifier on the comparative value",
},
["sup"] = list_param,
["sup_qual"] = {list = "sup\1_qual", allow_holes = true, replaced_by = false,
instead = "use <l:...> or <q:...> inline modifier on the superlative value",
},
["componly"] = boolean_param,
["suponly"] = boolean_param,
},
func = function(args, data)
-- Process the comparatives and superlatives.
process_comparative_args(data, args, "adjectives")
end,
}
pos_functions["adverbs"] = {
params = {
[1] = list_param,
["comp_qual"] = {list = "comp\1_qual", allow_holes = true, replaced_by = false,
instead = "use <l:...> or <q:...> inline modifier on the comparative value",
},
["sup"] = list_param,
["sup_qual"] = {list = "sup\1_qual", allow_holes = true, replaced_by = false,
instead = "use <l:...> or <q:...> inline modifier on the superlative value",
},
["componly"] = boolean_param,
["suponly"] = boolean_param,
},
func = function(args, data)
-- Process the comparatives and superlatives.
process_comparative_args(data, args, "adverbs")
end,
}
local function escape(str)
return (str:gsub("\\([:#])", "\\\\%1")
:gsub("[:#]", "\\%0"))
end
local function canonicalize_plural(pl, pagename, pos)
if pl == "+" then
return escape(add_suffix(pagename, "s.plural", pos))
elseif pl == "++" then
return escape(compute_plusplus_s_form(pagename, add_suffix(pagename, "s.plural", pos)))
elseif pl == "*" then
return escape(pagename)
elseif pl == "ies" then
if pagename:sub(-1) == "y" then
return escape(pagename:gsub("e?y$", pl))
end
error("Can't specify 'ies' plural unless the term ends with 'y'.")
elseif pl == "s" or pl == "es" or pl == "'s" then
return escape(pagename .. pl)
end
end
local function do_nouns(args, data, pos)
local pagename = data.pagename
pos = pos or "noun"
local plurals = parse_inflection(args, 1)
local function insert_plurale_tantum_inflections(is_plural_only, originating_label)
if args.sg[1] then
insert_fixed_inflection(data, "normally plural", originating_label)
parse_and_insert_inflection(data, args, "sg", "singular")
elseif is_plural_only then
insert_fixed_inflection(data, "plural only", originating_label)
end
if args.attr[1] then
parse_and_insert_inflection(data, args, "attr", "attributive")
end
end
local function first_pl_term()
return plurals[1] and plurals[1].term or nil
end
if first_pl_term() == "p" then
-- plurale tantum
if plurals[2] then
error("With plurale tantum noun, can't specify more than one plural")
end
data.genders = {"p"} -- this should auto-insert the correct 'pluralia tantum' category
insert_plurale_tantum_inflections("plural only", plurals[1])
return
end
local function inscat(cat)
insert(data.categories, langname .. " " .. cat)
end
local need_default_plural = pos == "noun"
if first_pl_term() == "sp" then
-- construed as singular or plural
sp = remove(plurals, 1) -- Remove the "sp" but retain it for its decorations.
inscat("nouns construed as singular or plural")
data.genders = {"s", "p"} -- this should auto-insert the correct 'pluralia tantum' category
insert_plurale_tantum_inflections(nil, sp)
need_default_plural = false
elseif first_pl_term() == "-" then
-- Uncountable noun; may occasionally have a plural
local hyphpl = remove(plurals, 1) -- Remove the "-" but retain for decorations.
inscat("uncountable nouns")
-- If plural forms were given explicitly, then show "usually"
if plurals[1] then
insert_fixed_inflection(data, "usually <<uncountable>>", hyphpl)
else
insert_fixed_inflection(data, "<<uncountable>>", hyphpl)
end
need_default_plural = false
elseif first_pl_term() == "#" then
-- Usually countable (e.g., "grilled cheese")
local hashpl = remove(plurals, 1) -- Remove the "#" but retain for decorations.
insert_fixed_inflection(data, "usually <<countable>>", hashpl)
inscat("uncountable nouns")
inscat("countable nouns")
-- If no plural was given, add a default one now
if not plurals[1] then
plurals[1] = {term = escape(add_suffix(pagename, "s.plural", pos))}
end
elseif first_pl_term() == "~" then
-- Mixed countable/uncountable noun, always has a plural
local tildepl = remove(plurals, 1) -- Remove the "~" but retain for decorations.
insert_fixed_inflection(data, "<<countable>> and <<uncountable>>", tildepl)
inscat("uncountable nouns")
inscat("countable nouns")
-- If no plural was given, add a default one now
if not plurals[1] then
plurals[1] = {term = escape(add_suffix(pagename, "s.plural", pos))}
end
end
-- Plural is unknown
if first_pl_term() == "?" then
local questionpl = remove(plurals, 1) -- Remove the "?" but retain for decorations.
-- Not desired; see [[Wiktionary:Tea_room/2021/August#"Plural unknown or uncertain"]]
-- insert_fixed_inflection(data, "plural unknown or uncertain", questionpl)
inscat("nouns with unknown or uncertain plurals")
if plurals[1] then
error("Can't specify explicit plurals along with '?' for unknown/uncertain plural")
end
return
end
-- Plural is not attested
if first_pl_term() == "!" then
local exclampl = remove(plurals, 1) -- Remove the "!" but retain for decorations.
insert_fixed_inflection(data, "plural not attested", exclampl)
inscat("nouns with unattested plurals")
if plurals[1] then
error("Can't specify explicit plurals along with '!' for unattested plural")
end
return
end
-- If no plural was given, maybe add a default one, otherwise (when "-" was given or proper noun) return.
if not plurals[1] then
if not need_default_plural then
inscat("uncountable nouns")
return
end
plurals[1] = {term = escape(add_suffix(pagename, "s.plural", pos))}
end
-- There are plural forms to show, so show them.
inscat("countable nouns")
local irregular, indeclinable
for i, pl in ipairs(plurals) do
local canon_pl = canonicalize_plural(pl.term, pagename, pos)
if canon_pl then
pl.term = canon_pl
end
local pl_term = get_link_page(pl.term, lang)
if not (pagename:find(" ") or is_regular_plural(pl_term, pagename)) then
irregular = true
if pl_term == pagename then
indeclinable = true
end
end
end
if irregular then
inscat("nouns with irregular plurals")
end
if indeclinable then
inscat("indeclinable nouns")
end
insert_inflection(data, plurals, "plural", "p")
end
-- Return the parameters to be used for nouns and proper nouns. Currently the same.
local noun_params = {
[1] = list_param,
["pl\1qual"] = {list = true, allow_holes = true, replaced_by = false,
instead = "use <l:...> or <q:...> inline modifier on the plural",
},
-- The following four only used for pluralia tantum (1=p)
["sg"] = list_param,
["attr"] = list_param,
}
pos_functions["nouns"] = {
params = noun_params,
func = do_nouns,
}
pos_functions["proper nouns"] = {
params = noun_params,
func = function(args, data)
return do_nouns(args, data, "proper noun")
end,
}
local function base_default_verb_forms(verb)
return escape(add_suffix(verb, "s.verb")), escape(add_suffix(verb, "ing")), escape(add_suffix(verb, "d"))
end
local function default_verb_forms(verb)
local full_s_form, full_ing_form, full_ed_form = base_default_verb_forms(verb)
if verb:find(" ") then
local first, rest = verb:match("^(.-)( .*)$")
local first_s_form, first_ing_form, first_ed_form = base_default_verb_forms(first)
return full_s_form, full_ing_form, full_ed_form, first_s_form .. rest, first_ing_form .. rest,
first_ed_form .. rest, first, rest
else
return full_s_form, full_ing_form, full_ed_form, nil, nil, nil, nil, nil
end
end
local function compute_double_last_cons_stem_of_split_verb(verb, ending)
local first, rest = verb:match("^(.-)( .*)$")
if not first then
error("Verb '" .. verb .. "' must have a space in it to use **")
end
local last_cons = first:match("([bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])$")
if not last_cons then
error("First word '" .. first .. "' must end in a consonant to use **")
end
return first .. last_cons .. ending .. rest
end
local function check_non_nil_star_form(form, pagename)
if form == nil then
error("Verb '" .. pagename .. "' must have a space in it to use *, **, *l, *! or *'")
end
return form
end
local function sub_tilde(form, pagename)
if not form then
return nil
end
if form:find("~") then
form = form:gsub("~", replacement_escape(pagename))
end
return form
end
local deprecated_qual_replaced_by_inline_modifier = {
list = true, allow_holes = true, replaced_by = false,
instead = "use an inline modifier <q:...> or <l:...> on the value"
}
pos_functions["verbs"] = {
params = {
[1] = {list = "pres_3sg", disallow_holes = true},
["pres_3sg\1_qual"] = deprecated_qual_replaced_by_inline_modifier,
[2] = {list = "pres_ptc", disallow_holes = true},
["pres_ptc\1_qual"] = deprecated_qual_replaced_by_inline_modifier,
[3] = {list = "past", disallow_holes = true},
["past\1_qual"] = deprecated_qual_replaced_by_inline_modifier,
[4] = {list = "past_ptc", allow_holes = true},
["past_ptc\1_qual"] = deprecated_qual_replaced_by_inline_modifier,
["noautolinkverb"] = boolean_param,
["angle_bracket"] = boolean_param,
},
func = function(args, data)
-- Get parameters
local par1s
local par2s = parse_inflection(args, {2, "pres_ptc"})
local par3s = parse_inflection(args, {3, "past"})
local par4s = parse_inflection(args, {4, "past_ptc"})
local pres_3sgs, pres_ptcs, pasts, past_ptcs
local pagename = data.pagename
------------------------------------------- UTILITY FUNCTIONS #2 ------------------------------------------
-- These functions are used in both in the separate-parameter format and in the override params such as past_ptc2=.
local full_default_s, full_default_ing, full_default_ed, split_default_s, split_default_ing, split_default_ed
local lemma
local function set_lemma_and_default_forms(the_lemma)
lemma = the_lemma
full_default_s, full_default_ing, full_default_ed, split_default_s, split_default_ing, split_default_ed,
lemma_first, lemma_rest = default_verb_forms(the_lemma)
end
local function canonicalize_s_form(form)
if form == "+" then
error("Internal error: Should not see '+' here")
elseif form == "^" then
return full_default_s
elseif form == "*" then
return check_non_nil_star_form(split_default_s, lemma)
elseif form == "++" then
return compute_plusplus_s_form(lemma, full_default_s)
elseif form == "**" then
if lemma:find("^[^ ]*[szx] ") then
return compute_double_last_cons_stem_of_split_verb(lemma, "es")
else
return check_non_nil_star_form(split_default_s, lemma)
end
elseif form == "+!" then
return lemma .. "s"
elseif form == "*!" then
return check_non_nil_star_form(lemma_first) .. "s" .. lemma_rest
elseif form == "+'" then
return lemma .. "'s"
elseif form == "*'" then
return check_non_nil_star_form(lemma_first) .. "'s" .. lemma_rest
elseif form == "+l" then
if lemma:find("[szx]$") then
return {{term = full_default_s, l = {"US"}},
{term = compute_plusplus_s_form(lemma, full_default_s), l = {"UK"}}}
else
return compute_plusplus_s_form(lemma, full_default_s)
end
elseif form == "*l" then
if lemma:find("^[^ ]*[szx] ") then
return {{term = check_non_nil_star_form(split_default_s, lemma), l = {"US"}},
{term = compute_double_last_cons_stem_of_split_verb(lemma, "es"), l = {"UK"}}}
else
return check_non_nil_star_form(split_default_s, lemma)
end
else
return sub_tilde(form, lemma)
end
end
local function canonicalize_ing_form(form)
if form == "+" then
error("Internal error: Should not see '+' here")
elseif form == "^" then
return full_default_ing
elseif form == "*" then
return check_non_nil_star_form(split_default_ing, lemma)
elseif form == "++" then
return compute_double_last_cons_stem(lemma) .. "ing"
elseif form == "**" then
return compute_double_last_cons_stem_of_split_verb(lemma, "ing")
elseif form == "+!" then
return lemma .. "ing"
elseif form == "*!" then
return check_non_nil_star_form(lemma_first) .. "ing" .. lemma_rest
elseif form == "+'" then
return lemma .. "'ing"
elseif form == "*'" then
return check_non_nil_star_form(lemma_first) .. "'ing" .. lemma_rest
elseif form == "+l" then
return {{term = full_default_ing, l = {"US"}},
{term = compute_double_last_cons_stem(lemma) .. "ing", l = {"UK"}}}
elseif form == "*l" then
return {{term = check_non_nil_star_form(split_default_ing, lemma), l = {"US"}},
{term = compute_double_last_cons_stem_of_split_verb(lemma, "ing"), l = {"UK"}}}
else
return sub_tilde(form, lemma)
end
end
local function canonicalize_ed_form(form)
if form == "+" then
error("Internal error: Should not see '+' here")
elseif form == "^" then
return full_default_ed
elseif form == "*" then
return check_non_nil_star_form(split_default_ed, lemma)
elseif form == "++" then
return compute_double_last_cons_stem(lemma) .. "ed"
elseif form == "+!" then
return lemma .. "ed"
elseif form == "*!" then
return check_non_nil_star_form(lemma_first) .. "ed" .. lemma_rest
elseif form == "+'" then
return {{term = lemma .. "'d"}, {term = lemma .. "'ed"}}
elseif form == "*'" then
return {{term = check_non_nil_star_form(lemma_first) .. "'d" .. lemma_rest},
{term = check_non_nil_star_form(lemma_first) .. "'ed" .. lemma_rest}}
elseif form == "**" then
return compute_double_last_cons_stem_of_split_verb(lemma, "ed")
elseif form == "+l" then
return {{term = full_default_ed, l = {"US"}},
{term = compute_double_last_cons_stem(lemma) .. "ed", l = {"UK"}}}
elseif form == "*l" then
return {{term = check_non_nil_star_form(split_default_ed, lemma), l = {"US"}},
{term = compute_double_last_cons_stem_of_split_verb(lemma, "ed"), l = {"UK"}}}
else
return sub_tilde(form, lemma)
end
end
-- FIXME: options should be "+", "*", "++", "**", "+n", "*n", "++n" and "**n", but not "n"
local function canonicalize_en_form(form)
if form == "n" then
track("n4")
return add_suffix(lemma, "n")
end
return canonicalize_ed_form(form)
end
--------------------------------- MAIN PARSING/CONJUGATING CODE --------------------------------
local is_angle_bracket = args.angle_bracket
if is_angle_bracket then
if par2s[1] or par3s[1] or par4s[1] then
error("Can't specify explicit values for 2=, 3= or 4= along with the angle-bracket format")
end
elseif is_angle_bracket == nil and not par2s[1] and not par3s[1] and not par4s[1] and not args[1][2] and
args[1][1] and args[1][1]:find("<") then
if put.term_contains_top_level_html(args[1][1]) then
-- Often, term_contains_top_level_html() returns true on the angle-bracket format, which would
-- make the pcall() below succeed but leave the angle brackets as-is. Check for this and only do the
-- pcall() if term_contains_top_level_html() returns false.
is_angle_bracket = true
else
-- If it's ambiguous whether it's an angle-bracket format or separate params with an inline modifier,
-- try to parse as the latter. If an error occurs, treat as the former.
local ok
ok, par1s = pcall(parse_inflection, args, {1, "pres_3sg"})
if not ok then
par1s = nil
is_angle_bracket = true
end
end
end
if is_angle_bracket then
-------------------------- ANGLE-BRACKET FORMAT --------------------------
-- (0) Expand multiword term with angle brackets just on the first word.
local arg11 = args[1][1]
if arg11:find("^<.*>$") and pagename:find(" ") then
local first, rest = pagename:match("^(.-)( .*)$")
arg11 = first .. arg11 .. rest
end
-- (1) Parse the indicator specs inside of angle brackets.
local function parse_indicator_spec(angle_bracket_spec)
local inside = angle_bracket_spec:match("^<(.*)>$")
assert(inside)
local segments = put.parse_balanced_segment_run(inside, "[", "]")
local comma_separated_groups = put.split_alternating_runs(segments, ",")
if #comma_separated_groups > 4 then
error("Too many comma-separated parts in indicator spec, expected at most 4: " ..
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
error("Extraneous text after bracketed footnotes: '" .. concat(separated_group) .. "'")
end
if not footnotes then
footnotes = {}
end
insert(footnotes, separated_group[j])
end
return footnotes
end
local function fetch_specs(comma_separated_group)
if not comma_separated_group then
return {{term = "+"}}
end
local specs = {}
local colon_separated_groups = put.split_alternating_runs(comma_separated_group, ":")
for _, colon_separated_group in ipairs(colon_separated_groups) do
local form = colon_separated_group[1]
if form == "*" or form == "**" or form == "*l" or form == "*!" or form == "*'" then
error("*, **, *l, *! and *' not allowed inside of indicator specs: " .. angle_bracket_spec)
end
if form == "" then
form = "+"
end
local termobj = {
term = form
}
local footnotes = fetch_footnotes(colon_separated_group)
if footnotes then
for _, footnote in ipairs(footnotes) do
m_headword_utilities.add_footnote_to_termobj(termobj, footnote)
end
end
insert(specs, termobj)
end
return specs
end
local s_specs = fetch_specs(comma_separated_groups[1])
local ing_specs = fetch_specs(comma_separated_groups[2])
local ed_specs = fetch_specs(comma_separated_groups[3])
local en_specs = fetch_specs(comma_separated_groups[4])
return {
forms = {},
s_specs = s_specs,
ing_specs = ing_specs,
ed_specs = ed_specs,
en_specs = en_specs,
}
end
local parse_props = {
parse_indicator_spec = parse_indicator_spec,
}
local alternant_multiword_spec = iut.parse_inflected_text(arg11, parse_props)
-- (2) Check for user-specified brackets; remove any links from the lemma, but remember the original
-- form so we can use it below in the 'lemma_linked' form.
-- Check to see if there are brackets in the pre-text or post-text. If so, use the linked lemma (with the
-- verb autolinked unless noautolinkverb is given). Otherwise, use the default headword algorithm.
local function check_bracket(val)
if val:find("%[%[") then
alternant_multiword_spec.saw_bracket = true
end
end
for _, alternant_or_word_spec in ipairs(alternant_multiword_spec.alternant_or_word_specs) do
check_bracket(alternant_or_word_spec.before_text)
if alternant_or_word_spec.alternants then
for _, multiword_spec in ipairs(alternant_or_word_spec.alternants) do
for _, word_spec in ipairs(multiword_spec.word_specs) do
check_bracket(word_spec.before_text)
end
check_bracket(multiword_spec.post_text)
end
end
end
check_bracket(alternant_multiword_spec.post_text)
iut.map_word_specs(alternant_multiword_spec, function(base)
if base.lemma == "" then
base.lemma = pagename
end
base.orig_lemma = base.lemma
base.lemma = remove_links(base.lemma)
if args.noautolinkverb or base.orig_lemma:find("%[%[") then
base.linked_lemma = base.orig_lemma
else
base.linked_lemma = "[[" .. base.orig_lemma .. "]]"
end
end)
-- (3) Conjugate the verbs according to the indicator specs parsed above.
local all_verb_slots = {
lemma = "infinitive",
lemma_linked = "infinitive",
s_form = "3|s|pres",
ing_form = "pres|ptcp",
ed_form = "past",
en_form = "past|ptcp",
}
local function conjugate_verb(base)
local function process_specs(slot, specs, canon_func, default_values, default_already_formobj)
local function insert_termobj_into_slot(termobj)
local formobj = m_headword_utilities.convert_termobj_to_formobj(termobj)
-- If the form is -, don't insert any forms, which will result in there being no overall forms
-- (in fact it will be nil). We check for that down below and substitute a single "-" as the
-- form, which in turn gets turned into special labels like "no present participle".
if formobj.form == "-" then
if formobj.footnotes then
error("Unable to preserve footnotes specified on missing form '-': FIXME: " ..
dump(formobj.footnotes))
end
else
iut.insert_form(base.forms, slot, formobj)
end
end
local function canonicalize_and_insert(arg)
local canon_arg = canon_func(arg)
if type(canon_arg) == "string" then
arg.term = canon_arg
insert_termobj_into_slot(arg)
else
for _, canon in ipairs(canon_arg) do
m_headword_utilities.combine_termobj_decorations(canon, arg)
insert_termobj_into_slot(canon)
end
end
end
for _, arg in ipairs(specs) do
if arg.term == "+" then
if default_values then -- will be nil if past tense specified as - and no past ptc given
for _, val in ipairs(default_values) do
val = shallowCopy(val)
if default_already_formobj then
local argformobj = m_headword_utilities.convert_termobj_to_formobj(arg)
val.footnotes = iut.combine_footnotes(val.footnotes, argformobj.footnotes)
iut.insert_form(base.forms, slot, val)
else
m_headword_utilities.combine_termobj_decorations(val, arg)
canonicalize_and_insert(val)
end
end
end
else
canonicalize_and_insert(arg)
end
end
end
set_lemma_and_default_forms(base.lemma)
local all_part_default_specs = {}
local function process_and_canonicalize_s_form(arg)
local form = arg.term
if form == "+" then
error("Internal error: '+' should have been converted to '^' by now")
end
if form == "*" or form == "**" or form == "*l" or form == "*!" or form == "*'" then
error(("Internal error: '%s' should have already thrown an error"):format(form))
end
if form == "^" or form == "++" or form == "+l" or form == "+!" or form == "+'" then
insert(all_part_default_specs, shallowCopy(arg))
end
return canonicalize_s_form(form)
end
process_specs("s_form", base.s_specs, process_and_canonicalize_s_form, {{term = "^"}})
if not all_part_default_specs[1] then
all_part_default_specs[1] = {term = "^"}
end
process_specs("ing_form", base.ing_specs, function(arg) return canonicalize_ing_form(arg.term) end,
all_part_default_specs)
process_specs("ed_form", base.ed_specs, function(arg) return canonicalize_ed_form(arg.term) end,
all_part_default_specs)
process_specs("en_form", base.en_specs, function(arg) return canonicalize_en_form(arg.term) end,
base.forms.ed_form, "default already formobj")
iut.insert_form(base.forms, "lemma", {form = base.lemma})
-- Add linked version of lemma for use in head=. We write this in a general fashion in case
-- there are multiple lemma forms (which isn't possible currently at this level, although it's
-- possible overall using the ((...,...)) notation).
iut.insert_forms(base.forms, "lemma_linked", iut.map_forms(base.forms.lemma, function(form)
if form == base.lemma and base.linked_lemma:find("%[%[") then
return base.linked_lemma
else
return form
end
end))
end
local inflect_props = {
slot_table = all_verb_slots,
inflect_word_spec = conjugate_verb,
}
iut.inflect_multiword_or_alternant_multiword_spec(alternant_multiword_spec, inflect_props)
-- (4) Fetch the forms and put the conjugated lemmas in data.heads if not explicitly given.
local function fetch_termobjs(slot)
local forms = alternant_multiword_spec.forms[slot]
-- See above. This should only occur if the user explicitly used - for a spec.
if not forms or not forms[1] then
return {{term = "-"}}
end
local termobjs = {}
for _, formobj in ipairs(forms) do
insert(termobjs, m_headword_utilities.convert_formobj_to_termobj(formobj))
end
return termobjs
end
pres_3sgs = fetch_termobjs("s_form")
pres_ptcs = fetch_termobjs("ing_form")
pasts = fetch_termobjs("ed_form")
past_ptcs = fetch_termobjs("en_form")
-- Use the "linked" form of the lemma as the head if no head= explicitly given and the user specified
-- brackets in one of the lemmas. Otherwise we use the default headword-linking algorithm.
if not data.user_specified_heads[1] and alternant_multiword_spec.saw_bracket then
data.heads = {}
for _, lemma_obj in ipairs(alternant_multiword_spec.forms.lemma_linked) do
insert(data.heads, m_headword_utilities.convert_formobj_to_termobj(lemma_obj))
end
end
else
-------------------------- SEPARATE-PARAM FORMAT --------------------------
set_lemma_and_default_forms(pagename)
par1s = par1s or parse_inflection(args, {1, "pres_3sg"})
pres_3sgs = {}
pres_ptcs = {}
pasts = {}
past_ptcs = {}
if not par1s[1] then
par1s = {{term = "+"}}
end
if not par2s[1] then
par2s = {{term = "+"}}
end
if not par3s[1] then
par3s = {{term = "+"}}
end
if not par4s[1] then
par4s = {{term = "+"}}
end
local function process_argument(args, dest, canon_func, default_values, default_already_canonicalized)
local function canonicalize_and_insert(arg)
local canon_arg = canon_func(arg)
if type(canon_arg) == "string" then
arg.term = canon_arg
m_headword_utilities.insert_termobj_combining_duplicates(dest, arg)
else
for _, canon in ipairs(canon_arg) do
m_headword_utilities.combine_termobj_decorations(canon, arg)
m_headword_utilities.insert_termobj_combining_duplicates(dest, canon)
end
end
end
for _, arg in ipairs(args) do
if arg.term == "+" then
for _, val in ipairs(default_values) do
val = shallowCopy(val)
m_headword_utilities.combine_termobj_decorations(val, arg)
if default_already_canonicalized then
m_headword_utilities.insert_termobj_combining_duplicates(dest, val)
else
canonicalize_and_insert(val)
end
end
else
canonicalize_and_insert(arg)
end
end
end
local all_part_default_specs = {}
local function process_and_canonicalize_s_form(arg)
local form = arg.term
if form == "+" then
error("Internal error: '+' should have been converted to '^' by now")
end
if form == "^" or form == "++" or form == "+l" or form == "+!" or form == "+'" or
form == "*" or form == "**" or form == "*l" or form == "*!" or form == "*'" then
insert(all_part_default_specs, shallowCopy(arg))
end
return canonicalize_s_form(form)
end
process_argument(par1s, pres_3sgs, process_and_canonicalize_s_form, {{term = "^"}})
if not all_part_default_specs[1] then
all_part_default_specs[1] = {term = "^"}
end
process_argument(par2s, pres_ptcs, function(arg) return canonicalize_ing_form(arg.term) end,
all_part_default_specs)
process_argument(par3s, pasts, function(arg) return canonicalize_ed_form(arg.term) end,
all_part_default_specs)
process_argument(par4s, past_ptcs, function(arg) return canonicalize_en_form(arg.term) end,
pasts, "default already canonicalized")
end
------------------------------------------- INSERT INFLECTIONS ------------------------------------------
insert_inflection(data, pres_3sgs, "third-person singular simple present", "s-verb-form")
insert_inflection(data, pres_ptcs, "present participle", "ing-form")
if deepEquals(pasts, past_ptcs) then
insert_inflection(data, pasts, "simple past and past participle", "ed-form",
"no simple past or past participle")
else
insert_inflection(data, pasts, "simple past", "spast")
insert_inflection(data, past_ptcs, "past participle", "past|part")
end
if pagename:find(" ") then
-- Check for placeholder "it"
local words = split(pagename, " ")
for _, word in ipairs(words) do
if word == "it" or word == "its" or word == "it's" then
insert(data.categories, langname .. ' terms with placeholder "it"')
break
end
end
-- Check for phrasal verbs
local phrasal_adverbs = list_to_set{
-- NOTE: This should only contain common phrasal adverbs, not random words like [[low]],
-- [[adrift]], etc.
"aback",
"about",
"above",
"across",
"after",
"against",
"ahead",
"along",
"apart",
"around",
"as",
"aside",
"at",
"away",
"back",
"before",
"behind",
"below",
"between",
"beyond",
"by",
"down",
"for",
"forth",
"from",
"in",
"into",
"of",
"off",
"on",
"onto",
"out",
"over",
"past",
"round",
"through",
"to",
"together",
"towards",
"under",
"up",
"upon",
"with",
"without",
}
local allowed_non_adverb_words = list_to_set{
"it",
"one",
"oneself",
"someone",
}
local base = pagename
local seen_adverbs = {}
-- Only consider a verb to be phrasal if it consists of a single base verb followed exclusively by either
-- adverbs from `phrasal_adverbs` or placeholder words from `allowed_non_adverb_words`, where at
-- least one following word is from `phrasal_adverbs` (hence [[can it]] is not a phrasal verb).
while true do
local prev, word = base:match("^(.+) (.-)$")
if not prev then
break
end
if phrasal_adverbs[word] then
insert(seen_adverbs, word)
elseif allowed_non_adverb_words[word] then
-- do nothing
else
break
end
base = prev
end
if not base:find(" ") and seen_adverbs[1] then
insert(data.categories, langname .. " phrasal verbs")
for i = #seen_adverbs, 1, -1 do
insert(data.categories, langname .. ' phrasal verbs formed with "' .. seen_adverbs[i] ..
'"')
end
end
end
end,
}
-----------------------------------------------------------------------------------------
-- Suffix forms --
-----------------------------------------------------------------------------------------
pos_functions["suffix forms"] = {
params = {
[1] = {required = true, list = true, disallow_holes = true},
},
func = function(args, data, is_suffix)
local suffix_type = {}
for _, typ in ipairs(args[1]) do
insert(suffix_type, typ .. "-forming suffix")
end
insert(data.inflections, {label = "non-lemma form of " .. m_table.serialCommaJoin(suffix_type, {conj = "or"})})
end,
}
return export
16kpxx6u8rw55bh0m9lw3iuegl8ptae
375862
375860
2026-09-25T11:28:39Z
SNN95
2113
375862
Scribunto
text/plain
local export = {}
local pos_functions = {}
local force_cat = false -- for testing; if true, categories appear in non-mainspace pages
local require = require
local require_when_needed = require("Module:require when needed")
local en_utilities_module = "Module:en-utilities"
local headword_utilities_module = "Module:headword utilities"
local headword_module = "Module:headword"
local inflection_utilities_module = "Module:inflection utilities"
local parse_utilities_module = "Module:parse utilities"
local JSON_module = "Module:JSON"
local links_module = "Module:links"
local parameters_module = "Module:parameters"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local utilities_module = "Module:utilities"
local iut = require_when_needed(inflection_utilities_module)
local put = require_when_needed(parse_utilities_module)
local add_links_to_multiword_term = require_when_needed(headword_utilities_module, "add_links_to_multiword_term")
local add_suffix = require_when_needed(en_utilities_module, "add_suffix")
local apply_link_modifiers = require_when_needed(headword_utilities_module, "apply_link_modifiers")
local concat = table.concat
local format_categories = require_when_needed(utilities_module, "format_categories")
local full_headword = require_when_needed(headword_module, "full_headword")
local get_link_page = require_when_needed(links_module, "get_link_page")
local insert = table.insert
local ipairs = ipairs
local is_regular_plural = require_when_needed(en_utilities_module, "is_regular_plural")
local list_to_set = require_when_needed(table_module, "listToSet")
local pairs = pairs
local process_params = require_when_needed(parameters_module, "process")
local remove = table.remove
local remove_links = require_when_needed(links_module, "remove_links")
local singularize = require_when_needed(en_utilities_module, "singularize")
local split = require_when_needed(string_utilities_module, "split")
local toJSON = require_when_needed(JSON_module, "toJSON")
local toNFD = mw.ustring.toNFD
local type = type
local ulen = require_when_needed(string_utilities_module, "len")
local ulower = require_when_needed(string_utilities_module, "lower")
local umatch = require_when_needed(string_utilities_module, "match")
local u = require_when_needed(string_utilities_module, "char")
local ugsub = require_when_needed(string_utilities_module, "gsub")
local lang = require("Module:languages").getByCode("en")
local langname = lang:getCanonicalName()
local function glossary_link(entry, text)
text = text or entry
return "[[Lampiran:Glosari#" .. entry .. "|" .. text .. "]]"
end
local function track(page)
require("Module:debug/track")("en-headword/" .. page)
return true
end
------------------------------------------- UTILITY FUNCTIONS ------------------------------------------
-- These functions are used directly in the <> format as well as in the utility functions #2 below.
local function compute_double_last_cons_stem(term)
local last_cons = term:match("([bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])$")
if not last_cons then
error("Verb stem '" .. term .. "' must end in a consonant to use ++")
end
return term .. last_cons
end
local function compute_plusplus_s_form(term, default_s_form)
if term:find("[sz]$") then
-- regas -> regasses, derez -> derezzes
return compute_double_last_cons_stem(term) .. "es"
else
return default_s_form
end
end
-- The main entry point.
-- This is the only function that can be invoked from a template.
function export.show(frame)
local poscat = frame.args[1] or error("Part of speech has not been specified. Please pass parameter 1 to the module invocation.")
local boolean = {type = "boolean"}
local params = {
["head"] = {list = true},
["id"] = true,
["json"] = boolean,
["sort"] = true,
["splithyph"] = boolean,
["nosplithyph"] = boolean,
["hyphspace"] = boolean,
["nolink"] = boolean,
["nolinkhead"] = {type = "boolean", alias_of = "nolink"},
["nosuffix"] = boolean,
["nomultiwordcat"] = boolean,
["pagename"] = true, -- for testing
}
local pos_data, pos_func = pos_functions[poscat]
if pos_data then
local pos_params = pos_data.params
if pos_params then
for key, val in pos_params() do
params[key] = val
end
end
pos_func = pos_data.func
end
local args = process_params(frame:getParent().args, params, nil, "en-headword", "show")
local pagename = args.pagename or mw.loadData("Module:headword/data").pagename -- Accounts for unsupported titles.
local user_specified_heads = args.head
local heads = user_specified_heads
local autohead
if args.nolink or not pagename:find("[ '%-]") then
autohead = pagename
else
local en_no_split_apostrophe_words = list_to_set{
"one's",
"someone's",
"he's",
"she's",
"it's",
}
local en_include_hyphen_prefixes = list_to_set{
-- We don't include things that are also words even though they are often (perhaps mostly) prefixes, e.g.
-- "be", "counter", "cross", "extra", "half", "mid", "over", "pan", "under".
"acro",
"acousto",
"Afro",
"agro",
"anarcho",
"angio",
"Anglo",
"ante",
"anti",
"arch",
"auto",
"bi",
"bio",
"cis",
"co",
"cryo",
"crypto",
"de",
"demi",
"eco",
"electro",
"Euro",
"ex",
"Greco",
"hemi",
"hydro",
"hyper",
"hypo",
"infra",
"Indo",
"inter",
"intra",
"Judeo",
"macro",
"meta",
"micro",
"mini",
"multi",
"neo",
"neuro",
"non",
"para",
"peri",
"post",
"pre",
"pro",
"proto",
"pseudo",
"re",
"semi",
"sub",
"super",
"trans",
"un",
"vice",
}
local function is_english(term)
local title = mw.title.new(term)
if title and title.exists then
local content = title:getContent()
if content and content:find("==Bahasa Inggeris==\n") then
return true
end
end
return false
end
local function en_split_hyphen_when_space(word)
if not word:find("-", nil, true) then
return nil
end
if args.hyphspace then
return "[[" .. word:gsub("%-+", " ") .. "|" .. word .. "]]"
end
if args.nosplithyph then
return "[[" .. word .. "]]"
end
if not args.splithyph then
local space_word = word:gsub("%-+", " ")
if is_english(space_word) then
return "[[" .. space_word .. "|" .. word .. "]]"
end
if is_english(word) then
return "[[" .. word .. "]]"
end
end
return nil
end
local function en_split_apostrophe(word)
local base = word:match("^(.*)'s$")
if base then
return "[[" .. base .. "]][[-'s|'s]]"
end
base = word:match("^(.*)'$")
if base then
if base:find("s$") then
local sg = singularize(base)
if is_english(sg) then
return "[[" .. sg .. "|" .. base .. "]][[-'|']]"
end
end
return "[[" .. base .. "]][[-'|']]"
end
return "[[" .. word .. "]]"
end
autohead = add_links_to_multiword_term(pagename, {
split_hyphen_when_space = en_split_hyphen_when_space,
split_apostrophe = en_split_apostrophe,
no_split_apostrophe_words = en_no_split_apostrophe_words,
include_hyphen_prefixes = en_include_hyphen_prefixes,
})
end
if #heads == 0 then
heads = {autohead}
else
for i, head in ipairs(heads) do
if head:find("^~") then
head = apply_link_modifiers(autohead, head:sub(2))
heads[i] = head
end
if head == autohead then
track("redundant-head")
end
end
end
local data = {
lang = lang,
pos_category = poscat,
categories = {},
heads = heads,
user_specified_heads = user_specified_heads,
no_redundant_head_cat = #user_specified_heads == 0,
inflections = {},
nomultiwordcat = args.nomultiwordcat,
sort_key = args.sort,
pagename = args.pagename,
-- This is always set, and in the case of unsupported titles, it's the displayed version (e.g. 'C|N>K' instead of
-- 'Unsupported titles/C through N to K').
displayed_pagename = pagename,
id = args.id,
force_cat_output = force_cat,
}
local is_suffix = false
if not args.nosuffix and pagename:find("^%-") and not pagename:find("^%-%-") and poscat ~= "bentuk akhiran" then
is_suffix = true
data.pos_category = "akhiran"
local singular_poscat = singularize(poscat)
insert(data.categories, "Akhiran pembentuk " .. singular_poscat .. langname)
insert(data.inflections, {label = "Akhiran pembentuk " .. singular_poscat})
end
if pos_func then
pos_func(args, data, is_suffix)
end
local extra_categories = {}
if pagename:find("[Qq]") then
-- Check for q not followed by u. We want to exclude things like [[13q deletion syndrome]] and [[BFOQ]] that
-- don't have a lowercase letter on either side, as well as things like [[& seq.]] and [[acq.]] that are
-- abbreviations for words containing a following u.
--
-- Approximate range of combining diacritics; we want to remove them so the checks below for
-- a lowercase letter next to the q aren't tripped up by diacritics on the letter.
local u300 = u(0x0300)
local u36F = u(0x036F)
local pagename_no_diacritics = ugsub(toNFD(pagename), "[" .. u300 .. "-" .. u36F .. "]", "")
if pagename_no_diacritics:find("[Qq][a-tv-z]") or pagename_no_diacritics:find("[a-z]q[^u.]") or
pagename_no_diacritics:find("[a-z]q$") then
insert(data.categories, "Perkataan dengan Q tidak diikuti U bahasa " .. langname)
end
end
-- toNFD performs decomposition, so letters that decompose to an ASCII
-- vowel and a diacritic, such as é, are counted as vowels and do not do not
-- need to be included in the pattern.
if not umatch(ulower(toNFD(pagename)), "[aeiouyæœøəªºαεηιουω]") then
insert(data.categories, "Perkataan dieja tanpa vokal bahasa " .. langname)
end
if pagename:find("yre$") then
insert(data.categories, 'Perkataan berakhir dengan "-yre" bahasa ' .. langname)
end
if not pagename:find(" ") and ulen(pagename) >= 25 then
insert(extra_categories, "Perkataan panjang bahasa " .. langname)
end
if pagename:find("^[^aeiou ]*a[^aeiou ]*e[^aeiou ]*i[^aeiou ]*o[^aeiou ]*u[^aeiou ]*$") then
insert(data.categories, "Perkataan menggunakan semua vokal dalam urutan abjad bahasa " .. langname)
end
if args.json then
return toJSON(data)
end
return full_headword(data)
.. (#extra_categories > 0
and format_categories(extra_categories, lang, args.sort)
or "")
end
-- This function does the common work between adjectives and adverbs
local function make_comparatives(params, data)
local comp_parts = {label = glossary_link("bandingan"), accel = {form = "bandingan"}}
local sup_parts = {label = glossary_link("penghabisan"), accel = {form = "penghabisan"}}
local pagename = data.displayed_pagename
if #params == 0 then
insert(params, {"more"})
end
-- Go over each parameter given and create a comparative and superlative
-- form.
for i, val in ipairs(params) do
local comp = val[1]
local comp_qual = val[2]
local sup = val[3]
local sup_qual = val[4]
local comp_part, sup_part
if comp == "more" and pagename ~= "many" and pagename ~= "much" then
comp_part = "more [[" .. pagename .. "]]"
sup_part = sup or "most [[" .. pagename .. "]]"
elseif comp == "further" and pagename ~= "far" then
comp_part = "further [[" .. pagename .. "]]"
sup_part = sup or "furthest [[" .. pagename .. "]]"
elseif comp == "er" then
-- Add the "-er" and "-est" suffixes.
comp_part = add_suffix(pagename, "r")
sup_part = sup or add_suffix(pagename, "st.superlative")
elseif comp == "ier" then
if pagename:sub(-1) ~= "y" then
error("Can't specify 'ier' comparative unless the term ends with 'y'.")
end
comp_part = pagename:gsub("e?y$", "ier")
sup_part = sup or pagename:gsub("e?y$", "iest")
elseif comp == "-" or sup == "-" then
-- Allowing '-' makes it more flexible to not have some forms
if comp ~= "-" then
comp_part = comp
end
if sup ~= "-" then
sup_part = sup
end
else
-- If the full comparative was given, but no superlative, then
-- create it by replacing the ending -er with -est.
if not sup then
if comp:sub(-2) == "er" then
sup = comp:sub(1, -3) .. "est"
else
error("The superlative of \"" .. comp .. "\" cannot be generated automatically. Please provide it with the \"sup" .. (i == 1 and "" or i) .. "=\" parameter.")
end
end
comp_part = comp
sup_part = sup
end
if comp_part then
insert(comp_parts, {term = comp_part, q = {comp_qual}})
end
if sup_part then
insert(sup_parts, {term = sup_part, q = {sup_qual}})
end
end
insert(data.inflections, comp_parts)
insert(data.inflections, sup_parts)
end
local function make_heads_definite(args, data)
if args.def == "~" then
local newheads = {}
for _, head in ipairs(data.heads) do
insert(newheads, head)
insert(newheads, "the " .. head)
end
data.heads = newheads
else
for i, head in ipairs(data.heads) do
data.heads[i] = "the " .. head
end
end
end
pos_functions["kata sifat"] = {
params = function()
local list_allow_holes = {list = true, allow_holes = true}
return pairs{
[1] = list_allow_holes,
["def"] = true,
["the"] = {alias_of = "def"},
["comp_qual"] = {list = "comp\1_qual", allow_holes = true},
["sup"] = list_allow_holes,
["sup_qual"] = {list = "sup\1_qual", allow_holes = true},
}
end,
func = function(args, data)
local shift = 0
local is_not_comparable = false
local is_comparative_only = false
if args.def then
make_heads_definite(args, data)
end
-- If the first parameter is ?, then don't show anything, just return.
if args[1][1] == "?" then
return
-- If the first parameter is -, then move all parameters up one position.
elseif args[1][1] == "-" then
shift = 1
is_not_comparable = true
-- If the only argument is +, then remember this and clear parameters
elseif args[1][1] == "+" and args[1].maxindex == 1 then
shift = 1
is_comparative_only = true
end
-- Gather all the comparative and superlative parameters.
local params = {}
for i = 1, args[1].maxindex - shift do
local comp = args[1][i + shift]
local comp_qual = args["comp_qual"][i + shift]
local sup = args["sup"][i]
local sup_qual = args["sup_qual"][i + shift]
if comp or sup then
insert(params, {comp, comp_qual, sup, sup_qual})
end
end
if shift == 1 then
-- If the first parameter is "-" but there are no parameters,
-- then show "not comparable" only and return.
-- If there are parameters, then show "not generally comparable"
-- before the forms.
if #params == 0 then
if is_not_comparable then
insert(data.inflections, {label = "tidak " .. glossary_link("sebanding")})
insert(data.categories, "Kata sifat bahasa " .. langname .. " tidak sebanding")
return
end
if is_comparative_only then
insert(data.inflections, {label = glossary_link("bandingan") .. " sahaja"})
insert(data.categories, "Kata sifat bahasa " .. langname .. " bandingan sahaja")
return
end
else
insert(data.inflections, {label = "biasanya tidak " .. glossary_link("sebanding")})
end
end
-- Process the parameters
make_comparatives(params, data)
end,
}
pos_functions["adverba"] = {
params = function()
local list_allow_holes = {list = true, allow_holes = true}
return pairs{
[1] = list_allow_holes,
["comp_qual"] = {list = "comp\1_qual", allow_holes = true},
["sup"] = list_allow_holes,
["sup_qual"] = {list = "sup\1_qual", allow_holes = true},
}
end,
func = function(args, data)
local shift = 0
-- If the first parameter is ?, then don't show anything, just return.
if args[1][1] == "?" then
return
-- If the first parameter is -, then move all parameters up one position.
elseif args[1][1] == "-" then
shift = 1
end
-- Gather all the comparative and superlative parameters.
local params = {}
for i = 1, args[1].maxindex - shift do
local comp = args[1][i + shift]
local comp_qual = args["comp_qual"][i + shift]
local sup = args["sup"][i]
local sup_qual = args["sup_qual"][i + shift]
if comp or sup then
insert(params, {comp, comp_qual, sup, sup_qual})
end
end
if shift == 1 then
-- If the first parameter is "-" but there are no parameters,
-- then show "not comparable" only and return. If there are parameters,
-- then show "not generally comparable" before the forms.
if #params == 0 then
insert(data.inflections, {label = "tidak " .. glossary_link("sebanding")})
insert(data.categories, "Adverba bahasa " .. langname .. " tidak sebanding")
return
else
insert(data.inflections, {label = "biasanya tidak " .. glossary_link("sebanding")})
end
end
-- Process the parameters
make_comparatives(params, data)
end,
}
pos_functions["kata hubung"] = {
params = function()
return pairs{
[1] = {alias_of = "head", list = false},
}
end,
}
pos_functions["kata seru"] = pos_functions["kata hubung"]
local function gather_inflections_with_quals(args, infl_field, qual_field, label)
-- Gather all the plural parameters from the numbered parameters.
local infls = {}
if label then
infls.label = label
end
for i, infl in ipairs(args[infl_field]) do
local qual = args[qual_field][i]
if qual then
insert(infls, {term = infl, q = {qual}})
else
insert(infls, infl)
end
end
return infls
end
local function escape(str)
return (str:gsub("\\([:#])", "\\\\%1")
:gsub("[:#]", "\\%0"))
end
local function canonicalize_plural(pl, pagename, pos)
if pl == "+" then
return escape(add_suffix(pagename, "s.plural", pos))
elseif pl == "++" then
return escape(compute_plusplus_s_form(pagename, add_suffix(pagename, "s.plural", pos)))
elseif pl == "*" then
return escape(pagename)
elseif pl == "ies" then
if pagename:sub(-1) == "y" then
return escape(pagename:gsub("e?y$", pl))
end
error("Can't specify 'ies' plural unless the term ends with 'y'.")
elseif pl == "s" or pl == "es" or pl == "'s" then
return escape(pagename .. pl)
end
end
local function do_nouns(args, data, pos)
local pagename = data.displayed_pagename
pos = pos or "noun"
if args.def then
make_heads_definite(args, data)
end
local plurals = gather_inflections_with_quals(args, 1, "plqual")
local function insert_plurale_tantum_inflections(is_plural_only)
if args.sg[1] then
insert(data.inflections, {label = "biasanya jamak"})
insert(data.inflections, gather_inflections_with_quals(args, "sg", "sgqual", "singular"))
elseif is_plural_only then
insert(data.inflections, {label = "jamak sahaja"})
end
if args.attr[1] then
insert(data.inflections, gather_inflections_with_quals(args, "attr", "attrqual", "attributive"))
end
end
if plurals[1] == "p" then
-- plurale tantum
if plurals[2] then
error("With plurale tantum noun, can't specify more than one plural")
end
data.genders = {"p"} -- this should auto-insert the correct 'pluralia tantum' category
insert_plurale_tantum_inflections("plural only")
return
end
local function inscat(cat)
cat = cat:sub(1,1):upper() .. cat:sub(2)
insert(data.categories, cat .. " bahasa " .. langname)
end
local need_default_plural = pos == "noun"
local sp = false
if plurals[1] == "sp" then
-- construed as singular or plural
remove(plurals, 1) -- Remove the "sp"
inscat("nouns construed as singular or plural")
data.genders = {"s", "p"} -- this should auto-insert the correct 'pluralia tantum' category
need_default_plural = false
sp = true
end
if plurals[1] == "-" then
-- Uncountable noun; may occasionally have a plural
remove(plurals, 1) -- Remove the "-"
inscat("kata nama tidak berbilang")
-- If plural forms were given explicitly, then show "usually"
if plurals[1] then
insert(data.inflections, {label = "biasanya " .. glossary_link("tidak terbilang")})
else
insert(data.inflections, {label = glossary_link("tidak terbilang")})
end
need_default_plural = false
elseif plurals[1] == "#" then
-- Usually countable (e.g., "grilled cheese")
remove(plurals, 1) -- Remove the "#"
insert(data.inflections, {label = "biasanya " .. glossary_link("terbilang")})
inscat("kata nama tidak berbilang")
inscat("kata nama berbilang")
-- If no plural was given, add a default one now
if not plurals[1] and need_default_plural then
plurals[1] = escape(add_suffix(pagename, "s.plural", pos))
end
elseif plurals[1] == "~" then
-- Mixed countable/uncountable noun, always has a plural
remove(plurals, 1) -- Remove the "~"
insert(data.inflections, {label = glossary_link("terbilang") .. " dan " .. glossary_link("tidak terbilang")})
inscat("kata nama tidak berbilang")
inscat("kata nama berbilang")
-- If no plural was given, add a default one now
if not plurals[1] and need_default_plural then
plurals[1] = escape(add_suffix(pagename, "s.plural", pos))
end
end
-- Plural is unknown
if plurals[1] == "?" then
remove(plurals, 1) -- Remove the "?"
-- Not desired; see [[Wiktionary:Tea_room/2021/August#"Plural unknown or uncertain"]]
-- insert(data.inflections, {label = "plural unknown or uncertain"})
inscat("nouns with unknown or uncertain plurals")
if plurals[1] then
error("Can't specify explicit plurals along with '?' for unknown/uncertain plural")
end
return
end
-- Plural is not attested
if plurals[1] == "!" then
remove(plurals, 1) -- Remove the "!"
insert(data.inflections, {label = "bentuk jamak tidak terbukti"})
inscat("nouns with unattested plurals")
if plurals[1] then
error("Can't specify explicit plurals along with '!' for unattested plural")
end
return
end
-- If no plural was given, maybe add a default one, otherwise (when "-" was given or proper noun) return.
if not plurals[1] and not sp then
if not need_default_plural then
inscat("kata nama tidak berbilang")
return
end
plurals[1] = escape(add_suffix(pagename, "s.plural", pos))
end
if sp then
insert_plurale_tantum_inflections()
return
end
-- There are plural forms to show, so show them.
inscat("kata nama berbilang")
plurals.label = "jamak"
plurals.accel = {form = "p"}
local irregular, indeclinable
for i, pl in ipairs(plurals) do
local pl_type = type(pl)
local pl_term = pl_type == "table" and pl.term or pl
local canon_pl = canonicalize_plural(pl_term, pagename, pos)
if canon_pl then
pl_term = canon_pl
if pl_type == "table" then
pl.term = pl_term
else
plurals[i] = pl_term
end
end
pl_term = get_link_page(pl_term, lang)
if not (pagename:find(" ") or is_regular_plural(pl_term, pagename)) then
irregular = true
if pl_term == pagename then
indeclinable = true
end
end
end
if irregular then
inscat("Kata nama dengan bentuk jamak tak teratur")
end
if indeclinable then
inscat("Kata nama tegar")
end
insert(data.inflections, plurals)
end
-- Return the parameters to be used for nouns and proper nouns. Currently the same.
local function get_noun_params()
local list_allow_holes = {list = true, allow_holes = true}
local list_disallow_holes = {list = true, disallow_holes = true}
return pairs{
[1] = list_disallow_holes,
["def"] = true,
["the"] = {alias_of = "def"},
["pl\1qual"] = list_allow_holes,
-- The following four only used for pluralia tantum (1=p)
["sg"] = list_disallow_holes,
["sg\1qual"] = list_allow_holes,
["attr"] = list_disallow_holes,
["attr\1qual"] = list_allow_holes,
}
end
pos_functions["kata nama"] = {
params = get_noun_params,
func = do_nouns,
}
pos_functions["kata nama khas"] = {
params = get_noun_params,
func = function(args, data)
return do_nouns(args, data, "kata nama khas")
end,
}
local function base_default_verb_forms(verb)
return escape(add_suffix(verb, "s.verb")), escape(add_suffix(verb, "ing")), escape(add_suffix(verb, "d"))
end
local function default_verb_forms(verb)
local full_s_form, full_ing_form, full_ed_form = base_default_verb_forms(verb)
if verb:find(" ") then
local first, rest = verb:match("^(.-)( .*)$")
local first_s_form, first_ing_form, first_ed_form = base_default_verb_forms(first)
return full_s_form, full_ing_form, full_ed_form, first_s_form .. rest, first_ing_form .. rest, first_ed_form .. rest
else
return full_s_form, full_ing_form, full_ed_form, nil, nil, nil
end
end
local function compute_double_last_cons_stem_of_split_verb(verb, ending)
local first, rest = verb:match("^(.-)( .*)$")
if not first then
error("Verb '" .. verb .. "' must have a space in it to use ++*")
end
local last_cons = first:match("([bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])$")
if not last_cons then
error("First word '" .. first .. "' must end in a consonant to use ++*")
end
return first .. last_cons .. ending .. rest
end
local function check_non_nil_star_form(form, pagename)
if form == nil then
error("Verb '" .. pagename .. "' must have a space in it to use * or ++*")
end
return form
end
local function sub_tilde(form, pagename)
if not form then
return nil
end
return (form:gsub("~", pagename))
end
pos_functions["kata kerja"] = {
params = function()
return pairs{
[1] = {list = "pres_3sg", allow_holes = true},
["pres_3sg_qual"] = {list = "pres_3sg\1_qual", allow_holes = true},
[2] = {list = "pres_ptc", allow_holes = true},
["pres_ptc_qual"] = {list = "pres_ptc\1_qual", allow_holes = true},
[3] = {list = "past", allow_holes = true},
["past_qual"] = {list = "past\1_qual", allow_holes = true},
[4] = {list = "past_ptc", allow_holes = true},
["past_ptc_qual"] = {list = "past_ptc\1_qual", allow_holes = true},
["noautolinkverb"] = {type = "boolean"},
}
end,
func = function(args, data)
-- Get parameters
local par1 = args[1][1]
local par2 = args[2][1]
local par3 = args[3][1]
local par4 = args[4][1]
local pres_3sgs, pres_ptcs, pasts, past_ptcs
local pagename = data.displayed_pagename
------------------------------------------- UTILITY FUNCTIONS #2 ------------------------------------------
-- These functions are used in both in the separate-parameter format and in the override params such as past_ptc2=.
local new_default_s, new_default_ing, new_default_ed, split_default_s, split_default_ing, split_default_ed =
default_verb_forms(pagename)
local function canonicalize_s_form(form)
if form == "+" then
return new_default_s
elseif form == "*" then
return check_non_nil_star_form(split_default_s, pagename)
elseif form == "++" then
return compute_plusplus_s_form(pagename, new_default_s)
elseif form == "++*" then
if pagename:find("^[^ ]*[sz] ") then
return compute_double_last_cons_stem_of_split_verb(pagename, "es")
else
return check_non_nil_star_form(split_default_s, pagename)
end
else
return sub_tilde(form, pagename)
end
end
local function canonicalize_ing_form(form)
if form == "+" then
return new_default_ing
elseif form == "*" then
return check_non_nil_star_form(split_default_ing, pagename)
elseif form == "++" then
return compute_double_last_cons_stem(pagename) .. "ing"
elseif form == "++*" then
return compute_double_last_cons_stem_of_split_verb(pagename, "ing")
else
return sub_tilde(form, pagename)
end
end
local function canonicalize_ed_form(form)
if form == "+" then
return new_default_ed
elseif form == "*" then
return check_non_nil_star_form(split_default_ed, pagename)
elseif form == "++" then
return compute_double_last_cons_stem(pagename) .. "ed"
elseif form == "++*" then
return compute_double_last_cons_stem_of_split_verb(pagename, "ed")
else
return sub_tilde(form, pagename)
end
end
-- FIXME: options should be "+", "*", "++", "++*", "+n", "*n", "++n" and "++*n", but not "n"
local function canonicalize_en_form(form)
if form == "n" then
track("n4")
return add_suffix(pagename, "n")
end
return canonicalize_ed_form(form)
end
--------------------------------- MAIN PARSING/CONJUGATING CODE --------------------------------
local past_ptcs_given
if par1 and par1:find("<") then
-------------------------- ANGLE-BRACKET FORMAT --------------------------
if par2 or par3 or par4 then
error("Can't specify 2=, 3= or 4= when 1= contains angle brackets: " .. par1)
end
-- In the angle bracket format, we always copy the full past tense specs to the past participle
-- specs if none of the latter are given, so act as if the past participle is always given.
-- There is a separate check to see if the past tense and past participle are identical, in any case.
past_ptcs_given = true
-- (1) Parse the indicator specs inside of angle brackets.
local function parse_indicator_spec(angle_bracket_spec)
local inside = angle_bracket_spec:match("^<(.*)>$")
assert(inside)
local segments = put.parse_balanced_segment_run(inside, "[", "]")
local comma_separated_groups = put.split_alternating_runs(segments, ",")
if #comma_separated_groups > 4 then
error("Too many comma-separated parts in indicator spec: " .. angle_bracket_spec)
end
local function fetch_qualifiers(separated_group)
local qualifiers
for j = 2, #separated_group - 1, 2 do
if separated_group[j + 1] ~= "" then
error("Extraneous text after bracketed qualifiers: '" .. concat(separated_group) .. "'")
end
if not qualifiers then
qualifiers = {}
end
insert(qualifiers, separated_group[j])
end
return qualifiers
end
local function fetch_specs(comma_separated_group)
if not comma_separated_group then
return {{}}
end
local specs = {}
local colon_separated_groups = put.split_alternating_runs(comma_separated_group, ":")
for _, colon_separated_group in ipairs(colon_separated_groups) do
local form = colon_separated_group[1]
if form == "*" or form == "++*" then
error("* and ++* not allowed inside of indicator specs: " .. angle_bracket_spec)
end
if form == "" then
form = nil
end
insert(specs, {form = form, q = fetch_qualifiers(colon_separated_group)})
end
return specs
end
local s_specs = fetch_specs(comma_separated_groups[1])
local ing_specs = fetch_specs(comma_separated_groups[2])
local ed_specs = fetch_specs(comma_separated_groups[3])
local en_specs = fetch_specs(comma_separated_groups[4])
for _, spec in ipairs(s_specs) do
if spec.form == "++" and #ing_specs == 1 and not ing_specs[1].form and not ing_specs[1].q
and #ed_specs == 1 and not ed_specs[1].form and not ed_specs[1].q then
ing_specs[1].form = "++"
ed_specs[1].form = "++"
break
end
end
return {
forms = {},
s_specs = s_specs,
ing_specs = ing_specs,
ed_specs = ed_specs,
en_specs = en_specs,
}
end
local parse_props = {
parse_indicator_spec = parse_indicator_spec,
}
local alternant_multiword_spec = iut.parse_inflected_text(par1, parse_props)
-- (2) Check for user-specified brackets; remove any links from the lemma, but remember the original
-- form so we can use it below in the 'lemma_linked' form.
-- Check to see if there are brackets in the pre-text or post-text. If so, use the linked lemma (with the
-- verb autolinked unless noautolinkverb is given). Otherwise, use the default headword algorithm.
local function check_bracket(val)
if val:find("%[%[") then
alternant_multiword_spec.saw_bracket = true
end
end
for _, alternant_or_word_spec in ipairs(alternant_multiword_spec.alternant_or_word_specs) do
check_bracket(alternant_or_word_spec.before_text)
if alternant_or_word_spec.alternants then
for _, multiword_spec in ipairs(alternant_or_word_spec.alternants) do
for _, word_spec in ipairs(multiword_spec.word_specs) do
check_bracket(word_spec.before_text)
end
check_bracket(multiword_spec.post_text)
end
end
end
check_bracket(alternant_multiword_spec.post_text)
iut.map_word_specs(alternant_multiword_spec, function(base)
if base.lemma == "" then
base.lemma = pagename
end
base.orig_lemma = base.lemma
base.lemma = remove_links(base.lemma)
if args.noautolinkverb or base.orig_lemma:find("%[%[") then
base.linked_lemma = base.orig_lemma
else
base.linked_lemma = "[[" .. base.orig_lemma .. "]]"
end
end)
-- (3) Conjugate the verbs according to the indicator specs parsed above.
local all_verb_slots = {
lemma = "infinitive",
lemma_linked = "infinitive",
s_form = "3|s|pres",
ing_form = "pres|ptcp",
ed_form = "past",
en_form = "past|ptcp",
}
local function conjugate_verb(base)
local def_s_form, def_ing_form, def_ed_form = base_default_verb_forms(base.lemma)
local function process_specs(slot, specs, default_form, canonicalize_plusplus)
for _, spec in ipairs(specs) do
local form = spec.form
if not form or form == "+" then
form = default_form
elseif form == "++" then
form = canonicalize_plusplus()
end
-- If there's a ~ in the form, substitute it with the lemma,
-- but make sure to first replace % in the lemma with %% so that
-- it doesn't get interpreted as a capture replace expression.
if form:find("~") then
-- Assign to a var because gsub returns multiple values.
local subbed_lemma = base.lemma:gsub("%%", "%%%%")
form = form:gsub("~", subbed_lemma)
end
-- If the form is -, don't insert any forms, which will result
-- in there being no overall forms (in fact it will be nil).
-- We check for that down below and substitute a single "-" as
-- the form, which in turn gets turned into special labels like
-- "no present participle".
if form ~= "-" then
iut.insert_form(base.forms, slot, {form = form, footnotes = spec.q})
end
end
end
process_specs("s_form", base.s_specs, def_s_form,
function() return compute_plusplus_s_form(base.lemma, def_s_form) end)
process_specs("ing_form", base.ing_specs, def_ing_form,
function() return compute_double_last_cons_stem(base.lemma) .. "ing" end)
process_specs("ed_form", base.ed_specs, def_ed_form,
function() return compute_double_last_cons_stem(base.lemma) .. "ed" end)
-- If the -en spec is completely missing, substitute the -ed spec in its entirely.
-- Otherwise, if individual -en forms are missing or use +, we will substitute the
-- default -ed form, as with the -ed spec.
local en_specs = base.en_specs
if #en_specs == 1 and not en_specs[1].form and not en_specs[1].q then
en_specs = base.ed_specs
end
process_specs("en_form", en_specs, def_ed_form,
function() return compute_double_last_cons_stem(base.lemma) .. "ed" end)
iut.insert_form(base.forms, "lemma", {form = base.lemma})
-- Add linked version of lemma for use in head=. We write this in a general fashion in case
-- there are multiple lemma forms (which isn't possible currently at this level, although it's
-- possible overall using the ((...,...)) notation).
iut.insert_forms(base.forms, "lemma_linked", iut.map_forms(base.forms.lemma, function(form)
if form == base.lemma and base.linked_lemma:find("%[%[") then
return base.linked_lemma
else
return form
end
end))
end
local inflect_props = {
slot_table = all_verb_slots,
inflect_word_spec = conjugate_verb,
}
iut.inflect_multiword_or_alternant_multiword_spec(alternant_multiword_spec, inflect_props)
-- (4) Fetch the forms and put the conjugated lemmas in data.heads if not explicitly given.
local function fetch_forms(slot)
local forms = alternant_multiword_spec.forms[slot]
-- See above. This should only occur if the user explicitly used -
-- for a spec.
if not forms or #forms == 0 then
forms = {{form = "-"}}
end
return forms
end
pres_3sgs = fetch_forms("s_form")
pres_ptcs = fetch_forms("ing_form")
pasts = fetch_forms("ed_form")
past_ptcs = fetch_forms("en_form")
-- Use the "linked" form of the lemma as the head if no head= explicitly given and the user specified brackets
-- in one of the lemmas. Otherwise we use the default headword-linking algorithm.
if #data.user_specified_heads == 0 and alternant_multiword_spec.saw_bracket then
data.heads = {}
for _, lemma_obj in ipairs(alternant_multiword_spec.forms.lemma_linked) do
local quals, refs = iut.convert_footnotes_to_qualifiers_and_references(lemma_obj.footnotes)
insert(data.heads, {term = lemma_obj.form, q = quals, refs = refs})
end
end
else
-------------------------- SEPARATE-PARAM FORMAT --------------------------
local pres_3sg, pres_ptc, past
if par1 and not (par2 or par3) then
-- Use of a single parameter other than "++", "*" or "++*" is now the "legacy" format,
-- and no longer supported.
if par1 == "es" or par1 == "ies" or par1 == "d" then
error("Legacy parameter 1=es/ies/d no longer supported, just use 'en-verb' without params")
elseif par1 == "++" or par1 == "*" or par1 == "++*" then
pres_3sg = canonicalize_s_form(par1)
pres_ptc = canonicalize_ing_form(par1)
past = canonicalize_ed_form(par1)
else
error("Legacy parameter 1=STEM no longer supported, just use 'en-verb' without params")
end
else
if par2 then
track("xxx2")
end
if par3 then
track("xxx3")
end
end
if not pres_3sg or not pres_ptc or not past then
-- Either all three should be set above, or none of them.
assert(not pres_3sg and not pres_ptc and not past)
if par1 then
pres_3sg = canonicalize_s_form(par1)
else
pres_3sg = new_default_s
end
if par2 then
pres_ptc = canonicalize_ing_form(par2)
else
pres_ptc = new_default_ing
end
if par3 then
past = canonicalize_ed_form(par3)
else
past = new_default_ed
end
end
local past_ptc
if par4 then
past_ptcs_given = true
past_ptc = canonicalize_en_form(par4)
track("xxx4")
else
past_ptc = past
end
pres_3sgs = {{form = pres_3sg}}
pres_ptcs = {{form = pres_ptc}}
pasts = {{form = past}}
past_ptcs = {{form = past_ptc}}
end
------------------------------------------- HANDLE OVERRIDES ------------------------------------------
local function strip_brackets(qualifiers)
if not qualifiers then
return nil
end
local stripped_qualifiers = {}
for _, qualifier in ipairs(qualifiers) do
local stripped_qualifier = qualifier:match("^%[(.*)%]$")
if not stripped_qualifier then
error("Internal error: Qualifier should be surrounded by brackets at this stage: " .. qualifier)
end
insert(stripped_qualifiers, stripped_qualifier)
end
return stripped_qualifiers
end
local function collect_forms(label, accel_form, defaults, overrides, override_qualifiers, canonicalize)
if defaults[1].form == "-" then
return {label = "no " .. label}
else
local into_table = {label = label, accel = {form = accel_form}}
local maxindex = math.max(#defaults, overrides.maxindex)
local qualifiers = override_qualifiers[1] and {override_qualifiers[1]} or strip_brackets(defaults[1].footnotes)
insert(into_table, {term = defaults[1].form, q = qualifiers})
-- Present 3rd singular
for i = 2, maxindex do
local override_form = canonicalize(overrides[i])
if override_form then
-- If there is an override such as past_ptc2=..., only use the qualifier specified
-- using an override (past_ptc2_qual=...), if any; it doesn't make sense to combine
-- an override form with a qualifier specified inside of angle brackets.
insert(into_table, {term = override_form, q = {override_qualifiers[i]}})
elseif defaults[i] then
-- If the form comes from inside angle brackets, allow any override qualifier
-- (past_ptc2_qual=...) to override any qualifier specified inside of angle brackets.
-- FIXME: Maybe we should throw an error here if both exist.
local qualifiers = override_qualifiers[i] and {override_qualifiers[i]} or strip_brackets(defaults[i].footnotes)
insert(into_table, {term = defaults[i].form, q = qualifiers})
end
end
return into_table
end
end
local pres_3sg_infls = collect_forms("third-person singular simple present", "s-verb-form",
pres_3sgs, args[1], args.pres_3sg_qual, canonicalize_s_form)
local pres_ptc_infls = collect_forms("present participle", "ing-form",
pres_ptcs, args[2], args.pres_ptc_qual, canonicalize_ing_form)
local past_infls = collect_forms("simple past", "spast",
pasts, args[3], args.past_qual, canonicalize_ed_form)
local past_ptc_infls = collect_forms("past participle", "past|part",
past_ptcs, args[4], args.past_ptc_qual, canonicalize_en_form)
-- Are the past forms identical to the past participle forms? If so, we use a single
-- combined "simple past and past participle" label on the past tense forms.
-- We check for two conditions: Either no past participle forms were given at all, or
-- they were given but are identical in every way (all forms and qualifiers) to the past
-- tense forms. The former "no explicit past participle forms" check is important in the
-- "separate-parameter" format; if past tense overrides are given and no past participle
-- forms given, the past tense overrides should apply to the past participle as well.
-- In the angle-bracket format, it's expected that all forms and qualifiers are specified
-- using that format, and we explicitly copy past tense forms and qualifiers to past
-- participle ones if the latter are omitted, so we disable to "no explicit past participle
-- forms" check.
if args[4].maxindex > 0 or args.past_ptc_qual.maxindex > 0 then
past_ptcs_given = true
end
local identical = true
-- For the past and past participle to be identical, there must be
-- the same number of inflections, and each inflection must match
-- in term and qualifiers.
if #past_infls ~= #past_ptc_infls then
identical = false
else
for key, val in ipairs(past_infls) do
if past_ptc_infls[key].term ~= val.term then
identical = false
break
else
local quals1 = past_ptc_infls[key].q
local quals2 = val.q
if (not not quals1) ~= (not not quals2) then
-- one is nil, the other is not
identical = false
elseif quals1 and quals2 then
-- qualifiers present in both; each qualifier must match
if #quals1 ~= #quals2 then
identical = false
else
for k, v in ipairs(quals1) do
if v ~= quals2[k] then
identical = false
break
end
end
end
end
if not identical then
break
end
end
end
end
-- Insert the forms
insert(data.inflections, pres_3sg_infls)
insert(data.inflections, pres_ptc_infls)
if not past_ptcs_given or identical then
if past_ptcs[1].form == "-" then
past_infls.label = "no simple past or past participle"
else
past_infls.label = "simple past and past participle"
past_infls.accel = {form = "ed-form"}
end
insert(data.inflections, past_infls)
else
insert(data.inflections, past_infls)
insert(data.inflections, past_ptc_infls)
end
if pagename:find(" ") then
-- Check for placeholder "it"
local words = split(pagename, " ")
for _, word in ipairs(words) do
if word == "it" or word == "its" or word == "it's" then
insert(data.categories, 'Perkataan dengan sandaran "it" bahasa ' .. langname)
break
end
end
-- Check for phrasal verbs
local phrasal_adverbs = list_to_set{
-- NOTE: This should only contain common phrasal adverbs, not random words like [[low]],
-- [[adrift]], etc.
"aback",
"about",
"above",
"across",
"after",
"against",
"ahead",
"along",
"apart",
"around",
"as",
"aside",
"at",
"away",
"back",
"before",
"behind",
"below",
"between",
"beyond",
"by",
"down",
"for",
"forth",
"from",
"in",
"into",
"of",
"off",
"on",
"onto",
"out",
"over",
"past",
"round",
"through",
"to",
"together",
"towards",
"under",
"up",
"upon",
"with",
"without",
}
local allowed_non_adverb_words = list_to_set{
"it",
"one",
"oneself",
"someone",
}
local base = pagename
local seen_adverbs = {}
-- Only consider a verb to be phrasal if it consists of a single base verb followed exclusively by either
-- adverbs from `phrasal_adverbs` or placeholder words from `allowed_non_adverb_words`, where at
-- least one following word is from `phrasal_adverbs` (hence [[can it]] is not a phrasal verb).
while true do
local prev, word = base:match("^(.+) (.-)$")
if not prev then
break
end
if phrasal_adverbs[word] then
insert(seen_adverbs, word)
elseif allowed_non_adverb_words[word] then
-- do nothing
else
break
end
base = prev
end
if not base:find(" ") and #seen_adverbs > 0 then
insert(data.categories, "Kata kerja frasa bahasa " .. langname)
for i = #seen_adverbs, 1, -1 do
insert(data.categories, "Kata kerja frasa dibentuk dengan " .. seen_adverbs[i] .. " bahasa " .. langname
)
end
end
end
end,
}
return export
863ze9f4b4yxqz6zmf9rogk10ovte95
375863
375862
2026-09-25T11:38:22Z
SNN95
2113
375863
Scribunto
text/plain
local export = {}
local pos_functions = {}
--[==[
Pengarang dari 2020 dan seterusnya: kebanyakannya Benwing2, dengan sumbangan besar daripada Theknightwho. Berdasarkan versi terdahulu oleh Rua
(kini kebanyakannya telah ditulis semula), dengan sumbangan daripada Erutuon dan lain-lain (lihat sejarah untuk atribusi penuh).
]==]
local force_cat = false -- untuk pengujian; jika benar, kategori akan muncul pada halaman ruang nama utama
local require = require
local require_when_needed = require("Module:require when needed")
local en_utilities_module = "Module:en-utilities"
local headword_utilities_module = "Module:headword utilities"
local headword_module = "Module:headword"
local inflection_utilities_module = "Module:inflection utilities"
local parse_utilities_module = "Module:parse utilities"
local JSON_module = "Module:JSON"
local labels_module = "Module:labels"
local links_module = "Module:links"
local parameters_module = "Module:parameters"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local utilities_module = "Module:utilities"
local yesno_module = "Module:yesno"
local iut = require_when_needed(inflection_utilities_module)
local put = require_when_needed(parse_utilities_module)
local m_headword_utilities = require_when_needed(headword_utilities_module)
local add_links_to_multiword_term = require_when_needed(headword_utilities_module, "add_links_to_multiword_term")
local add_suffix = require_when_needed(en_utilities_module, "add_suffix")
local apply_link_modifiers = require_when_needed(headword_utilities_module, "apply_link_modifiers")
local concat = table.concat
local deepEquals = require_when_needed(table_module, "deepEquals")
local dump = mw.dumpObject
local format_categories = require_when_needed(utilities_module, "format_categories")
local full_headword = require_when_needed(headword_module, "full_headword")
local get_label_info = require_when_needed(labels_module, "get_label_info")
local get_link_page = require_when_needed(links_module, "get_link_page")
local glossary_link = require_when_needed(headword_utilities_module, "glossary_link")
local insert = table.insert
local insertIfNot = require_when_needed(table_module, "insertIfNot")
local ipairs = ipairs
local is_regular_plural = require_when_needed(en_utilities_module, "is_regular_plural")
local list_to_set = require_when_needed(table_module, "listToSet")
local pairs = pairs
local process_params = require_when_needed(parameters_module, "process")
local remove = table.remove
local remove_links = require_when_needed(links_module, "remove_links")
local replacement_escape = require_when_needed(string_utilities_module, "replacement_escape")
local shallowCopy = require_when_needed(table_module, "shallowCopy")
local singularize = require_when_needed(en_utilities_module, "singularize")
local split = require_when_needed(string_utilities_module, "split")
local toJSON = require_when_needed(JSON_module, "toJSON")
local toNFD = mw.ustring.toNFD
local type = type
local ulen = require_when_needed(string_utilities_module, "len")
local ulower = require_when_needed(string_utilities_module, "lower")
local umatch = require_when_needed(string_utilities_module, "match")
local u = require_when_needed(string_utilities_module, "char")
local ugsub = require_when_needed(string_utilities_module, "gsub")
local lang = require("Module:languages").getByCode("en")
local langname = lang:getCanonicalName()
local list_param = {list = true, disallow_holes = true}
local list_allow_holes = {list = true, allow_holes = true}
local boolean_param = {type = "boolean"}
local function ine(val)
if val == "" then return nil else return val end
end
local function track(page)
require("Module:debug/track")("en-headword/" .. page)
return true
end
------------------------------------------- FUNGSI UTILITI ------------------------------------------
-- Menghurai dan mengembalikan infleksi yang tidak memerlukan pemprosesan tambahan. Argumen mentah diambil daripada `args[field]`, yang
-- dihurai untuk pengubah suai dalam baris.
local function parse_inflection(args, field, is_head)
local argfield = field
if type(argfield) == "table" then
argfield = argfield[1]
end
return m_headword_utilities.parse_term_list_with_modifiers {
paramname = field,
forms = args[argfield],
splitchar = ",",
is_head = is_head,
}
end
-- Masukkan infleksi yang dihurai dalam `terms` (seperti yang dihurai oleh `parse_inflection`) ke dalam `data.inflections`, dengan label
-- `label` dan spesifikasi pencepat pilihan `accel`.
local function insert_inflection(data, terms, label, accel, no_label)
for _, termobj in ipairs(terms) do
m_headword_utilities.remove_termobj_field_modifiers(termobj)
end
m_headword_utilities.insert_inflection {
headdata = data,
terms = terms,
label = label,
no_label = no_label,
accel = accel and {form = accel} or nil,
}
end
-- Masukkan label tetap `label` ke dalam infleksi untuk `data`. Jika `originating_term` dibekalkan, salin hiasan
-- daripadanya ke dalam label tetap.
local function insert_fixed_inflection(data, label, originating_term)
m_headword_utilities.insert_fixed_inflection {
headdata = data,
originating_term = originating_term,
label = label,
}
end
-- Menghurai dan memasukkan infleksi yang tidak memerlukan pemprosesan tambahan ke dalam `data.inflections`. Argumen mentah diambil daripada
-- `args[field]`, yang dihurai untuk pengubah suai dalam baris. `label` ialah label yang diberikan kepada infleksi;
-- `accel` ialah bentuk pencepat, atau nil.
local function parse_and_insert_inflection(data, args, field, label, accel)
m_headword_utilities.parse_and_insert_inflection {
headdata = data,
forms = args[field],
paramname = field,
splitchar = ",",
label = label,
accel = accel and {form = accel} or nil,
}
end
-- Fungsi-fungsi ini digunakan secara langsung dalam format <> dan juga dalam fungsi utiliti #2 di bawah.
local function compute_double_last_cons_stem(term)
local last_cons = term:match("([bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])$")
if not last_cons then
error("Kata dasar kata kerja '" .. term .. "' mesti berakhir dengan konsonan untuk menggunakan ++")
end
return term .. last_cons
end
local function compute_plusplus_s_form(term, default_s_form)
if term:find("[szx]$") then
-- regas -> regasses, derez -> derezzes
return compute_double_last_cons_stem(term) .. "es"
else
return default_s_form
end
end
-- Titik masuk utama.
-- Ini adalah satu-satunya fungsi yang boleh dipanggil daripada templat.
function export.show(frame)
local iparams = {
[1] = true,
}
local iargs = require("Module:parameters").process(frame.args, iparams)
local parargs = frame:getParent().args
local poscat = iargs[1]
local pos_in_1 = not poscat
if pos_in_1 then
poscat = ine(parargs[1]) or
mw.title.getCurrentTitle().fullText == "Template:en-head" and "interjection" or
error("Golongan kata mesti dinyatakan pada 1=")
poscat = require(headword_module).canonicalize_pos(poscat)
end
local indexing_poscat = pos_in_1 and "head" or poscat
local params = {
["head"] = list_param,
["id"] = true,
["json"] = boolean_param,
["sort"] = true,
["splithyph"] = boolean_param,
["nosplithyph"] = boolean_param,
["hyphspace"] = boolean_param,
["nolink"] = boolean_param,
["nolinkhead"] = {type = "boolean_param", alias_of = "nolink"},
["suffix"] = boolean_param,
["nosuffix"] = boolean_param,
["nomultiwordcat"] = boolean_param,
["abbr"] = list_param,
["the"] = true,
["def"] = {alias_of = "the"},
["pagename"] = true, -- untuk pengujian
}
if pos_in_1 then
params[1] = {required = true} -- diperlukan tetapi diabaikan kerana sudah diproses di atas
end
local pos_data = pos_functions[indexing_poscat]
local pos_func
if pos_data then
local pos_params = pos_data.params
if pos_params then
for key, val in pairs(pos_params) do
params[key] = val
end
end
pos_func = pos_data.func
end
local args = process_params(parargs, params)
-- Ambil kira tajuk yang tidak disokong, cth. 'C|N>K' bukannya 'Unsupported titles/C through N to K'.
local pagename = args.pagename or mw.loadData("Module:headword/data").pagename
local user_specified_heads = parse_inflection(args, "head", "is_head")
local heads = user_specified_heads
local autohead
if args.nolink or not pagename:find("[ '%-]") then
autohead = pagename
else
local en_no_split_apostrophe_words = list_to_set {
"one's",
"someone's",
"he's",
"she's",
"it's",
}
local en_include_hyphen_prefixes = list_to_set {
-- Kita tidak menyertakan perkara yang juga merupakan perkataan walaupun ia selalunya (mungkin kebanyakannya) merupakan imbuhan awalan, cth.
-- "be", "counter", "cross", "extra", "half", "mid", "over", "pan", "under".
"acro",
"acousto",
"Afro",
"agro",
"anarcho",
"angio",
"Anglo",
"ante",
"anti",
"arch",
"auto",
"bi",
"bio",
"cis",
"co",
"cryo",
"crypto",
"de",
"demi",
"eco",
"electro",
"Euro",
"ex",
"Greco",
"hemi",
"hydro",
"hyper",
"hypo",
"infra",
"Indo",
"inter",
"intra",
"Judeo",
"macro",
"meta",
"micro",
"mini",
"multi",
"neo",
"neuro",
"non",
"para",
"peri",
"post",
"pre",
"pro",
"proto",
"pseudo",
"re",
"semi",
"sub",
"super",
"trans",
"un",
"vice",
}
local function is_english(term)
local title = mw.title.new(term)
if title and title.exists then
local content = title:getContent()
if content and content:find("==English==\n") then
return true
end
end
return false
end
local function en_split_hyphen_when_space(word)
if not word:find("-", nil, true) then
return nil
end
if args.hyphspace then
return "[[" .. word:gsub("%-+", " ") .. "|" .. word .. "]]"
end
if args.nosplithyph then
return "[[" .. word .. "]]"
end
if not args.splithyph then
local space_word = word:gsub("%-+", " ")
if is_english(space_word) then
return "[[" .. space_word .. "|" .. word .. "]]"
end
if is_english(word) then
return "[[" .. word .. "]]"
end
end
return nil
end
local function en_split_apostrophe(word)
local base = word:match("^(.*)'s$")
if base then
return "[[" .. base .. "]][[-'s|'s]]"
end
-- Hanya anggap koma atas akhir sebagai kepunyaan jika didahului oleh sesuatu yang kelihatan seperti jamak yang berakhir dengan /z/.
-- Khususnya kita tidak mahu melakukannya untuk perkataan seperti [[truckin']].
base = word:match("^(.*[sxz])'$")
if base then
if base:find("s$") then
local sg = singularize(base)
if is_english(sg) then
return "[[" .. sg .. "|" .. base .. "]][[-'|']]"
end
end
return "[[" .. base .. "]][[-'|']]"
end
return "[[" .. word .. "]]"
end
autohead = add_links_to_multiword_term(pagename, {
split_hyphen_when_space = en_split_hyphen_when_space,
split_apostrophe = en_split_apostrophe,
no_split_apostrophe_words = en_no_split_apostrophe_words,
include_hyphen_prefixes = en_include_hyphen_prefixes,
})
end
if not heads[1] then
heads = {{term = autohead}}
else
for _, headobj in ipairs(heads) do
local head = headobj.term
if head:find("^~") then
head = apply_link_modifiers(autohead, head:sub(2), lang)
headobj.term = head
elseif head:find("^[!?]$") then
-- Jika head= yang nyata hanya mengandungi ! atau ?, tambahkannya di hujung kata dasar lalai.
headobj.term = autohead .. head
end
if head == autohead then
track("redundant-head")
end
end
end
-- mengendalikan the=/def=
if args.the == "~" then
local newheads = {}
for _, headobj in ipairs(heads) do
local barehead = shallowCopy(headobj)
insert(newheads, barehead)
headobj.term = "the " .. headobj.term
insert(newheads, headobj)
end
heads = newheads
elseif args.the then
local the = require(yesno_module)(args.the)
if the then
for _, headobj in ipairs(heads) do
headobj.term = "the " .. headobj.term
end
end
end
local data = {
lang = lang,
pos_category = poscat,
categories = {},
heads = heads,
user_specified_heads = user_specified_heads,
-- Kita menggunakan algoritma pemisahan kita sendiri supaya kategori kata dasar lewah tidak tepat.
no_redundant_head_cat = true,
inflections = {},
nomultiwordcat = args.nomultiwordcat,
sort_key = args.sort,
pagename = pagename,
id = args.id,
force_cat_output = force_cat,
}
local function inscat(cat)
insert(data.categories, "Kategori: " .. langname .. " " .. cat)
end
local is_suffix = false
if args.suffix or not args.nosuffix and pagename:find("^%-") and not pagename:find("^%-%-") and poscat ~= "suffix forms" then
is_suffix = true
data.pos_category = "suffixes"
local singular_poscat = singularize(poscat)
inscat("akhiran membentuk " .. singular_poscat)
insert(data.inflections, {label = "akhiran membentuk " .. singular_poscat})
end
if pos_func then
pos_func(args, data, is_suffix)
end
local extra_categories = {}
if pagename:find("[Qq]") then
-- Semak q yang tidak diikuti oleh u. Kita mahu mengecualikan perkara seperti [[13q deletion syndrome]] dan [[BFOQ]] yang
-- tiada huruf kecil pada kedua-dua sisi, serta perkara seperti [[& seq.]] dan [[acq.]] yang merupakan
-- singkatan untuk perkataan yang mengandungi u selepasnya.
--
-- Julat anggaran bagi diakritik gabungan; kita mahu membuangnya supaya semakan di bawah untuk
-- huruf kecil bersebelahan q tidak terpengaruh dengan diakritik pada huruf.
local u300 = u(0x0300)
local u36F = u(0x036F)
local pagename_no_diacritics = ugsub(toNFD(pagename), "[" .. u300 .. "-" .. u36F .. "]", "")
if pagename_no_diacritics:find("[Qq][a-tv-z]") or pagename_no_diacritics:find("[a-z]q[^u.]") or
pagename_no_diacritics:find("[a-z]q$") then
inscat("perkataan yang mengandungi Q yang tidak diikuti oleh U")
end
end
-- toNFD melakukan penguraian, jadi huruf yang terurai kepada vokal ASCII
-- dan diakritik, seperti é, dikira sebagai vokal dan tidak
-- perlu disertakan dalam corak.
if not umatch(ulower(toNFD(pagename)), "[aeiouyæœøəªºαεηιουω]") then
inscat("perkataan yang dieja tanpa vokal")
end
if pagename:find("yre$") then
inscat('perkataan berakhir dengan "-yre"')
end
if not pagename:find(" ") and ulen(pagename) >= 25 then
insert(extra_categories, "Perkataan " .. langname .. " yang panjang")
end
if pagename:find("^[^aeiou ]*a[^aeiou ]*e[^aeiou ]*i[^aeiou ]*o[^aeiou ]*u[^aeiou ]*$") then
inscat("perkataan yang menggunakan semua vokal dalam susunan abjad")
end
parse_and_insert_inflection(data, args, "abbr", "abbreviation")
if args.json then
return toJSON(data)
end
return full_headword(data)
.. (extra_categories[1]
and format_categories(extra_categories, lang, args.sort)
or "")
end
local function make_default_comparative(word)
if word == "good" or word == "well" then
return {"better"}
elseif word == "bad" or word == "badly" then
return {"worse"}
elseif word == "far" then
return {"further", "farther"}
else
return {add_suffix(word, "r")}
end
end
local function make_default_superlative(word)
if word == "good" or word == "well" then
return {"best"}
elseif word == "bad" or word == "badly" then
return {"worst"}
elseif word == "far" then
return {"furthest", "farthest"}
else
return {add_suffix(word, "st.superlative")}
end
end
-- Fungsi ini melakukan kerja yang sama antara kata sifat dan kata keterangan.
local function process_comparative_args(data, args, plpos)
local pagename = data.pagename
local comps = parse_inflection(args, 1)
local sups = parse_inflection(args, "sup")
local outcomps, outsups
if args.componly then
if comps[1] then
error("Can't specify comparatives of comparative-only " .. plpos)
end
insert(data.inflections, {label = "hanya " .. glossary_link("bandingan")})
insert(data.categories, "Kategori: " .. langname .. " " .. plpos .. " bandingan-sahaja")
-- Tetapkan kepada senarai kosong supaya kita tidak mendapat sebarang output bentuk bandingan, tetapi proses bentuk 최상급 jika dinyatakan.
outcomps = {}
if not sups[1] then
-- Tetapkan kepada senarai kosong supaya kita tidak mendapat sebarang output bentuk 최상급 melainkan dinyatakan dengan jelas.
outsups = {}
end
elseif args.suponly then
if comps[1] or sups[1] then
error("Can't specify comparatives or superlatives of or superlative-only " .. plpos)
end
insert(data.inflections, {label = "hanya " .. glossary_link("palingan")})
insert(data.categories, "Kategori: " .. langname .. " " .. plpos .. " palingan-sahaja")
return
end
-- Jika parameter pertama ialah ?, maka jangan paparkan apa-apa, hanya kembalikan.
if comps[1] and comps[1].term == "?" then
if comps[2] then
error("Can't specify additional comparatives along with '?'")
end
if sups[1] then
error("Can't specify superlatives along with '?' for the comparative")
end
return
end
if comps[1] and comps[1].term == "-" then
local hyphencomp = remove(comps, 1) -- Buang "-" tetapi simpan untuk hiasan.
-- Bukan (secara amnya) boleh dibanding; sekali-sekala mungkin mempunyai bentuk bandingan
if comps[1] then
insert_fixed_inflection(data, "tidak selalunya <<comparable>>", hyphencomp)
elseif not sups[1] then
insert_fixed_inflection(data, "tidak <<comparable>>", hyphencomp)
insert(data.categories, "Kategori: " .. langname .. " " .. plpos .. " tidak boleh dibandingkan")
return
else
-- Tiada bentuk bandingan, tetapi mempunyai bentuk 최상급. insert_inflection() akan menjana 'tiada bandingan' dengan betul jika kita
-- hantar "-" sebagai nilai.
outcomps = {hyphencomp}
end
elseif not comps[1] then
comps = {{term = "more"}}
end
if not outcomps then -- bukan jika kita menetapkan `outcomps` kepada "-" di atas atau memproses kata yang hanya bentuk bandingan
outcomps = {}
-- Periksa setiap parameter yang diberikan dan bina bentuk bandingan dan palingan.
for _, compobj in ipairs(comps) do
local comp = compobj.term
if comp == "-" then
error("Comparative of '-' only allowed as first comparative")
end
if comp == "+" then
comp = "+more"
elseif comp == "more" and pagename ~= "many" and pagename ~= "much" then
comp = "+more"
elseif comp == "further" and pagename ~= "far" then
comp = "+further"
elseif comp == "better" and pagename ~= "good" and pagename ~= "well" then
comp = "+better"
elseif comp:find("~") then
comp = comp:gsub("~", replacement_escape(pagename))
end
compobj.origterm = comp
if comp == "+more" then
comp = "more [[" .. pagename .. "]]"
elseif comp == "+further" then
comp = {"further [[" .. pagename .. "]]", "farther [[" .. pagename .. "]]"}
elseif comp == "+better" then
comp = "better [[" .. pagename .. "]]"
elseif comp == "er" then
-- Tambah -er.
comp = add_suffix(pagename, "r")
elseif comp == "ier" then
if pagename:sub(-1) ~= "y" then
error("Can't specify 'ier' comparative unless the term ends with 'y': " .. pagename)
end
comp = pagename:gsub("e?y$", "ier")
elseif comp:find("^%+") then
local special = m_headword_utilities.get_special_indicator(comp, "noerror")
if special then
comp = m_headword_utilities.handle_multiword(pagename, special, make_default_comparative)
end
end
if type(comp) == "table" and not comp[2] then
comp = comp[1]
end
if type(comp) == "table" then
for i = 1, #comp - 1 do
local outobj = shallowCopy(compobj)
outobj.term = comp[i]
insert(outcomps, outobj)
end
compobj.term = comp[#comp]
insert(outcomps, compobj)
else
compobj.term = comp
insert(outcomps, compobj)
end
end
end
if sups[1] and sups[1].term == "-" then
if sups[2] then
error("Can't specify '-' as superlative followed by further values")
end
-- Tiada bentuk palingan. insert_inflection() akan menjana 'tiada palingan' dengan betul jika kita hantar "-" sebagai nilai.
outsups = sups
else
if not sups[1] then
sups = {{term = "+"}}
end
end
-- `outsups` akan ditetapkan jika kita menetapkan `outsups` kepada "-" di atas atau memproses kata yang hanya bentuk bandingan tanpa bentuk palingan.
if not outsups then
outsups = {}
local function process_sup(sup, special, supobj, compobj)
if special then
sup = m_headword_utilities.handle_multiword(pagename, special, make_default_superlative)
elseif sup == "-" or sup == "+" then
error(("Internal error: Superlative value of '%s' should have been handled earlier"):format(sup))
elseif sup == "+most" then
sup = "most [[" .. pagename .. "]]"
elseif sup == "+furthest" then
sup = {"furthest [[" .. pagename .. "]]", "farthest [[" .. pagename .. "]]"}
elseif sup == "+best" then
sup = "best [[" .. pagename .. "]]"
elseif sup == "est" then
-- Tambah -est.
sup = add_suffix(pagename, "st.superlative")
elseif sup == "iest" then
if pagename:sub(-1) ~= "y" then
error("Can't specify 'iest' superlative unless the term ends with 'y': " .. pagename)
end
sup = pagename:gsub("e?y$", "iest")
end
if type(sup) == "table" and not sup[2] then
sup = sup[1]
end
if compobj then
supobj = shallowCopy(supobj)
supobj = m_headword_utilities.combine_termobj_decorations(supobj, compobj)
end
if type(sup) == "table" then
for i = 1, #sup - 1 do
local outobj = shallowCopy(supobj)
outobj.term = sup[i]
insert(outsups, outobj)
end
supobj.term = sup[#sup]
insert(outsups, supobj)
else
supobj.term = sup
insert(outsups, supobj)
end
end
for _, supobj in ipairs(sups) do
local sup = supobj.term
if sup == "-" then
error("Superlative of '-' only allowed as first superlative")
end
if sup == "+" then
if not comps[1] then
error("Superlative of '+' can't be specified when there are no comparatives")
end
for _, compobj in ipairs(comps) do
local comp = compobj.origterm
local special
if comp == "+more" then
sup = "+most"
elseif comp == "+further" then
sup = "+furthest"
elseif comp == "+better" then
sup = "+best"
elseif comp == "er" then
sup = "est"
elseif comp == "ier" then
sup = "iest"
else
if comp:find("^%+") then
special = m_headword_utilities.get_special_indicator(comp, "noerror")
end
if not special then
-- Jika bentuk bandingan penuh diberikan, kemudian jana bentuk palingan dengan menukar -er kepada
-- -est.
if comp:sub(-2) == "er" then
sup = comp:sub(1, -3) .. "est"
else
error(("The superlative cannot be derived automatically from comparative '%s' because it doesn't end in -er"):format(comp))
end
end
end
process_sup(sup, special, supobj, compobj)
end
else
local special = m_headword_utilities.get_special_indicator(sup, "noerror")
-- Lakukan sebahagian kerja di sini dan bukannya dalam process_sup() supaya kita tidak berakhir dengan memproses dua kali kata dengan '~'
-- di dalamnya atau kata yang secara kebetulan 'most' atau serupa selepas menggantikan ~ dalam bentuk bandingan.
if not special then
if sup == "most" and pagename ~= "many" and pagename ~= "much" then
sup = "+most"
elseif sup == "furthest" and pagename ~= "far" then
sup = "+furthest"
elseif sup == "best" and pagename ~= "good" and pagename ~= "well" then
sup = "+best"
elseif sup:find("~") then
sup = sup:gsub("~", replacement_escape(pagename))
end
end
process_sup(sup, special, supobj)
end
end
end
insert_inflection(data, outcomps, "<<comparative>>", "comparative")
insert_inflection(data, outsups, "<<superlative>>", "superlative")
end
pos_functions["adjectives"] = {
params = {
[1] = list_param,
["comp_qual"] = {list = "comp\1_qual", allow_holes = true, replaced_by = false,
instead = "gunakan pengubah suai dalam baris <l:...> atau <q:...> pada nilai bandingan",
},
["sup"] = list_param,
["sup_qual"] = {list = "sup\1_qual", allow_holes = true, replaced_by = false,
instead = "gunakan pengubah suai dalam baris <l:...> atau <q:...> pada nilai palingan",
},
["componly"] = boolean_param,
["suponly"] = boolean_param,
},
func = function(args, data)
-- Proses bentuk bandingan dan palingan.
process_comparative_args(data, args, "kata sifat")
end,
}
pos_functions["adverbs"] = {
params = {
[1] = list_param,
["comp_qual"] = {list = "comp\1_qual", allow_holes = true, replaced_by = false,
instead = "gunakan pengubah suai dalam baris <l:...> atau <q:...> pada nilai bandingan",
},
["sup"] = list_param,
["sup_qual"] = {list = "sup\1_qual", allow_holes = true, replaced_by = false,
instead = "gunakan pengubah suai dalam baris <l:...> atau <q:...> pada nilai palingan",
},
["componly"] = boolean_param,
["suponly"] = boolean_param,
},
func = function(args, data)
-- Proses bentuk bandingan dan palingan.
process_comparative_args(data, args, "kata keterangan")
end,
}
local function escape(str)
return (str:gsub("\\([:#])", "\\\\%1")
:gsub("[:#]", "\\%0"))
end
local function canonicalize_plural(pl, pagename, pos)
if pl == "+" then
return escape(add_suffix(pagename, "s.plural", pos))
elseif pl == "++" then
return escape(compute_plusplus_s_form(pagename, add_suffix(pagename, "s.plural", pos)))
elseif pl == "*" then
return escape(pagename)
elseif pl == "ies" then
if pagename:sub(-1) == "y" then
return escape(pagename:gsub("e?y$", pl))
end
error("Can't specify 'ies' plural unless the term ends with 'y'.")
elseif pl == "s" or pl == "es" or pl == "'s" then
return escape(pagename .. pl)
end
end
local function do_nouns(args, data, pos)
local pagename = data.pagename
pos = pos or "noun"
local plurals = parse_inflection(args, 1)
local function insert_plurale_tantum_inflections(is_plural_only, originating_label)
if args.sg[1] then
insert_fixed_inflection(data, "lazimnya jamak", originating_label)
parse_and_insert_inflection(data, args, "sg", "tunggal")
elseif is_plural_only then
insert_fixed_inflection(data, "jamak sahaja", originating_label)
end
if args.attr[1] then
parse_and_insert_inflection(data, args, "attr", "atributif")
end
end
local function first_pl_term()
return plurals[1] and plurals[1].term or nil
end
if first_pl_term() == "p" then
-- plurale tantum
if plurals[2] then
error("With plurale tantum noun, can't specify more than one plural")
end
data.genders = {"p"} -- ini sepatutnya memasukkan secara automatik kategori 'pluralia tantum' yang betul
insert_plurale_tantum_inflections("jamak sahaja", plurals[1])
return
end
local function inscat(cat)
insert(data.categories, "Kategori: " .. langname .. " " .. cat)
end
local need_default_plural = pos == "noun"
if first_pl_term() == "sp" then
-- ditafsirkan sebagai tunggal atau jamak
sp = remove(plurals, 1) -- Buang "sp" tetapi simpan untuk hiasannya.
inscat("kata nama ditafsirkan sebagai tunggal atau jamak")
data.genders = {"s", "p"} -- ini sepatutnya memasukkan secara automatik kategori 'pluralia tantum' yang betul
insert_plurale_tantum_inflections(nil, sp)
need_default_plural = false
elseif first_pl_term() == "-" then
-- Kata nama tak berbilang; sekali-sekala mungkin mempunyai bentuk jamak
local hyphpl = remove(plurals, 1) -- Buang "-" tetapi simpan untuk hiasan.
inscat("kata nama tak berbilang")
-- Jika bentuk jamak diberikan secara eksplisit, kemudian tunjukkan "lazimnya"
if plurals[1] then
insert_fixed_inflection(data, "lazimnya <<uncountable>>", hyphpl)
else
insert_fixed_inflection(data, "<<uncountable>>", hyphpl)
end
need_default_plural = false
elseif first_pl_term() == "#" then
-- Selalunya boleh dibilang (cth., "grilled cheese")
local hashpl = remove(plurals, 1) -- Buang "#" tetapi simpan untuk hiasan.
insert_fixed_inflection(data, "lazimnya <<countable>>", hashpl)
inscat("kata nama tak berbilang")
inscat("kata nama boleh dibilang")
-- Jika tiada bentuk jamak diberikan, tambah satu lalai sekarang
if not plurals[1] then
plurals[1] = {term = escape(add_suffix(pagename, "s.plural", pos))}
end
elseif first_pl_term() == "~" then
-- Kata nama berbilang/tak berbilang campur, sentiasa mempunyai bentuk jamak
local tildepl = remove(plurals, 1) -- Buang "~" tetapi simpan untuk hiasan.
insert_fixed_inflection(data, "<<countable>> dan <<uncountable>>", tildepl)
inscat("kata nama tak berbilang")
inscat("kata nama boleh dibilang")
-- Jika tiada bentuk jamak diberikan, tambah satu lalai sekarang
if not plurals[1] then
plurals[1] = {term = escape(add_suffix(pagename, "s.plural", pos))}
end
end
-- Bentuk jamak tidak diketahui
if first_pl_term() == "?" then
local questionpl = remove(plurals, 1) -- Buang "?" tetapi simpan untuk hiasan.
-- Tidak dikehendaki; lihat [[Wiktionary:Tea_room/2021/August#"Plural unknown or uncertain"]]
-- insert_fixed_inflection(data, "plural unknown or uncertain", questionpl)
inscat("kata nama dengan bentuk jamak yang tidak diketahui atau tidak pasti")
if plurals[1] then
error("Can't specify explicit plurals along with '?' for unknown/uncertain plural")
end
return
end
-- Bentuk jamak tidak diperakui
if first_pl_term() == "!" then
local exclampl = remove(plurals, 1) -- Buang "!" tetapi simpan untuk hiasan.
insert_fixed_inflection(data, "bentuk jamak tidak diperakui", exclampl)
inscat("kata nama dengan bentuk jamak yang tidak diperakui")
if plurals[1] then
error("Can't specify explicit plurals along with '!' for unattested plural")
end
return
end
-- Jika tiada bentuk jamak diberikan, mungkin tambah satu lalai, sebaliknya (apabila "-" diberikan atau kata nama khas) kembalikan.
if not plurals[1] then
if not need_default_plural then
inscat("kata nama tak berbilang")
return
end
plurals[1] = {term = escape(add_suffix(pagename, "s.plural", pos))}
end
-- Terdapat bentuk jamak untuk dipaparkan, jadi paparkannya.
inscat("kata nama boleh dibilang")
local irregular, indeclinable
for i, pl in ipairs(plurals) do
local canon_pl = canonicalize_plural(pl.term, pagename, pos)
if canon_pl then
pl.term = canon_pl
end
local pl_term = get_link_page(pl.term, lang)
if not (pagename:find(" ") or is_regular_plural(pl_term, pagename)) then
irregular = true
if pl_term == pagename then
indeclinable = true
end
end
end
if irregular then
inscat("kata nama dengan bentuk jamak tak nalar")
end
if indeclinable then
inscat("kata nama tak terubah form")
end
insert_inflection(data, plurals, "jamak", "p")
end
-- Mengembalikan parameter yang akan digunakan untuk kata nama dan kata nama khas. Pada masa ini ia sama.
local noun_params = {
[1] = list_param,
["pl\1qual"] = {list = true, allow_holes = true, replaced_by = false,
instead = "gunakan pengubah suai dalam baris <l:...> atau <q:...> pada bentuk jamak",
},
-- Empat yang berikut hanya digunakan untuk pluralia tantum (1=p)
["sg"] = list_param,
["attr"] = list_param,
}
pos_functions["nouns"] = {
params = noun_params,
func = do_nouns,
}
pos_functions["proper nouns"] = {
params = noun_params,
func = function(args, data)
return do_nouns(args, data, "proper noun")
end,
}
local function base_default_verb_forms(verb)
return escape(add_suffix(verb, "s.verb")), escape(add_suffix(verb, "ing")), escape(add_suffix(verb, "d"))
end
local function default_verb_forms(verb)
local full_s_form, full_ing_form, full_ed_form = base_default_verb_forms(verb)
if verb:find(" ") then
local first, rest = verb:match("^(.-)( .*)$")
local first_s_form, first_ing_form, first_ed_form = base_default_verb_forms(first)
return full_s_form, full_ing_form, full_ed_form, first_s_form .. rest, first_ing_form .. rest,
first_ed_form .. rest, first, rest
else
return full_s_form, full_ing_form, full_ed_form, nil, nil, nil, nil, nil
end
end
local function compute_double_last_cons_stem_of_split_verb(verb, ending)
local first, rest = verb:match("^(.-)( .*)$")
if not first then
error("Kata kerja '" .. verb .. "' mesti mempunyai ruang padanya untuk menggunakan **")
end
local last_cons = first:match("([bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])$")
if not last_cons then
error("Perkataan pertama '" .. first .. "' mesti berakhir dengan konsonan untuk menggunakan **")
end
return first .. last_cons .. ending .. rest
end
local function check_non_nil_star_form(form, pagename)
if form == nil then
error("Kata kerja '" .. pagename .. "' mesti mempunyai ruang padanya untuk menggunakan *, **, *l, *! atau *'")
end
return form
end
local function sub_tilde(form, pagename)
if not form then
return nil
end
if form:find("~") then
form = form:gsub("~", replacement_escape(pagename))
end
return form
end
local deprecated_qual_replaced_by_inline_modifier = {
list = true, allow_holes = true, replaced_by = false,
instead = "gunakan pengubah suai dalam baris <q:...> atau <l:...> pada nilai"
}
pos_functions["verbs"] = {
params = {
[1] = {list = "pres_3sg", disallow_holes = true},
["pres_3sg\1_qual"] = deprecated_qual_replaced_by_inline_modifier,
[2] = {list = "pres_ptc", disallow_holes = true},
["pres_ptc\1_qual"] = deprecated_qual_replaced_by_inline_modifier,
[3] = {list = "past", disallow_holes = true},
["past\1_qual"] = deprecated_qual_replaced_by_inline_modifier,
[4] = {list = "past_ptc", allow_holes = true},
["past_ptc\1_qual"] = deprecated_qual_replaced_by_inline_modifier,
["noautolinkverb"] = boolean_param,
["angle_bracket"] = boolean_param,
},
func = function(args, data)
-- Dapatkan parameter
local par1s
local par2s = parse_inflection(args, {2, "pres_ptc"})
local par3s = parse_inflection(args, {3, "past"})
local par4s = parse_inflection(args, {4, "past_ptc"})
local pres_3sgs, pres_ptcs, pasts, past_ptcs
local pagename = data.pagename
------------------------------------------- FUNGSI UTILITI #2 ------------------------------------------
-- Fungsi-fungsi ini digunakan dalam kedua-dua format parameter berasingan dan dalam parameter pengesampingan seperti past_ptc2=.
local full_default_s, full_default_ing, full_default_ed, split_default_s, split_default_ing, split_default_ed
local lemma
local function set_lemma_and_default_forms(the_lemma)
lemma = the_lemma
full_default_s, full_default_ing, full_default_ed, split_default_s, split_default_ing, split_default_ed,
lemma_first, lemma_rest = default_verb_forms(the_lemma)
end
local function canonicalize_s_form(form)
if form == "+" then
error("Internal error: Should not see '+' here")
elseif form == "^" then
return full_default_s
elseif form == "*" then
return check_non_nil_star_form(split_default_s, lemma)
elseif form == "++" then
return compute_plusplus_s_form(lemma, full_default_s)
elseif form == "**" then
if lemma:find("^[^ ]*[szx] ") then
return compute_double_last_cons_stem_of_split_verb(lemma, "es")
else
return check_non_nil_star_form(split_default_s, lemma)
end
elseif form == "+!" then
return lemma .. "s"
elseif form == "*!" then
return check_non_nil_star_form(lemma_first) .. "s" .. lemma_rest
elseif form == "+'" then
return lemma .. "'s"
elseif form == "*'" then
return check_non_nil_star_form(lemma_first) .. "'s" .. lemma_rest
elseif form == "+l" then
if lemma:find("[szx]$") then
return {{term = full_default_s, l = {"AS"}},
{term = compute_plusplus_s_form(lemma, full_default_s), l = {"UK"}}}
else
return compute_plusplus_s_form(lemma, full_default_s)
end
elseif form == "*l" then
if lemma:find("^[^ ]*[szx] ") then
return {{term = check_non_nil_star_form(split_default_s, lemma), l = {"AS"}},
{term = compute_double_last_cons_stem_of_split_verb(lemma, "es"), l = {"UK"}}}
else
return check_non_nil_star_form(split_default_s, lemma)
end
else
return sub_tilde(form, lemma)
end
end
local function canonicalize_ing_form(form)
if form == "+" then
error("Internal error: Should not see '+' here")
elseif form == "^" then
return full_default_ing
elseif form == "*" then
return check_non_nil_star_form(split_default_ing, lemma)
elseif form == "++" then
return compute_double_last_cons_stem(lemma) .. "ing"
elseif form == "**" then
return compute_double_last_cons_stem_of_split_verb(lemma, "ing")
elseif form == "+!" then
return lemma .. "ing"
elseif form == "*!" then
return check_non_nil_star_form(lemma_first) .. "ing" .. lemma_rest
elseif form == "+'" then
return lemma .. "'ing"
elseif form == "*'" then
return check_non_nil_star_form(lemma_first) .. "'ing" .. lemma_rest
elseif form == "+l" then
return {{term = full_default_ing, l = {"AS"}},
{term = compute_double_last_cons_stem(lemma) .. "ing", l = {"UK"}}}
elseif form == "*l" then
return {{term = check_non_nil_star_form(split_default_ing, lemma), l = {"AS"}},
{term = compute_double_last_cons_stem_of_split_verb(lemma, "ing"), l = {"UK"}}}
else
return sub_tilde(form, lemma)
end
end
local function canonicalize_ed_form(form)
if form == "+" then
error("Internal error: Should not see '+' here")
elseif form == "^" then
return full_default_ed
elseif form == "*" then
return check_non_nil_star_form(split_default_ed, lemma)
elseif form == "++" then
return compute_double_last_cons_stem(lemma) .. "ed"
elseif form == "+!" then
return lemma .. "ed"
elseif form == "*!" then
return check_non_nil_star_form(lemma_first) .. "ed" .. lemma_rest
elseif form == "+'" then
return {{term = lemma .. "'d"}, {term = lemma .. "'ed"}}
elseif form == "*'" then
return {{term = check_non_nil_star_form(lemma_first) .. "'d" .. lemma_rest},
{term = check_non_nil_star_form(lemma_first) .. "'ed" .. lemma_rest}}
elseif form == "**" then
return compute_double_last_cons_stem_of_split_verb(lemma, "ed")
elseif form == "+l" then
return {{term = full_default_ed, l = {"AS"}},
{term = compute_double_last_cons_stem(lemma) .. "ed", l = {"UK"}}}
elseif form == "*l" then
return {{term = check_non_nil_star_form(split_default_ed, lemma), l = {"AS"}},
{term = compute_double_last_cons_stem_of_split_verb(lemma, "ed"), l = {"UK"}}}
else
return sub_tilde(form, lemma)
end
end
-- BAIKI: pilihan seharusnya "+", "*", "++", "**", "+n", "*n", "++n" dan "**n", tetapi bukan "n"
local function canonicalize_en_form(form)
if form == "n" then
track("n4")
return add_suffix(lemma, "n")
end
return canonicalize_ed_form(form)
end
--------------------------------- KOD PENGHURAIAN/PENTASRIFAN UTAMA --------------------------------
local is_angle_bracket = args.angle_bracket
if is_angle_bracket then
if par2s[1] or par3s[1] or par4s[1] then
error("Can't specify explicit values for 2=, 3= or 4= along with the angle-bracket format")
end
elseif is_angle_bracket == nil and not par2s[1] and not par3s[1] and not par4s[1] and not args[1][2] and
args[1][1] and args[1][1]:find("<") then
if put.term_contains_top_level_html(args[1][1]) then
-- Selalunya, term_contains_top_level_html() mengembalikan 'true' pada format kurungan sudut, yang akan
-- menjadikan pcall() di bawah berjaya tetapi membiarkan kurungan sudut seperti sedia ada. Periksa untuk ini dan hanya lakukan
-- pcall() jika term_contains_top_level_html() mengembalikan false.
is_angle_bracket = true
else
-- Jika terdapat kekaburan sama ada ia adalah format kurungan sudut atau parameter berasingan dengan pengubah suai dalam baris,
-- cuba untuk menghuraikannya sebagai parameter berasingan. Jika ralat berlaku, anggap ia sebagai format kurungan sudut.
local ok
ok, par1s = pcall(parse_inflection, args, {1, "pres_3sg"})
if not ok then
par1s = nil
is_angle_bracket = true
end
end
end
if is_angle_bracket then
-------------------------- FORMAT KURUNGAN SUDUT --------------------------
-- (0) Kembangkan perkataan berbilang suku kata dengan kurungan sudut tepat pada perkataan pertama.
local arg11 = args[1][1]
if arg11:find("^<.*>$") and pagename:find(" ") then
local first, rest = pagename:match("^(.-)( .*)$")
arg11 = first .. arg11 .. rest
end
-- (1) Menghurai spesifikasi penunjuk di dalam kurungan sudut.
local function parse_indicator_spec(angle_bracket_spec)
local inside = angle_bracket_spec:match("^<(.*)>$")
assert(inside)
local segments = put.parse_balanced_segment_run(inside, "[", "]")
local comma_separated_groups = put.split_alternating_runs(segments, ",")
if #comma_separated_groups > 4 then
error("Terlalu banyak bahagian yang dipisahkan dengan koma dalam spesifikasi penunjuk, dijangkakan paling banyak 4: " ..
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
error("Teks tidak berkaitan selepas nota kaki berkurungan: '" .. concat(separated_group) .. "'")
end
if not footnotes then
footnotes = {}
end
insert(footnotes, separated_group[j])
end
return footnotes
end
local function fetch_specs(comma_separated_group)
if not comma_separated_group then
return {{term = "+"}}
end
local specs = {}
local colon_separated_groups = put.split_alternating_runs(comma_separated_group, ":")
for _, colon_separated_group in ipairs(colon_separated_groups) do
local form = colon_separated_group[1]
if form == "*" or form == "**" or form == "*l" or form == "*!" or form == "*'" then
error("*, **, *l, *! dan *' tidak dibenarkan di dalam spesifikasi penunjuk: " .. angle_bracket_spec)
end
if form == "" then
form = "+"
end
local termobj = {
term = form
}
local footnotes = fetch_footnotes(colon_separated_group)
if footnotes then
for _, footnote in ipairs(footnotes) do
m_headword_utilities.add_footnote_to_termobj(termobj, footnote)
end
end
insert(specs, termobj)
end
return specs
end
local s_specs = fetch_specs(comma_separated_groups[1])
local ing_specs = fetch_specs(comma_separated_groups[2])
local ed_specs = fetch_specs(comma_separated_groups[3])
local en_specs = fetch_specs(comma_separated_groups[4])
return {
forms = {},
s_specs = s_specs,
ing_specs = ing_specs,
ed_specs = ed_specs,
en_specs = en_specs,
}
end
local parse_props = {
parse_indicator_spec = parse_indicator_spec,
}
local alternant_multiword_spec = iut.parse_inflected_text(arg11, parse_props)
-- (2) Periksa kurungan yang ditentukan pengguna; buang sebarang pautan dari kata dasar, tetapi ingat bentuk asalnya
-- supaya kita boleh menggunakannya di bawah dalam bentuk 'lemma_linked'.
-- Semak untuk melihat jika terdapat kurungan dalam pra-teks atau pasca-teks. Jika ada, gunakan kata dasar berpaut (dengan
-- kata kerja dipautkan secara automatik kecuali noautolinkverb diberikan). Jika tidak, gunakan algoritma kata kepala lalai.
local function check_bracket(val)
if val:find("%[%[") then
alternant_multiword_spec.saw_bracket = true
end
end
for _, alternant_or_word_spec in ipairs(alternant_multiword_spec.alternant_or_word_specs) do
check_bracket(alternant_or_word_spec.before_text)
if alternant_or_word_spec.alternants then
for _, multiword_spec in ipairs(alternant_or_word_spec.alternants) do
for _, word_spec in ipairs(multiword_spec.word_specs) do
check_bracket(word_spec.before_text)
end
check_bracket(multiword_spec.post_text)
end
end
end
check_bracket(alternant_multiword_spec.post_text)
iut.map_word_specs(alternant_multiword_spec, function(base)
if base.lemma == "" then
base.lemma = pagename
end
base.orig_lemma = base.lemma
base.lemma = remove_links(base.lemma)
if args.noautolinkverb or base.orig_lemma:find("%[%[") then
base.linked_lemma = base.orig_lemma
else
base.linked_lemma = "[[" .. base.orig_lemma .. "]]"
end
end)
-- (3) Mentasrif kata kerja mengikut spesifikasi penunjuk yang dihurai di atas.
local all_verb_slots = {
lemma = "kata dasar",
lemma_linked = "infinitive",
s_form = "3|s|pres",
ing_form = "pres|ptcp",
ed_form = "past",
en_form = "past|ptcp",
}
local function conjugate_verb(base)
local function process_specs(slot, specs, canon_func, default_values, default_already_formobj)
local function insert_termobj_into_slot(termobj)
local formobj = m_headword_utilities.convert_termobj_to_formobj(termobj)
-- Jika bentuk itu ialah -, jangan masukkan apa-apa bentuk, ini akan mengakibatkan tiada bentuk keseluruhan
-- (malah ia akan menjadi nil). Kita akan semak hal itu di bawah dan akan menggantikan tanda "-" tunggal sebagai
-- bentuk yang membolehkannya ditukar menjadi label istimewa seperti "tiada partisipel kini".
if formobj.form == "-" then
if formobj.footnotes then
error("Tidak dapat mengekalkan nota kaki yang dinyatakan pada bentuk yang hilang '-': BAIKI: " ..
dump(formobj.footnotes))
end
else
iut.insert_form(base.forms, slot, formobj)
end
end
local function canonicalize_and_insert(arg)
local canon_arg = canon_func(arg)
if type(canon_arg) == "string" then
arg.term = canon_arg
insert_termobj_into_slot(arg)
else
for _, canon in ipairs(canon_arg) do
m_headword_utilities.combine_termobj_decorations(canon, arg)
insert_termobj_into_slot(canon)
end
end
end
for _, arg in ipairs(specs) do
if arg.term == "+" then
if default_values then -- akan menjadi nil jika masa lepas dinyatakan sebagai - dan tiada past ptc diberikan
for _, val in ipairs(default_values) do
val = shallowCopy(val)
if default_already_formobj then
local argformobj = m_headword_utilities.convert_termobj_to_formobj(arg)
val.footnotes = iut.combine_footnotes(val.footnotes, argformobj.footnotes)
iut.insert_form(base.forms, slot, val)
else
m_headword_utilities.combine_termobj_decorations(val, arg)
canonicalize_and_insert(val)
end
end
end
else
canonicalize_and_insert(arg)
end
end
end
set_lemma_and_default_forms(base.lemma)
local all_part_default_specs = {}
local function process_and_canonicalize_s_form(arg)
local form = arg.term
if form == "+" then
error("Ralat dalaman: '+' sepatutnya ditukar menjadi '^' sekarang")
end
if form == "*" or form == "**" or form == "*l" or form == "*!" or form == "*'" then
error(("Ralat dalaman: '%s' sepatutnya telah melemparkan ralat"):format(form))
end
if form == "^" or form == "++" or form == "+l" or form == "+!" or form == "+'" then
insert(all_part_default_specs, shallowCopy(arg))
end
return canonicalize_s_form(form)
end
process_specs("s_form", base.s_specs, process_and_canonicalize_s_form, {{term = "^"}})
if not all_part_default_specs[1] then
all_part_default_specs[1] = {term = "^"}
end
process_specs("ing_form", base.ing_specs, function(arg) return canonicalize_ing_form(arg.term) end,
all_part_default_specs)
process_specs("ed_form", base.ed_specs, function(arg) return canonicalize_ed_form(arg.term) end,
all_part_default_specs)
process_specs("en_form", base.en_specs, function(arg) return canonicalize_en_form(arg.term) end,
base.forms.ed_form, "default already formobj")
iut.insert_form(base.forms, "lemma", {form = base.lemma})
-- Tambah versi pautan bagi lemma untuk kegunaan dalam head=. Kita menulis ini dalam gaya yang umum sekiranya
-- ada berbilang bentuk lemma (yang ketika ini tidak mungkin pada tahap ini, walaupun ia
-- mungkin dilakukan secara menyeluruh menggunakan tatatanda ((...,...))).
iut.insert_forms(base.forms, "lemma_linked", iut.map_forms(base.forms.lemma, function(form)
if form == base.lemma and base.linked_lemma:find("%[%[") then
return base.linked_lemma
else
return form
end
end))
end
local inflect_props = {
slot_table = all_verb_slots,
inflect_word_spec = conjugate_verb,
}
iut.inflect_multiword_or_alternant_multiword_spec(alternant_multiword_spec, inflect_props)
-- (4) Dapatkan borang-borang dan letakkan kata dasar yang ditasrif dalam data.heads jika tidak diberikan secara tersurat.
local function fetch_termobjs(slot)
local forms = alternant_multiword_spec.forms[slot]
-- Lihat di atas. Ini sepatutnya hanya berlaku jika pengguna menggunakan '-' untuk sesuatu spesifikasi.
if not forms or not forms[1] then
return {{term = "-"}}
end
local termobjs = {}
for _, formobj in ipairs(forms) do
insert(termobjs, m_headword_utilities.convert_formobj_to_termobj(formobj))
end
return termobjs
end
pres_3sgs = fetch_termobjs("s_form")
pres_ptcs = fetch_termobjs("ing_form")
pasts = fetch_termobjs("ed_form")
past_ptcs = fetch_termobjs("en_form")
-- Guna bentuk kata dasar "berpaut" sebagai pengepala jika tiada head= diberikan secara nyata dan pengguna menentukan
-- kurungan dalam salah satu kata dasar. Jika tidak kita akan gunakan algoritma pemautan kata kepala lalai.
if not data.user_specified_heads[1] and alternant_multiword_spec.saw_bracket then
data.heads = {}
for _, lemma_obj in ipairs(alternant_multiword_spec.forms.lemma_linked) do
insert(data.heads, m_headword_utilities.convert_formobj_to_termobj(lemma_obj))
end
end
else
-------------------------- FORMAT PARAMETER BERASINGAN --------------------------
set_lemma_and_default_forms(pagename)
par1s = par1s or parse_inflection(args, {1, "pres_3sg"})
pres_3sgs = {}
pres_ptcs = {}
pasts = {}
past_ptcs = {}
if not par1s[1] then
par1s = {{term = "+"}}
end
if not par2s[1] then
par2s = {{term = "+"}}
end
if not par3s[1] then
par3s = {{term = "+"}}
end
if not par4s[1] then
par4s = {{term = "+"}}
end
local function process_argument(args, dest, canon_func, default_values, default_already_canonicalized)
local function canonicalize_and_insert(arg)
local canon_arg = canon_func(arg)
if type(canon_arg) == "string" then
arg.term = canon_arg
m_headword_utilities.insert_termobj_combining_duplicates(dest, arg)
else
for _, canon in ipairs(canon_arg) do
m_headword_utilities.combine_termobj_decorations(canon, arg)
m_headword_utilities.insert_termobj_combining_duplicates(dest, canon)
end
end
end
for _, arg in ipairs(args) do
if arg.term == "+" then
for _, val in ipairs(default_values) do
val = shallowCopy(val)
m_headword_utilities.combine_termobj_decorations(val, arg)
if default_already_canonicalized then
m_headword_utilities.insert_termobj_combining_duplicates(dest, val)
else
canonicalize_and_insert(val)
end
end
else
canonicalize_and_insert(arg)
end
end
end
local all_part_default_specs = {}
local function process_and_canonicalize_s_form(arg)
local form = arg.term
if form == "+" then
error("Ralat dalaman: '+' sepatutnya ditukar menjadi '^' sekarang")
end
if form == "^" or form == "++" or form == "+l" or form == "+!" or form == "+'" or
form == "*" or form == "**" or form == "*l" or form == "*!" or form == "*'" then
insert(all_part_default_specs, shallowCopy(arg))
end
return canonicalize_s_form(form)
end
process_argument(par1s, pres_3sgs, process_and_canonicalize_s_form, {{term = "^"}})
if not all_part_default_specs[1] then
all_part_default_specs[1] = {term = "^"}
end
process_argument(par2s, pres_ptcs, function(arg) return canonicalize_ing_form(arg.term) end,
all_part_default_specs)
process_argument(par3s, pasts, function(arg) return canonicalize_ed_form(arg.term) end,
all_part_default_specs)
process_argument(par4s, past_ptcs, function(arg) return canonicalize_en_form(arg.term) end,
pasts, "default already canonicalized")
end
------------------------------------------- MASUKKAN INFLEKSI ------------------------------------------
insert_inflection(data, pres_3sgs, "orang ketiga tunggal ringkas kini", "s-verb-form")
insert_inflection(data, pres_ptcs, "partisipel kini", "ing-form")
if deepEquals(pasts, past_ptcs) then
insert_inflection(data, pasts, "lepas ringkas dan partisipel lepas", "ed-form",
"tiada kata lepas ringkas mahupun partisipel lepas")
else
insert_inflection(data, pasts, "lepas ringkas", "spast")
insert_inflection(data, past_ptcs, "partisipel lepas", "past|part")
end
if pagename:find(" ") then
-- Semak ruang letak "it"
local words = split(pagename, " ")
for _, word in ipairs(words) do
if word == "it" or word == "its" or word == "it's" then
insert(data.categories, "Kategori: Istilah " .. langname .. ' dengan ruang letak "it"')
break
end
end
-- Semak kata kerja frasa
local phrasal_adverbs = list_to_set{
-- NOTA: Ini hanya perlu mengandungi kata keterangan frasa yang lazim, bukan kata rawak seperti [[low]],
-- [[adrift]], dan lain-lain.
"aback",
"about",
"above",
"across",
"after",
"against",
"ahead",
"along",
"apart",
"around",
"as",
"aside",
"at",
"away",
"back",
"before",
"behind",
"below",
"between",
"beyond",
"by",
"down",
"for",
"forth",
"from",
"in",
"into",
"of",
"off",
"on",
"onto",
"out",
"over",
"past",
"round",
"through",
"to",
"together",
"towards",
"under",
"up",
"upon",
"with",
"without",
}
local allowed_non_adverb_words = list_to_set{
"it",
"one",
"oneself",
"someone",
}
local base = pagename
local seen_adverbs = {}
-- Hanya anggap kata kerja sebagai kata kerja frasa jika ia terdiri daripada satu kata kerja dasar yang diikuti hanya oleh sama ada
-- kata keterangan daripada `phrasal_adverbs` atau perkataan ruang letak daripada `allowed_non_adverb_words`, dengan
-- sekurang-kurangnya satu perkataan mengikut daripada `phrasal_adverbs` (oleh itu [[can it]] bukan kata kerja frasa).
while true do
local prev, word = base:match("^(.+) (.-)$")
if not prev then
break
end
if phrasal_adverbs[word] then
insert(seen_adverbs, word)
elseif allowed_non_adverb_words[word] then
-- tidak berbuat apa-apa
else
break
end
base = prev
end
if not base:find(" ") and seen_adverbs[1] then
insert(data.categories, "Kategori: Kata kerja frasa " .. langname)
for i = #seen_adverbs, 1, -1 do
insert(data.categories, 'Kategori: Kata kerja frasa ' .. langname .. ' yang dibentuk dengan "' .. seen_adverbs[i] ..
'"')
end
end
end
end,
}
-----------------------------------------------------------------------------------------
-- Bentuk imbuhan akhiran --
-----------------------------------------------------------------------------------------
pos_functions["suffix forms"] = {
params = {
[1] = {required = true, list = true, disallow_holes = true},
},
func = function(args, data, is_suffix)
local suffix_type = {}
for _, typ in ipairs(args[1]) do
insert(suffix_type, "akhiran membentuk " .. typ)
end
insert(data.inflections, {label = "bentuk bukan lema daripada " .. m_table.serialCommaJoin(suffix_type, {conj = "or"})})
end,
}
return export
qof3fxt4ief103hnhtsmlnh27b884bl
375864
375863
2026-09-25T11:40:33Z
SNN95
2113
375864
Scribunto
text/plain
local export = {}
local pos_functions = {}
--[==[
Pengarang dari 2020 dan seterusnya: kebanyakannya Benwing2, dengan sumbangan besar daripada Theknightwho. Berdasarkan versi terdahulu oleh Rua
(kini kebanyakannya telah ditulis semula), dengan sumbangan daripada Erutuon dan lain-lain (lihat sejarah untuk atribusi penuh).
]==]
local force_cat = false -- untuk pengujian; jika benar, kategori akan muncul pada halaman ruang nama utama
local require = require
local require_when_needed = require("Module:require when needed")
local en_utilities_module = "Module:en-utilities"
local headword_utilities_module = "Module:headword utilities"
local headword_module = "Module:headword"
local inflection_utilities_module = "Module:inflection utilities"
local parse_utilities_module = "Module:parse utilities"
local JSON_module = "Module:JSON"
local labels_module = "Module:labels"
local links_module = "Module:links"
local parameters_module = "Module:parameters"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local utilities_module = "Module:utilities"
local yesno_module = "Module:yesno"
local iut = require_when_needed(inflection_utilities_module)
local put = require_when_needed(parse_utilities_module)
local m_headword_utilities = require_when_needed(headword_utilities_module)
local add_links_to_multiword_term = require_when_needed(headword_utilities_module, "add_links_to_multiword_term")
local add_suffix = require_when_needed(en_utilities_module, "add_suffix")
local apply_link_modifiers = require_when_needed(headword_utilities_module, "apply_link_modifiers")
local concat = table.concat
local deepEquals = require_when_needed(table_module, "deepEquals")
local dump = mw.dumpObject
local format_categories = require_when_needed(utilities_module, "format_categories")
local full_headword = require_when_needed(headword_module, "full_headword")
local get_label_info = require_when_needed(labels_module, "get_label_info")
local get_link_page = require_when_needed(links_module, "get_link_page")
local glossary_link = require_when_needed(headword_utilities_module, "glossary_link")
local insert = table.insert
local insertIfNot = require_when_needed(table_module, "insertIfNot")
local ipairs = ipairs
local is_regular_plural = require_when_needed(en_utilities_module, "is_regular_plural")
local list_to_set = require_when_needed(table_module, "listToSet")
local pairs = pairs
local process_params = require_when_needed(parameters_module, "process")
local remove = table.remove
local remove_links = require_when_needed(links_module, "remove_links")
local replacement_escape = require_when_needed(string_utilities_module, "replacement_escape")
local shallowCopy = require_when_needed(table_module, "shallowCopy")
local singularize = require_when_needed(en_utilities_module, "singularize")
local split = require_when_needed(string_utilities_module, "split")
local toJSON = require_when_needed(JSON_module, "toJSON")
local toNFD = mw.ustring.toNFD
local type = type
local ulen = require_when_needed(string_utilities_module, "len")
local ulower = require_when_needed(string_utilities_module, "lower")
local umatch = require_when_needed(string_utilities_module, "match")
local u = require_when_needed(string_utilities_module, "char")
local ugsub = require_when_needed(string_utilities_module, "gsub")
local lang = require("Module:languages").getByCode("en")
local langname = lang:getCanonicalName()
local list_param = {list = true, disallow_holes = true}
local list_allow_holes = {list = true, allow_holes = true}
local boolean_param = {type = "boolean"}
local function ine(val)
if val == "" then return nil else return val end
end
local function track(page)
require("Module:debug/track")("en-headword/" .. page)
return true
end
------------------------------------------- FUNGSI UTILITI ------------------------------------------
-- Menghurai dan mengembalikan infleksi yang tidak memerlukan pemprosesan tambahan. Argumen mentah diambil daripada `args[field]`, yang
-- dihurai untuk pengubah suai dalam baris.
local function parse_inflection(args, field, is_head)
local argfield = field
if type(argfield) == "table" then
argfield = argfield[1]
end
return m_headword_utilities.parse_term_list_with_modifiers {
paramname = field,
forms = args[argfield],
splitchar = ",",
is_head = is_head,
}
end
-- Masukkan infleksi yang dihurai dalam `terms` (seperti yang dihurai oleh `parse_inflection`) ke dalam `data.inflections`, dengan label
-- `label` dan spesifikasi pencepat pilihan `accel`.
local function insert_inflection(data, terms, label, accel, no_label)
for _, termobj in ipairs(terms) do
m_headword_utilities.remove_termobj_field_modifiers(termobj)
end
m_headword_utilities.insert_inflection {
headdata = data,
terms = terms,
label = label,
no_label = no_label,
accel = accel and {form = accel} or nil,
}
end
-- Masukkan label tetap `label` ke dalam infleksi untuk `data`. Jika `originating_term` dibekalkan, salin hiasan
-- daripadanya ke dalam label tetap.
local function insert_fixed_inflection(data, label, originating_term)
m_headword_utilities.insert_fixed_inflection {
headdata = data,
originating_term = originating_term,
label = label,
}
end
-- Menghurai dan memasukkan infleksi yang tidak memerlukan pemprosesan tambahan ke dalam `data.inflections`. Argumen mentah diambil daripada
-- `args[field]`, yang dihurai untuk pengubah suai dalam baris. `label` ialah label yang diberikan kepada infleksi;
-- `accel` ialah bentuk pencepat, atau nil.
local function parse_and_insert_inflection(data, args, field, label, accel)
m_headword_utilities.parse_and_insert_inflection {
headdata = data,
forms = args[field],
paramname = field,
splitchar = ",",
label = label,
accel = accel and {form = accel} or nil,
}
end
-- Fungsi-fungsi ini digunakan secara langsung dalam format <> dan juga dalam fungsi utiliti #2 di bawah.
local function compute_double_last_cons_stem(term)
local last_cons = term:match("([bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])$")
if not last_cons then
error("Kata dasar kata kerja '" .. term .. "' mesti berakhir dengan konsonan untuk menggunakan ++")
end
return term .. last_cons
end
local function compute_plusplus_s_form(term, default_s_form)
if term:find("[szx]$") then
-- regas -> regasses, derez -> derezzes
return compute_double_last_cons_stem(term) .. "es"
else
return default_s_form
end
end
-- Titik masuk utama.
-- Ini adalah satu-satunya fungsi yang boleh dipanggil daripada templat.
function export.show(frame)
local iparams = {
[1] = true,
}
local iargs = require("Module:parameters").process(frame.args, iparams)
local parargs = frame:getParent().args
local poscat = iargs[1]
local pos_in_1 = not poscat
if pos_in_1 then
poscat = ine(parargs[1]) or
mw.title.getCurrentTitle().fullText == "Template:en-head" and "interjection" or
error("Golongan kata mesti dinyatakan pada 1=")
poscat = require(headword_module).canonicalize_pos(poscat)
end
local indexing_poscat = pos_in_1 and "head" or poscat
local params = {
["head"] = list_param,
["id"] = true,
["json"] = boolean_param,
["sort"] = true,
["splithyph"] = boolean_param,
["nosplithyph"] = boolean_param,
["hyphspace"] = boolean_param,
["nolink"] = boolean_param,
["nolinkhead"] = {type = "boolean_param", alias_of = "nolink"},
["suffix"] = boolean_param,
["nosuffix"] = boolean_param,
["nomultiwordcat"] = boolean_param,
["abbr"] = list_param,
["the"] = true,
["def"] = {alias_of = "the"},
["pagename"] = true, -- untuk pengujian
}
if pos_in_1 then
params[1] = {required = true} -- diperlukan tetapi diabaikan kerana sudah diproses di atas
end
local pos_data = pos_functions[indexing_poscat]
local pos_func
if pos_data then
local pos_params = pos_data.params
if pos_params then
for key, val in pairs(pos_params) do
params[key] = val
end
end
pos_func = pos_data.func
end
local args = process_params(parargs, params)
-- Ambil kira tajuk yang tidak disokong, cth. 'C|N>K' bukannya 'Unsupported titles/C through N to K'.
local pagename = args.pagename or mw.loadData("Module:headword/data").pagename
local user_specified_heads = parse_inflection(args, "head", "is_head")
local heads = user_specified_heads
local autohead
if args.nolink or not pagename:find("[ '%-]") then
autohead = pagename
else
local en_no_split_apostrophe_words = list_to_set {
"one's",
"someone's",
"he's",
"she's",
"it's",
}
local en_include_hyphen_prefixes = list_to_set {
-- Kita tidak menyertakan perkara yang juga merupakan perkataan walaupun ia selalunya (mungkin kebanyakannya) merupakan imbuhan awalan, cth.
-- "be", "counter", "cross", "extra", "half", "mid", "over", "pan", "under".
"acro",
"acousto",
"Afro",
"agro",
"anarcho",
"angio",
"Anglo",
"ante",
"anti",
"arch",
"auto",
"bi",
"bio",
"cis",
"co",
"cryo",
"crypto",
"de",
"demi",
"eco",
"electro",
"Euro",
"ex",
"Greco",
"hemi",
"hydro",
"hyper",
"hypo",
"infra",
"Indo",
"inter",
"intra",
"Judeo",
"macro",
"meta",
"micro",
"mini",
"multi",
"neo",
"neuro",
"non",
"para",
"peri",
"post",
"pre",
"pro",
"proto",
"pseudo",
"re",
"semi",
"sub",
"super",
"trans",
"un",
"vice",
}
local function is_english(term)
local title = mw.title.new(term)
if title and title.exists then
local content = title:getContent()
if content and content:find("==English==\n") then
return true
end
end
return false
end
local function en_split_hyphen_when_space(word)
if not word:find("-", nil, true) then
return nil
end
if args.hyphspace then
return "[[" .. word:gsub("%-+", " ") .. "|" .. word .. "]]"
end
if args.nosplithyph then
return "[[" .. word .. "]]"
end
if not args.splithyph then
local space_word = word:gsub("%-+", " ")
if is_english(space_word) then
return "[[" .. space_word .. "|" .. word .. "]]"
end
if is_english(word) then
return "[[" .. word .. "]]"
end
end
return nil
end
local function en_split_apostrophe(word)
local base = word:match("^(.*)'s$")
if base then
return "[[" .. base .. "]][[-'s|'s]]"
end
-- Hanya anggap koma atas akhir sebagai kepunyaan jika didahului oleh sesuatu yang kelihatan seperti jamak yang berakhir dengan /z/.
-- Khususnya kita tidak mahu melakukannya untuk perkataan seperti [[truckin']].
base = word:match("^(.*[sxz])'$")
if base then
if base:find("s$") then
local sg = singularize(base)
if is_english(sg) then
return "[[" .. sg .. "|" .. base .. "]][[-'|']]"
end
end
return "[[" .. base .. "]][[-'|']]"
end
return "[[" .. word .. "]]"
end
autohead = add_links_to_multiword_term(pagename, {
split_hyphen_when_space = en_split_hyphen_when_space,
split_apostrophe = en_split_apostrophe,
no_split_apostrophe_words = en_no_split_apostrophe_words,
include_hyphen_prefixes = en_include_hyphen_prefixes,
})
end
if not heads[1] then
heads = {{term = autohead}}
else
for _, headobj in ipairs(heads) do
local head = headobj.term
if head:find("^~") then
head = apply_link_modifiers(autohead, head:sub(2), lang)
headobj.term = head
elseif head:find("^[!?]$") then
-- Jika head= yang nyata hanya mengandungi ! atau ?, tambahkannya di hujung kata dasar lalai.
headobj.term = autohead .. head
end
if head == autohead then
track("redundant-head")
end
end
end
-- mengendalikan the=/def=
if args.the == "~" then
local newheads = {}
for _, headobj in ipairs(heads) do
local barehead = shallowCopy(headobj)
insert(newheads, barehead)
headobj.term = "the " .. headobj.term
insert(newheads, headobj)
end
heads = newheads
elseif args.the then
local the = require(yesno_module)(args.the)
if the then
for _, headobj in ipairs(heads) do
headobj.term = "the " .. headobj.term
end
end
end
local data = {
lang = lang,
pos_category = poscat,
categories = {},
heads = heads,
user_specified_heads = user_specified_heads,
-- Kita menggunakan algoritma pemisahan kita sendiri supaya kategori kata dasar lewah tidak tepat.
no_redundant_head_cat = true,
inflections = {},
nomultiwordcat = args.nomultiwordcat,
sort_key = args.sort,
pagename = pagename,
id = args.id,
force_cat_output = force_cat,
}
local function inscat(cat)
insert(data.categories, "Kategori: " .. langname .. " " .. cat)
end
local is_suffix = false
if args.suffix or not args.nosuffix and pagename:find("^%-") and not pagename:find("^%-%-") and poscat ~= "suffix forms" then
is_suffix = true
data.pos_category = "suffixes"
local singular_poscat = singularize(poscat)
inscat("akhiran membentuk " .. singular_poscat)
insert(data.inflections, {label = "akhiran membentuk " .. singular_poscat})
end
if pos_func then
pos_func(args, data, is_suffix)
end
local extra_categories = {}
if pagename:find("[Qq]") then
-- Semak q yang tidak diikuti oleh u. Kita mahu mengecualikan perkara seperti [[13q deletion syndrome]] dan [[BFOQ]] yang
-- tiada huruf kecil pada kedua-dua sisi, serta perkara seperti [[& seq.]] dan [[acq.]] yang merupakan
-- singkatan untuk perkataan yang mengandungi u selepasnya.
--
-- Julat anggaran bagi diakritik gabungan; kita mahu membuangnya supaya semakan di bawah untuk
-- huruf kecil bersebelahan q tidak terpengaruh dengan diakritik pada huruf.
local u300 = u(0x0300)
local u36F = u(0x036F)
local pagename_no_diacritics = ugsub(toNFD(pagename), "[" .. u300 .. "-" .. u36F .. "]", "")
if pagename_no_diacritics:find("[Qq][a-tv-z]") or pagename_no_diacritics:find("[a-z]q[^u.]") or
pagename_no_diacritics:find("[a-z]q$") then
inscat("perkataan yang mengandungi Q yang tidak diikuti oleh U")
end
end
-- toNFD melakukan penguraian, jadi huruf yang terurai kepada vokal ASCII
-- dan diakritik, seperti é, dikira sebagai vokal dan tidak
-- perlu disertakan dalam corak.
if not umatch(ulower(toNFD(pagename)), "[aeiouyæœøəªºαεηιουω]") then
inscat("perkataan yang dieja tanpa vokal")
end
if pagename:find("yre$") then
inscat('perkataan berakhir dengan "-yre"')
end
if not pagename:find(" ") and ulen(pagename) >= 25 then
insert(extra_categories, "Perkataan " .. langname .. " yang panjang")
end
if pagename:find("^[^aeiou ]*a[^aeiou ]*e[^aeiou ]*i[^aeiou ]*o[^aeiou ]*u[^aeiou ]*$") then
inscat("perkataan yang menggunakan semua vokal dalam susunan abjad")
end
parse_and_insert_inflection(data, args, "abbr", "abbreviation")
if args.json then
return toJSON(data)
end
return full_headword(data)
.. (extra_categories[1]
and format_categories(extra_categories, lang, args.sort)
or "")
end
local function make_default_comparative(word)
if word == "good" or word == "well" then
return {"better"}
elseif word == "bad" or word == "badly" then
return {"worse"}
elseif word == "far" then
return {"further", "farther"}
else
return {add_suffix(word, "r")}
end
end
local function make_default_superlative(word)
if word == "good" or word == "well" then
return {"best"}
elseif word == "bad" or word == "badly" then
return {"worst"}
elseif word == "far" then
return {"furthest", "farthest"}
else
return {add_suffix(word, "st.superlative")}
end
end
-- Fungsi ini melakukan kerja yang sama antara kata sifat dan kata keterangan.
local function process_comparative_args(data, args, plpos)
local pagename = data.pagename
local comps = parse_inflection(args, 1)
local sups = parse_inflection(args, "sup")
local outcomps, outsups
if args.componly then
if comps[1] then
error("Can't specify comparatives of comparative-only " .. plpos)
end
insert(data.inflections, {label = "hanya " .. glossary_link("bandingan")})
insert(data.categories, "Kategori: " .. langname .. " " .. plpos .. " bandingan-sahaja")
-- Tetapkan kepada senarai kosong supaya kita tidak mendapat sebarang output bentuk bandingan, tetapi proses bentuk 최상급 jika dinyatakan.
outcomps = {}
if not sups[1] then
-- Tetapkan kepada senarai kosong supaya kita tidak mendapat sebarang output bentuk 최상급 melainkan dinyatakan dengan jelas.
outsups = {}
end
elseif args.suponly then
if comps[1] or sups[1] then
error("Can't specify comparatives or superlatives of or superlative-only " .. plpos)
end
insert(data.inflections, {label = "hanya " .. glossary_link("palingan")})
insert(data.categories, "Kategori: " .. langname .. " " .. plpos .. " palingan-sahaja")
return
end
-- Jika parameter pertama ialah ?, maka jangan paparkan apa-apa, hanya kembalikan.
if comps[1] and comps[1].term == "?" then
if comps[2] then
error("Can't specify additional comparatives along with '?'")
end
if sups[1] then
error("Can't specify superlatives along with '?' for the comparative")
end
return
end
if comps[1] and comps[1].term == "-" then
local hyphencomp = remove(comps, 1) -- Buang "-" tetapi simpan untuk hiasan.
-- Bukan (secara amnya) boleh dibanding; sekali-sekala mungkin mempunyai bentuk bandingan
if comps[1] then
insert_fixed_inflection(data, "tidak selalunya <<comparable>>", hyphencomp)
elseif not sups[1] then
insert_fixed_inflection(data, "tidak <<comparable>>", hyphencomp)
insert(data.categories, "Kategori: " .. langname .. " " .. plpos .. " tidak boleh dibandingkan")
return
else
-- Tiada bentuk bandingan, tetapi mempunyai bentuk 최상급. insert_inflection() akan menjana 'tiada bandingan' dengan betul jika kita
-- hantar "-" sebagai nilai.
outcomps = {hyphencomp}
end
elseif not comps[1] then
comps = {{term = "more"}}
end
if not outcomps then -- bukan jika kita menetapkan `outcomps` kepada "-" di atas atau memproses kata yang hanya bentuk bandingan
outcomps = {}
-- Periksa setiap parameter yang diberikan dan bina bentuk bandingan dan palingan.
for _, compobj in ipairs(comps) do
local comp = compobj.term
if comp == "-" then
error("Comparative of '-' only allowed as first comparative")
end
if comp == "+" then
comp = "+more"
elseif comp == "more" and pagename ~= "many" and pagename ~= "much" then
comp = "+more"
elseif comp == "further" and pagename ~= "far" then
comp = "+further"
elseif comp == "better" and pagename ~= "good" and pagename ~= "well" then
comp = "+better"
elseif comp:find("~") then
comp = comp:gsub("~", replacement_escape(pagename))
end
compobj.origterm = comp
if comp == "+more" then
comp = "more [[" .. pagename .. "]]"
elseif comp == "+further" then
comp = {"further [[" .. pagename .. "]]", "farther [[" .. pagename .. "]]"}
elseif comp == "+better" then
comp = "better [[" .. pagename .. "]]"
elseif comp == "er" then
-- Tambah -er.
comp = add_suffix(pagename, "r")
elseif comp == "ier" then
if pagename:sub(-1) ~= "y" then
error("Can't specify 'ier' comparative unless the term ends with 'y': " .. pagename)
end
comp = pagename:gsub("e?y$", "ier")
elseif comp:find("^%+") then
local special = m_headword_utilities.get_special_indicator(comp, "noerror")
if special then
comp = m_headword_utilities.handle_multiword(pagename, special, make_default_comparative)
end
end
if type(comp) == "table" and not comp[2] then
comp = comp[1]
end
if type(comp) == "table" then
for i = 1, #comp - 1 do
local outobj = shallowCopy(compobj)
outobj.term = comp[i]
insert(outcomps, outobj)
end
compobj.term = comp[#comp]
insert(outcomps, compobj)
else
compobj.term = comp
insert(outcomps, compobj)
end
end
end
if sups[1] and sups[1].term == "-" then
if sups[2] then
error("Can't specify '-' as superlative followed by further values")
end
-- Tiada bentuk palingan. insert_inflection() akan menjana 'tiada palingan' dengan betul jika kita hantar "-" sebagai nilai.
outsups = sups
else
if not sups[1] then
sups = {{term = "+"}}
end
end
-- `outsups` akan ditetapkan jika kita menetapkan `outsups` kepada "-" di atas atau memproses kata yang hanya bentuk bandingan tanpa bentuk palingan.
if not outsups then
outsups = {}
local function process_sup(sup, special, supobj, compobj)
if special then
sup = m_headword_utilities.handle_multiword(pagename, special, make_default_superlative)
elseif sup == "-" or sup == "+" then
error(("Internal error: Superlative value of '%s' should have been handled earlier"):format(sup))
elseif sup == "+most" then
sup = "most [[" .. pagename .. "]]"
elseif sup == "+furthest" then
sup = {"furthest [[" .. pagename .. "]]", "farthest [[" .. pagename .. "]]"}
elseif sup == "+best" then
sup = "best [[" .. pagename .. "]]"
elseif sup == "est" then
-- Tambah -est.
sup = add_suffix(pagename, "st.superlative")
elseif sup == "iest" then
if pagename:sub(-1) ~= "y" then
error("Can't specify 'iest' superlative unless the term ends with 'y': " .. pagename)
end
sup = pagename:gsub("e?y$", "iest")
end
if type(sup) == "table" and not sup[2] then
sup = sup[1]
end
if compobj then
supobj = shallowCopy(supobj)
supobj = m_headword_utilities.combine_termobj_decorations(supobj, compobj)
end
if type(sup) == "table" then
for i = 1, #sup - 1 do
local outobj = shallowCopy(supobj)
outobj.term = sup[i]
insert(outsups, outobj)
end
supobj.term = sup[#sup]
insert(outsups, supobj)
else
supobj.term = sup
insert(outsups, supobj)
end
end
for _, supobj in ipairs(sups) do
local sup = supobj.term
if sup == "-" then
error("Superlative of '-' only allowed as first superlative")
end
if sup == "+" then
if not comps[1] then
error("Superlative of '+' can't be specified when there are no comparatives")
end
for _, compobj in ipairs(comps) do
local comp = compobj.origterm
local special
if comp == "+more" then
sup = "+most"
elseif comp == "+further" then
sup = "+furthest"
elseif comp == "+better" then
sup = "+best"
elseif comp == "er" then
sup = "est"
elseif comp == "ier" then
sup = "iest"
else
if comp:find("^%+") then
special = m_headword_utilities.get_special_indicator(comp, "noerror")
end
if not special then
-- Jika bentuk bandingan penuh diberikan, kemudian jana bentuk palingan dengan menukar -er kepada
-- -est.
if comp:sub(-2) == "er" then
sup = comp:sub(1, -3) .. "est"
else
error(("The superlative cannot be derived automatically from comparative '%s' because it doesn't end in -er"):format(comp))
end
end
end
process_sup(sup, special, supobj, compobj)
end
else
local special = m_headword_utilities.get_special_indicator(sup, "noerror")
-- Lakukan sebahagian kerja di sini dan bukannya dalam process_sup() supaya kita tidak berakhir dengan memproses dua kali kata dengan '~'
-- di dalamnya atau kata yang secara kebetulan 'most' atau serupa selepas menggantikan ~ dalam bentuk bandingan.
if not special then
if sup == "most" and pagename ~= "many" and pagename ~= "much" then
sup = "+most"
elseif sup == "furthest" and pagename ~= "far" then
sup = "+furthest"
elseif sup == "best" and pagename ~= "good" and pagename ~= "well" then
sup = "+best"
elseif sup:find("~") then
sup = sup:gsub("~", replacement_escape(pagename))
end
end
process_sup(sup, special, supobj)
end
end
end
insert_inflection(data, outcomps, "<<comparative>>", "comparative")
insert_inflection(data, outsups, "<<superlative>>", "superlative")
end
pos_functions["adjectives"] = {
params = {
[1] = list_param,
["comp_qual"] = {list = "comp\1_qual", allow_holes = true, replaced_by = false,
instead = "gunakan pengubah suai dalam baris <l:...> atau <q:...> pada nilai bandingan",
},
["sup"] = list_param,
["sup_qual"] = {list = "sup\1_qual", allow_holes = true, replaced_by = false,
instead = "gunakan pengubah suai dalam baris <l:...> atau <q:...> pada nilai palingan",
},
["componly"] = boolean_param,
["suponly"] = boolean_param,
},
func = function(args, data)
-- Proses bentuk bandingan dan palingan.
process_comparative_args(data, args, "kata sifat")
end,
}
pos_functions["kata sifat"] = {
params = {
[1] = list_param,
["comp_qual"] = {list = "comp\1_qual", allow_holes = true, replaced_by = false,
instead = "gunakan pengubah suai dalam baris <l:...> atau <q:...> pada nilai bandingan",
},
["sup"] = list_param,
["sup_qual"] = {list = "sup\1_qual", allow_holes = true, replaced_by = false,
instead = "gunakan pengubah suai dalam baris <l:...> atau <q:...> pada nilai palingan",
},
["componly"] = boolean_param,
["suponly"] = boolean_param,
},
func = function(args, data)
-- Proses bentuk bandingan dan palingan.
process_comparative_args(data, args, "kata keterangan")
end,
}
local function escape(str)
return (str:gsub("\\([:#])", "\\\\%1")
:gsub("[:#]", "\\%0"))
end
local function canonicalize_plural(pl, pagename, pos)
if pl == "+" then
return escape(add_suffix(pagename, "s.plural", pos))
elseif pl == "++" then
return escape(compute_plusplus_s_form(pagename, add_suffix(pagename, "s.plural", pos)))
elseif pl == "*" then
return escape(pagename)
elseif pl == "ies" then
if pagename:sub(-1) == "y" then
return escape(pagename:gsub("e?y$", pl))
end
error("Can't specify 'ies' plural unless the term ends with 'y'.")
elseif pl == "s" or pl == "es" or pl == "'s" then
return escape(pagename .. pl)
end
end
local function do_nouns(args, data, pos)
local pagename = data.pagename
pos = pos or "noun"
local plurals = parse_inflection(args, 1)
local function insert_plurale_tantum_inflections(is_plural_only, originating_label)
if args.sg[1] then
insert_fixed_inflection(data, "lazimnya jamak", originating_label)
parse_and_insert_inflection(data, args, "sg", "tunggal")
elseif is_plural_only then
insert_fixed_inflection(data, "jamak sahaja", originating_label)
end
if args.attr[1] then
parse_and_insert_inflection(data, args, "attr", "atributif")
end
end
local function first_pl_term()
return plurals[1] and plurals[1].term or nil
end
if first_pl_term() == "p" then
-- plurale tantum
if plurals[2] then
error("With plurale tantum noun, can't specify more than one plural")
end
data.genders = {"p"} -- ini sepatutnya memasukkan secara automatik kategori 'pluralia tantum' yang betul
insert_plurale_tantum_inflections("jamak sahaja", plurals[1])
return
end
local function inscat(cat)
insert(data.categories, "Kategori: " .. langname .. " " .. cat)
end
local need_default_plural = pos == "noun"
if first_pl_term() == "sp" then
-- ditafsirkan sebagai tunggal atau jamak
sp = remove(plurals, 1) -- Buang "sp" tetapi simpan untuk hiasannya.
inscat("kata nama ditafsirkan sebagai tunggal atau jamak")
data.genders = {"s", "p"} -- ini sepatutnya memasukkan secara automatik kategori 'pluralia tantum' yang betul
insert_plurale_tantum_inflections(nil, sp)
need_default_plural = false
elseif first_pl_term() == "-" then
-- Kata nama tak berbilang; sekali-sekala mungkin mempunyai bentuk jamak
local hyphpl = remove(plurals, 1) -- Buang "-" tetapi simpan untuk hiasan.
inscat("kata nama tak berbilang")
-- Jika bentuk jamak diberikan secara eksplisit, kemudian tunjukkan "lazimnya"
if plurals[1] then
insert_fixed_inflection(data, "lazimnya <<uncountable>>", hyphpl)
else
insert_fixed_inflection(data, "<<uncountable>>", hyphpl)
end
need_default_plural = false
elseif first_pl_term() == "#" then
-- Selalunya boleh dibilang (cth., "grilled cheese")
local hashpl = remove(plurals, 1) -- Buang "#" tetapi simpan untuk hiasan.
insert_fixed_inflection(data, "lazimnya <<countable>>", hashpl)
inscat("kata nama tak berbilang")
inscat("kata nama boleh dibilang")
-- Jika tiada bentuk jamak diberikan, tambah satu lalai sekarang
if not plurals[1] then
plurals[1] = {term = escape(add_suffix(pagename, "s.plural", pos))}
end
elseif first_pl_term() == "~" then
-- Kata nama berbilang/tak berbilang campur, sentiasa mempunyai bentuk jamak
local tildepl = remove(plurals, 1) -- Buang "~" tetapi simpan untuk hiasan.
insert_fixed_inflection(data, "<<countable>> dan <<uncountable>>", tildepl)
inscat("kata nama tak berbilang")
inscat("kata nama boleh dibilang")
-- Jika tiada bentuk jamak diberikan, tambah satu lalai sekarang
if not plurals[1] then
plurals[1] = {term = escape(add_suffix(pagename, "s.plural", pos))}
end
end
-- Bentuk jamak tidak diketahui
if first_pl_term() == "?" then
local questionpl = remove(plurals, 1) -- Buang "?" tetapi simpan untuk hiasan.
-- Tidak dikehendaki; lihat [[Wiktionary:Tea_room/2021/August#"Plural unknown or uncertain"]]
-- insert_fixed_inflection(data, "plural unknown or uncertain", questionpl)
inscat("kata nama dengan bentuk jamak yang tidak diketahui atau tidak pasti")
if plurals[1] then
error("Can't specify explicit plurals along with '?' for unknown/uncertain plural")
end
return
end
-- Bentuk jamak tidak diperakui
if first_pl_term() == "!" then
local exclampl = remove(plurals, 1) -- Buang "!" tetapi simpan untuk hiasan.
insert_fixed_inflection(data, "bentuk jamak tidak diperakui", exclampl)
inscat("kata nama dengan bentuk jamak yang tidak diperakui")
if plurals[1] then
error("Can't specify explicit plurals along with '!' for unattested plural")
end
return
end
-- Jika tiada bentuk jamak diberikan, mungkin tambah satu lalai, sebaliknya (apabila "-" diberikan atau kata nama khas) kembalikan.
if not plurals[1] then
if not need_default_plural then
inscat("kata nama tak berbilang")
return
end
plurals[1] = {term = escape(add_suffix(pagename, "s.plural", pos))}
end
-- Terdapat bentuk jamak untuk dipaparkan, jadi paparkannya.
inscat("kata nama boleh dibilang")
local irregular, indeclinable
for i, pl in ipairs(plurals) do
local canon_pl = canonicalize_plural(pl.term, pagename, pos)
if canon_pl then
pl.term = canon_pl
end
local pl_term = get_link_page(pl.term, lang)
if not (pagename:find(" ") or is_regular_plural(pl_term, pagename)) then
irregular = true
if pl_term == pagename then
indeclinable = true
end
end
end
if irregular then
inscat("kata nama dengan bentuk jamak tak nalar")
end
if indeclinable then
inscat("kata nama tak terubah form")
end
insert_inflection(data, plurals, "jamak", "p")
end
-- Mengembalikan parameter yang akan digunakan untuk kata nama dan kata nama khas. Pada masa ini ia sama.
local noun_params = {
[1] = list_param,
["pl\1qual"] = {list = true, allow_holes = true, replaced_by = false,
instead = "gunakan pengubah suai dalam baris <l:...> atau <q:...> pada bentuk jamak",
},
-- Empat yang berikut hanya digunakan untuk pluralia tantum (1=p)
["sg"] = list_param,
["attr"] = list_param,
}
pos_functions["nouns"] = {
params = noun_params,
func = do_nouns,
}
pos_functions["proper nouns"] = {
params = noun_params,
func = function(args, data)
return do_nouns(args, data, "proper noun")
end,
}
local function base_default_verb_forms(verb)
return escape(add_suffix(verb, "s.verb")), escape(add_suffix(verb, "ing")), escape(add_suffix(verb, "d"))
end
local function default_verb_forms(verb)
local full_s_form, full_ing_form, full_ed_form = base_default_verb_forms(verb)
if verb:find(" ") then
local first, rest = verb:match("^(.-)( .*)$")
local first_s_form, first_ing_form, first_ed_form = base_default_verb_forms(first)
return full_s_form, full_ing_form, full_ed_form, first_s_form .. rest, first_ing_form .. rest,
first_ed_form .. rest, first, rest
else
return full_s_form, full_ing_form, full_ed_form, nil, nil, nil, nil, nil
end
end
local function compute_double_last_cons_stem_of_split_verb(verb, ending)
local first, rest = verb:match("^(.-)( .*)$")
if not first then
error("Kata kerja '" .. verb .. "' mesti mempunyai ruang padanya untuk menggunakan **")
end
local last_cons = first:match("([bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])$")
if not last_cons then
error("Perkataan pertama '" .. first .. "' mesti berakhir dengan konsonan untuk menggunakan **")
end
return first .. last_cons .. ending .. rest
end
local function check_non_nil_star_form(form, pagename)
if form == nil then
error("Kata kerja '" .. pagename .. "' mesti mempunyai ruang padanya untuk menggunakan *, **, *l, *! atau *'")
end
return form
end
local function sub_tilde(form, pagename)
if not form then
return nil
end
if form:find("~") then
form = form:gsub("~", replacement_escape(pagename))
end
return form
end
local deprecated_qual_replaced_by_inline_modifier = {
list = true, allow_holes = true, replaced_by = false,
instead = "gunakan pengubah suai dalam baris <q:...> atau <l:...> pada nilai"
}
pos_functions["verbs"] = {
params = {
[1] = {list = "pres_3sg", disallow_holes = true},
["pres_3sg\1_qual"] = deprecated_qual_replaced_by_inline_modifier,
[2] = {list = "pres_ptc", disallow_holes = true},
["pres_ptc\1_qual"] = deprecated_qual_replaced_by_inline_modifier,
[3] = {list = "past", disallow_holes = true},
["past\1_qual"] = deprecated_qual_replaced_by_inline_modifier,
[4] = {list = "past_ptc", allow_holes = true},
["past_ptc\1_qual"] = deprecated_qual_replaced_by_inline_modifier,
["noautolinkverb"] = boolean_param,
["angle_bracket"] = boolean_param,
},
func = function(args, data)
-- Dapatkan parameter
local par1s
local par2s = parse_inflection(args, {2, "pres_ptc"})
local par3s = parse_inflection(args, {3, "past"})
local par4s = parse_inflection(args, {4, "past_ptc"})
local pres_3sgs, pres_ptcs, pasts, past_ptcs
local pagename = data.pagename
------------------------------------------- FUNGSI UTILITI #2 ------------------------------------------
-- Fungsi-fungsi ini digunakan dalam kedua-dua format parameter berasingan dan dalam parameter pengesampingan seperti past_ptc2=.
local full_default_s, full_default_ing, full_default_ed, split_default_s, split_default_ing, split_default_ed
local lemma
local function set_lemma_and_default_forms(the_lemma)
lemma = the_lemma
full_default_s, full_default_ing, full_default_ed, split_default_s, split_default_ing, split_default_ed,
lemma_first, lemma_rest = default_verb_forms(the_lemma)
end
local function canonicalize_s_form(form)
if form == "+" then
error("Internal error: Should not see '+' here")
elseif form == "^" then
return full_default_s
elseif form == "*" then
return check_non_nil_star_form(split_default_s, lemma)
elseif form == "++" then
return compute_plusplus_s_form(lemma, full_default_s)
elseif form == "**" then
if lemma:find("^[^ ]*[szx] ") then
return compute_double_last_cons_stem_of_split_verb(lemma, "es")
else
return check_non_nil_star_form(split_default_s, lemma)
end
elseif form == "+!" then
return lemma .. "s"
elseif form == "*!" then
return check_non_nil_star_form(lemma_first) .. "s" .. lemma_rest
elseif form == "+'" then
return lemma .. "'s"
elseif form == "*'" then
return check_non_nil_star_form(lemma_first) .. "'s" .. lemma_rest
elseif form == "+l" then
if lemma:find("[szx]$") then
return {{term = full_default_s, l = {"AS"}},
{term = compute_plusplus_s_form(lemma, full_default_s), l = {"UK"}}}
else
return compute_plusplus_s_form(lemma, full_default_s)
end
elseif form == "*l" then
if lemma:find("^[^ ]*[szx] ") then
return {{term = check_non_nil_star_form(split_default_s, lemma), l = {"AS"}},
{term = compute_double_last_cons_stem_of_split_verb(lemma, "es"), l = {"UK"}}}
else
return check_non_nil_star_form(split_default_s, lemma)
end
else
return sub_tilde(form, lemma)
end
end
local function canonicalize_ing_form(form)
if form == "+" then
error("Internal error: Should not see '+' here")
elseif form == "^" then
return full_default_ing
elseif form == "*" then
return check_non_nil_star_form(split_default_ing, lemma)
elseif form == "++" then
return compute_double_last_cons_stem(lemma) .. "ing"
elseif form == "**" then
return compute_double_last_cons_stem_of_split_verb(lemma, "ing")
elseif form == "+!" then
return lemma .. "ing"
elseif form == "*!" then
return check_non_nil_star_form(lemma_first) .. "ing" .. lemma_rest
elseif form == "+'" then
return lemma .. "'ing"
elseif form == "*'" then
return check_non_nil_star_form(lemma_first) .. "'ing" .. lemma_rest
elseif form == "+l" then
return {{term = full_default_ing, l = {"AS"}},
{term = compute_double_last_cons_stem(lemma) .. "ing", l = {"UK"}}}
elseif form == "*l" then
return {{term = check_non_nil_star_form(split_default_ing, lemma), l = {"AS"}},
{term = compute_double_last_cons_stem_of_split_verb(lemma, "ing"), l = {"UK"}}}
else
return sub_tilde(form, lemma)
end
end
local function canonicalize_ed_form(form)
if form == "+" then
error("Internal error: Should not see '+' here")
elseif form == "^" then
return full_default_ed
elseif form == "*" then
return check_non_nil_star_form(split_default_ed, lemma)
elseif form == "++" then
return compute_double_last_cons_stem(lemma) .. "ed"
elseif form == "+!" then
return lemma .. "ed"
elseif form == "*!" then
return check_non_nil_star_form(lemma_first) .. "ed" .. lemma_rest
elseif form == "+'" then
return {{term = lemma .. "'d"}, {term = lemma .. "'ed"}}
elseif form == "*'" then
return {{term = check_non_nil_star_form(lemma_first) .. "'d" .. lemma_rest},
{term = check_non_nil_star_form(lemma_first) .. "'ed" .. lemma_rest}}
elseif form == "**" then
return compute_double_last_cons_stem_of_split_verb(lemma, "ed")
elseif form == "+l" then
return {{term = full_default_ed, l = {"AS"}},
{term = compute_double_last_cons_stem(lemma) .. "ed", l = {"UK"}}}
elseif form == "*l" then
return {{term = check_non_nil_star_form(split_default_ed, lemma), l = {"AS"}},
{term = compute_double_last_cons_stem_of_split_verb(lemma, "ed"), l = {"UK"}}}
else
return sub_tilde(form, lemma)
end
end
-- BAIKI: pilihan seharusnya "+", "*", "++", "**", "+n", "*n", "++n" dan "**n", tetapi bukan "n"
local function canonicalize_en_form(form)
if form == "n" then
track("n4")
return add_suffix(lemma, "n")
end
return canonicalize_ed_form(form)
end
--------------------------------- KOD PENGHURAIAN/PENTASRIFAN UTAMA --------------------------------
local is_angle_bracket = args.angle_bracket
if is_angle_bracket then
if par2s[1] or par3s[1] or par4s[1] then
error("Can't specify explicit values for 2=, 3= or 4= along with the angle-bracket format")
end
elseif is_angle_bracket == nil and not par2s[1] and not par3s[1] and not par4s[1] and not args[1][2] and
args[1][1] and args[1][1]:find("<") then
if put.term_contains_top_level_html(args[1][1]) then
-- Selalunya, term_contains_top_level_html() mengembalikan 'true' pada format kurungan sudut, yang akan
-- menjadikan pcall() di bawah berjaya tetapi membiarkan kurungan sudut seperti sedia ada. Periksa untuk ini dan hanya lakukan
-- pcall() jika term_contains_top_level_html() mengembalikan false.
is_angle_bracket = true
else
-- Jika terdapat kekaburan sama ada ia adalah format kurungan sudut atau parameter berasingan dengan pengubah suai dalam baris,
-- cuba untuk menghuraikannya sebagai parameter berasingan. Jika ralat berlaku, anggap ia sebagai format kurungan sudut.
local ok
ok, par1s = pcall(parse_inflection, args, {1, "pres_3sg"})
if not ok then
par1s = nil
is_angle_bracket = true
end
end
end
if is_angle_bracket then
-------------------------- FORMAT KURUNGAN SUDUT --------------------------
-- (0) Kembangkan perkataan berbilang suku kata dengan kurungan sudut tepat pada perkataan pertama.
local arg11 = args[1][1]
if arg11:find("^<.*>$") and pagename:find(" ") then
local first, rest = pagename:match("^(.-)( .*)$")
arg11 = first .. arg11 .. rest
end
-- (1) Menghurai spesifikasi penunjuk di dalam kurungan sudut.
local function parse_indicator_spec(angle_bracket_spec)
local inside = angle_bracket_spec:match("^<(.*)>$")
assert(inside)
local segments = put.parse_balanced_segment_run(inside, "[", "]")
local comma_separated_groups = put.split_alternating_runs(segments, ",")
if #comma_separated_groups > 4 then
error("Terlalu banyak bahagian yang dipisahkan dengan koma dalam spesifikasi penunjuk, dijangkakan paling banyak 4: " ..
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
error("Teks tidak berkaitan selepas nota kaki berkurungan: '" .. concat(separated_group) .. "'")
end
if not footnotes then
footnotes = {}
end
insert(footnotes, separated_group[j])
end
return footnotes
end
local function fetch_specs(comma_separated_group)
if not comma_separated_group then
return {{term = "+"}}
end
local specs = {}
local colon_separated_groups = put.split_alternating_runs(comma_separated_group, ":")
for _, colon_separated_group in ipairs(colon_separated_groups) do
local form = colon_separated_group[1]
if form == "*" or form == "**" or form == "*l" or form == "*!" or form == "*'" then
error("*, **, *l, *! dan *' tidak dibenarkan di dalam spesifikasi penunjuk: " .. angle_bracket_spec)
end
if form == "" then
form = "+"
end
local termobj = {
term = form
}
local footnotes = fetch_footnotes(colon_separated_group)
if footnotes then
for _, footnote in ipairs(footnotes) do
m_headword_utilities.add_footnote_to_termobj(termobj, footnote)
end
end
insert(specs, termobj)
end
return specs
end
local s_specs = fetch_specs(comma_separated_groups[1])
local ing_specs = fetch_specs(comma_separated_groups[2])
local ed_specs = fetch_specs(comma_separated_groups[3])
local en_specs = fetch_specs(comma_separated_groups[4])
return {
forms = {},
s_specs = s_specs,
ing_specs = ing_specs,
ed_specs = ed_specs,
en_specs = en_specs,
}
end
local parse_props = {
parse_indicator_spec = parse_indicator_spec,
}
local alternant_multiword_spec = iut.parse_inflected_text(arg11, parse_props)
-- (2) Periksa kurungan yang ditentukan pengguna; buang sebarang pautan dari kata dasar, tetapi ingat bentuk asalnya
-- supaya kita boleh menggunakannya di bawah dalam bentuk 'lemma_linked'.
-- Semak untuk melihat jika terdapat kurungan dalam pra-teks atau pasca-teks. Jika ada, gunakan kata dasar berpaut (dengan
-- kata kerja dipautkan secara automatik kecuali noautolinkverb diberikan). Jika tidak, gunakan algoritma kata kepala lalai.
local function check_bracket(val)
if val:find("%[%[") then
alternant_multiword_spec.saw_bracket = true
end
end
for _, alternant_or_word_spec in ipairs(alternant_multiword_spec.alternant_or_word_specs) do
check_bracket(alternant_or_word_spec.before_text)
if alternant_or_word_spec.alternants then
for _, multiword_spec in ipairs(alternant_or_word_spec.alternants) do
for _, word_spec in ipairs(multiword_spec.word_specs) do
check_bracket(word_spec.before_text)
end
check_bracket(multiword_spec.post_text)
end
end
end
check_bracket(alternant_multiword_spec.post_text)
iut.map_word_specs(alternant_multiword_spec, function(base)
if base.lemma == "" then
base.lemma = pagename
end
base.orig_lemma = base.lemma
base.lemma = remove_links(base.lemma)
if args.noautolinkverb or base.orig_lemma:find("%[%[") then
base.linked_lemma = base.orig_lemma
else
base.linked_lemma = "[[" .. base.orig_lemma .. "]]"
end
end)
-- (3) Mentasrif kata kerja mengikut spesifikasi penunjuk yang dihurai di atas.
local all_verb_slots = {
lemma = "kata dasar",
lemma_linked = "infinitive",
s_form = "3|s|pres",
ing_form = "pres|ptcp",
ed_form = "past",
en_form = "past|ptcp",
}
local function conjugate_verb(base)
local function process_specs(slot, specs, canon_func, default_values, default_already_formobj)
local function insert_termobj_into_slot(termobj)
local formobj = m_headword_utilities.convert_termobj_to_formobj(termobj)
-- Jika bentuk itu ialah -, jangan masukkan apa-apa bentuk, ini akan mengakibatkan tiada bentuk keseluruhan
-- (malah ia akan menjadi nil). Kita akan semak hal itu di bawah dan akan menggantikan tanda "-" tunggal sebagai
-- bentuk yang membolehkannya ditukar menjadi label istimewa seperti "tiada partisipel kini".
if formobj.form == "-" then
if formobj.footnotes then
error("Tidak dapat mengekalkan nota kaki yang dinyatakan pada bentuk yang hilang '-': BAIKI: " ..
dump(formobj.footnotes))
end
else
iut.insert_form(base.forms, slot, formobj)
end
end
local function canonicalize_and_insert(arg)
local canon_arg = canon_func(arg)
if type(canon_arg) == "string" then
arg.term = canon_arg
insert_termobj_into_slot(arg)
else
for _, canon in ipairs(canon_arg) do
m_headword_utilities.combine_termobj_decorations(canon, arg)
insert_termobj_into_slot(canon)
end
end
end
for _, arg in ipairs(specs) do
if arg.term == "+" then
if default_values then -- akan menjadi nil jika masa lepas dinyatakan sebagai - dan tiada past ptc diberikan
for _, val in ipairs(default_values) do
val = shallowCopy(val)
if default_already_formobj then
local argformobj = m_headword_utilities.convert_termobj_to_formobj(arg)
val.footnotes = iut.combine_footnotes(val.footnotes, argformobj.footnotes)
iut.insert_form(base.forms, slot, val)
else
m_headword_utilities.combine_termobj_decorations(val, arg)
canonicalize_and_insert(val)
end
end
end
else
canonicalize_and_insert(arg)
end
end
end
set_lemma_and_default_forms(base.lemma)
local all_part_default_specs = {}
local function process_and_canonicalize_s_form(arg)
local form = arg.term
if form == "+" then
error("Ralat dalaman: '+' sepatutnya ditukar menjadi '^' sekarang")
end
if form == "*" or form == "**" or form == "*l" or form == "*!" or form == "*'" then
error(("Ralat dalaman: '%s' sepatutnya telah melemparkan ralat"):format(form))
end
if form == "^" or form == "++" or form == "+l" or form == "+!" or form == "+'" then
insert(all_part_default_specs, shallowCopy(arg))
end
return canonicalize_s_form(form)
end
process_specs("s_form", base.s_specs, process_and_canonicalize_s_form, {{term = "^"}})
if not all_part_default_specs[1] then
all_part_default_specs[1] = {term = "^"}
end
process_specs("ing_form", base.ing_specs, function(arg) return canonicalize_ing_form(arg.term) end,
all_part_default_specs)
process_specs("ed_form", base.ed_specs, function(arg) return canonicalize_ed_form(arg.term) end,
all_part_default_specs)
process_specs("en_form", base.en_specs, function(arg) return canonicalize_en_form(arg.term) end,
base.forms.ed_form, "default already formobj")
iut.insert_form(base.forms, "lemma", {form = base.lemma})
-- Tambah versi pautan bagi lemma untuk kegunaan dalam head=. Kita menulis ini dalam gaya yang umum sekiranya
-- ada berbilang bentuk lemma (yang ketika ini tidak mungkin pada tahap ini, walaupun ia
-- mungkin dilakukan secara menyeluruh menggunakan tatatanda ((...,...))).
iut.insert_forms(base.forms, "lemma_linked", iut.map_forms(base.forms.lemma, function(form)
if form == base.lemma and base.linked_lemma:find("%[%[") then
return base.linked_lemma
else
return form
end
end))
end
local inflect_props = {
slot_table = all_verb_slots,
inflect_word_spec = conjugate_verb,
}
iut.inflect_multiword_or_alternant_multiword_spec(alternant_multiword_spec, inflect_props)
-- (4) Dapatkan borang-borang dan letakkan kata dasar yang ditasrif dalam data.heads jika tidak diberikan secara tersurat.
local function fetch_termobjs(slot)
local forms = alternant_multiword_spec.forms[slot]
-- Lihat di atas. Ini sepatutnya hanya berlaku jika pengguna menggunakan '-' untuk sesuatu spesifikasi.
if not forms or not forms[1] then
return {{term = "-"}}
end
local termobjs = {}
for _, formobj in ipairs(forms) do
insert(termobjs, m_headword_utilities.convert_formobj_to_termobj(formobj))
end
return termobjs
end
pres_3sgs = fetch_termobjs("s_form")
pres_ptcs = fetch_termobjs("ing_form")
pasts = fetch_termobjs("ed_form")
past_ptcs = fetch_termobjs("en_form")
-- Guna bentuk kata dasar "berpaut" sebagai pengepala jika tiada head= diberikan secara nyata dan pengguna menentukan
-- kurungan dalam salah satu kata dasar. Jika tidak kita akan gunakan algoritma pemautan kata kepala lalai.
if not data.user_specified_heads[1] and alternant_multiword_spec.saw_bracket then
data.heads = {}
for _, lemma_obj in ipairs(alternant_multiword_spec.forms.lemma_linked) do
insert(data.heads, m_headword_utilities.convert_formobj_to_termobj(lemma_obj))
end
end
else
-------------------------- FORMAT PARAMETER BERASINGAN --------------------------
set_lemma_and_default_forms(pagename)
par1s = par1s or parse_inflection(args, {1, "pres_3sg"})
pres_3sgs = {}
pres_ptcs = {}
pasts = {}
past_ptcs = {}
if not par1s[1] then
par1s = {{term = "+"}}
end
if not par2s[1] then
par2s = {{term = "+"}}
end
if not par3s[1] then
par3s = {{term = "+"}}
end
if not par4s[1] then
par4s = {{term = "+"}}
end
local function process_argument(args, dest, canon_func, default_values, default_already_canonicalized)
local function canonicalize_and_insert(arg)
local canon_arg = canon_func(arg)
if type(canon_arg) == "string" then
arg.term = canon_arg
m_headword_utilities.insert_termobj_combining_duplicates(dest, arg)
else
for _, canon in ipairs(canon_arg) do
m_headword_utilities.combine_termobj_decorations(canon, arg)
m_headword_utilities.insert_termobj_combining_duplicates(dest, canon)
end
end
end
for _, arg in ipairs(args) do
if arg.term == "+" then
for _, val in ipairs(default_values) do
val = shallowCopy(val)
m_headword_utilities.combine_termobj_decorations(val, arg)
if default_already_canonicalized then
m_headword_utilities.insert_termobj_combining_duplicates(dest, val)
else
canonicalize_and_insert(val)
end
end
else
canonicalize_and_insert(arg)
end
end
end
local all_part_default_specs = {}
local function process_and_canonicalize_s_form(arg)
local form = arg.term
if form == "+" then
error("Ralat dalaman: '+' sepatutnya ditukar menjadi '^' sekarang")
end
if form == "^" or form == "++" or form == "+l" or form == "+!" or form == "+'" or
form == "*" or form == "**" or form == "*l" or form == "*!" or form == "*'" then
insert(all_part_default_specs, shallowCopy(arg))
end
return canonicalize_s_form(form)
end
process_argument(par1s, pres_3sgs, process_and_canonicalize_s_form, {{term = "^"}})
if not all_part_default_specs[1] then
all_part_default_specs[1] = {term = "^"}
end
process_argument(par2s, pres_ptcs, function(arg) return canonicalize_ing_form(arg.term) end,
all_part_default_specs)
process_argument(par3s, pasts, function(arg) return canonicalize_ed_form(arg.term) end,
all_part_default_specs)
process_argument(par4s, past_ptcs, function(arg) return canonicalize_en_form(arg.term) end,
pasts, "default already canonicalized")
end
------------------------------------------- MASUKKAN INFLEKSI ------------------------------------------
insert_inflection(data, pres_3sgs, "orang ketiga tunggal ringkas kini", "s-verb-form")
insert_inflection(data, pres_ptcs, "partisipel kini", "ing-form")
if deepEquals(pasts, past_ptcs) then
insert_inflection(data, pasts, "lepas ringkas dan partisipel lepas", "ed-form",
"tiada kata lepas ringkas mahupun partisipel lepas")
else
insert_inflection(data, pasts, "lepas ringkas", "spast")
insert_inflection(data, past_ptcs, "partisipel lepas", "past|part")
end
if pagename:find(" ") then
-- Semak ruang letak "it"
local words = split(pagename, " ")
for _, word in ipairs(words) do
if word == "it" or word == "its" or word == "it's" then
insert(data.categories, "Kategori: Istilah " .. langname .. ' dengan ruang letak "it"')
break
end
end
-- Semak kata kerja frasa
local phrasal_adverbs = list_to_set{
-- NOTA: Ini hanya perlu mengandungi kata keterangan frasa yang lazim, bukan kata rawak seperti [[low]],
-- [[adrift]], dan lain-lain.
"aback",
"about",
"above",
"across",
"after",
"against",
"ahead",
"along",
"apart",
"around",
"as",
"aside",
"at",
"away",
"back",
"before",
"behind",
"below",
"between",
"beyond",
"by",
"down",
"for",
"forth",
"from",
"in",
"into",
"of",
"off",
"on",
"onto",
"out",
"over",
"past",
"round",
"through",
"to",
"together",
"towards",
"under",
"up",
"upon",
"with",
"without",
}
local allowed_non_adverb_words = list_to_set{
"it",
"one",
"oneself",
"someone",
}
local base = pagename
local seen_adverbs = {}
-- Hanya anggap kata kerja sebagai kata kerja frasa jika ia terdiri daripada satu kata kerja dasar yang diikuti hanya oleh sama ada
-- kata keterangan daripada `phrasal_adverbs` atau perkataan ruang letak daripada `allowed_non_adverb_words`, dengan
-- sekurang-kurangnya satu perkataan mengikut daripada `phrasal_adverbs` (oleh itu [[can it]] bukan kata kerja frasa).
while true do
local prev, word = base:match("^(.+) (.-)$")
if not prev then
break
end
if phrasal_adverbs[word] then
insert(seen_adverbs, word)
elseif allowed_non_adverb_words[word] then
-- tidak berbuat apa-apa
else
break
end
base = prev
end
if not base:find(" ") and seen_adverbs[1] then
insert(data.categories, "Kategori: Kata kerja frasa " .. langname)
for i = #seen_adverbs, 1, -1 do
insert(data.categories, 'Kategori: Kata kerja frasa ' .. langname .. ' yang dibentuk dengan "' .. seen_adverbs[i] ..
'"')
end
end
end
end,
}
-----------------------------------------------------------------------------------------
-- Bentuk imbuhan akhiran --
-----------------------------------------------------------------------------------------
pos_functions["suffix forms"] = {
params = {
[1] = {required = true, list = true, disallow_holes = true},
},
func = function(args, data, is_suffix)
local suffix_type = {}
for _, typ in ipairs(args[1]) do
insert(suffix_type, "akhiran membentuk " .. typ)
end
insert(data.inflections, {label = "bentuk bukan lema daripada " .. m_table.serialCommaJoin(suffix_type, {conj = "or"})})
end,
}
return export
m878ggpknq1yow5ywu4jd7mmt66yd8m
Templat:en-ks/ujian
10
145823
375861
2026-09-25T11:24:12Z
SNN95
2113
Mencipta laman baru dengan kandungan '{{#invoke:en-headword/testcases|show|kata sifat}}<noinclude>{{documentation}} </noinclude>'
375861
wikitext
text/x-wiki
{{#invoke:en-headword/testcases|show|kata sifat}}<noinclude>{{documentation}}
</noinclude>
nwswu5x7ow6m3huc2f40wlzscqk8ncx
Wikikamus:bdr/lemiyang
4
145824
375865
2026-09-25T11:51:07Z
Sipatung
10839
Membuat terjemahan baru
375865
wikitext
text/x-wiki
==Bahasa {{bahasa|bdr}}==
===Kata kerja===
{{inti|bdr|kata kerja}}
# {{label|1=bdr|2=dialek|3=Sabah}} terbang {{cp|bdr|Pipit '''lemiyang''' pekepa' jata' parai.|Burung pipit '''[[terbang]]''' hinggap di atas padi.}}
bs3hgo8afmb9he9kjhg8abhcmiwzltw