Wikikamus
mswiktionary
https://ms.wiktionary.org/wiki/Wikikamus:Laman_Utama
MediaWiki 1.47.0-wmf.19
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
Modul:en-headword
828
10145
373449
258129
2026-09-10T07:42:00Z
EmpAhmadK
4110
373449
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
Modul:affix
828
10384
373470
344469
2026-09-10T09:13:44Z
SNN95
2113
373470
Scribunto
text/plain
local export = {}
local debug_force_cat = false -- if set to true, always display categories even on userspace pages
local m_links = require("Module:links")
local m_str_utils = require("Module:string utilities")
local m_table = require("Module:table")
local en_utilities_module = "Module:en-utilities"
local etymology_module = "Module:etymology"
local pron_qualifier_module = "Module:pron qualifier"
local scripts_module = "Module:scripts"
local utilities_module = "Module:utilities"
-- Export this so the category code in [[Module:category tree/etymology]] can access it.
export.affix_lang_data_module_prefix = "Module:affix/lang-data/"
local ulen = m_str_utils.len
local rfind = m_str_utils.find
local rmatch = m_str_utils.match
local pluralize = require(en_utilities_module).pluralize
local u = m_str_utils.char
local ucfirst = m_str_utils.ucfirst
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
function export.affix_variants(canonical, variants)
local mappings = {}
for _, variant in ipairs(variants) do
mappings[variant] = canonical
end
return mappings
end
function export.id_mapping(default, ids)
local mapping = { default = default }
if ids then
for id, target in pairs(ids) do
mapping[id] = target
end
end
return mapping
end
function export.id_mapping_with_affix_variants(base, id_variants)
local mappings = {}
for id, variants in pairs(id_variants) do
for _, variant in ipairs(variants) do
mappings[variant] = export.id_mapping(base, {[id] = base})
end
end
return mappings
end
function export.merge_tables(...)
local result = {}
for i = 1, select('#', ...) do
local t = select(i, ...)
if t then
for k, v in pairs(t) do
result[k] = v
end
end
end
return result
end
-- Export this so the category code in [[Module:category tree/etymology]] can access it.
export.langs_with_lang_specific_data = {
["az"] = true,
["fi"] = true,
["fr"] = true,
["izh"] = true,
["la"] = true,
["sah"] = true,
["tr"] = true,
["trk-pro"] = true,
}
local default_pos = "perkataan"
--[==[ intro:
===About different types of hyphens ("template", "display" and "lookup"):===
* The "template hyphen" is the per-script hyphen character that is used in template calls to indicate that a term is an
affix. This is always a single Unicode char, but there may be multiple possible hyphens for a given script. Normally
this is just the regular hyphen character "-", but for some non-Latin-script languages (currently only right-to-left
languages), it is different.
* The "display hyphen" is the string (which might be an empty string) that is added onto a term as displayed and linked,
to indicate that a term is an affix. Currently this is always either the same as the template hyphen or an empty
string, but the code below is written generally enough to handle arbitrary display hyphens. Specifically:
*# For East Asian languages, the display hyphen is always blank.
*# For Arabic-script languages, either tatweel (ـ) or ZWNJ (zero-width non-joiner) are allowed as template hyphens,
where ZWNJ is supported primarily for Farsi, because some suffixes have non-joining behavior. The display hyphen
corresponding to tatweel is also tatweel, but the display hyphen corresponding to ZWNJ is blank (tatweel is also
the default display hyphen, for calls to {{tl|prefix}}/{{tl|suffix}}/etc. that don't include an explicit hyphen).
* The "lookup hyphen" is the hyphen that is used when looking up language-specific affix mappings. (These mappings are
discussed in more detail below when discussing link affixes.) It depends only on the script of the affix in question.
Most scripts (including East Asian scripts) use a regular hyphen "-" as the lookup hyphen, but Hebrew and Arabic
have their own lookup hyphens (respectively maqqef and tatweel). Note that for Arabic in particular, there are
three possible template hyphens that are recognized (tatweel, ZWNJ and regular hyphen), but mappings must use tatweel.
===About different types of affixes ("template", "display", "link", "lookup" and "category"):===
* A "template affix" is an affix in its source form as it appears in a template call. Generally, a template affix has an
attached template hyphen (see above) to indicate that it is an affix and indicate what type of affix it is (prefix,
suffix, interfix or circumfix), but some of the older-style templates such as {{tl|suffix}}, {{tl|prefix}},
{{tl|confix}}, etc. have "positional" affixes where the presence of the affix in a certain position (e.g. the second
or third parameter) indicates that it is a certain type of affix, whether or not it has an attached template hyphen.
* A "display affix" is the corresponding affix as it is actually displayed to the user. The display affix may differ
from the template affix for various reasons:
*# The display affix may be specified explicitly using the {{para|alt<var>N</var>}} parameter, the `<alt:...>` inline
modifier or a piped link of the form e.g. `<nowiki>[[-kas|-käs]]</nowiki>` (here indicating that the affix should
display as `-käs` but be linked as `-kas`). Here, the template affix is arguably the entire piped link, while the
display affix is `-käs`.
*# Even in the absence of {{para|alt<var>N</var>}} parameters, `<alt:...>` inline modifiers and piped links, certain
languages have differences between the "template hyphen" specified in the template (which always needs to be
specified somehow or other in templates like {{tl|affix}}, to indicate that the term is an affix and what type of
affix it is) and the display hyphen (see above), with corresponding differences between template and display
affixes.
* A (regular) "link affix" is the affix that is linked to when the affix is shown to the user. The link affix is usually
the same as the display affix, but will differ in one of three circumstances:
*# The display and link affixes are explicitly made different using {{para|alt<var>N</var>}} parameters, `<alt:...>`
inline modifiers or piped links, as described above under "display affix".
*# For certain languages, certain affixes are mapped to canonical form using language-specific mappings. For example,
in Finnish, the adjective-forming suffix {{m|fi|-kas}} appears as {{m|fi|-käs}} after front vowels, but logically
both forms are the same suffix and should be linked and categorized the same. Similarly, in Latin, the negative and
intensive prefixes spelled {{m|la|in-}} (etymologically two distinct prefixes) appear variously as {{m|la|il-}},
{{m|la|im-}} or {{m|la|ir-}} before certain consonants. Mappings are supplied in [[Module:affix/lang-data/LANGCODE]]
to convert Finnish {{m|fi|-käs}} to {{m|fi|-kas}} for linking and categorization purposes. Note that the affixes in
the mappings use "lookup hyphens" to indicate the different types of affixes, which is usually the same as the
template hyphen but differs for Arabic scripts, because there are multiple possible template hyphens recognized but
only one lookup hyphen (tatweel). The form of the affix as used to look up in the mapping tables is called the
"lookup affix"; see below.
* A "stripped link affix" is a link affix that has been passed through the language's `stripDiacritics()` function, which
may strip certain diacritics: e.g. macrons in Latin and Old English (indicating length); acute and grave accents in
Russian and various other Slavic languages (indicating stress); vowel diacritics in most Arabic-script languages; and
also tatweel in some Arabic-script languages (currently, for example, Persian, Arabic and Urdu strip tatweel, but
Ottoman Turkish does not). Stripped link affixes are currently what are used in category names.
* A "lookup affix" is the form of the affix as it is looked up in the language-specific lookup mappings described above
under link affixes. There are actually two lookup stages:
*# First, the affix is looked up in a modified display form (specifically, the same as the display affix but using
lookup hyphens). Note that this lookup does not occur if an explicit display form is given using
{{para|alt<var>N</var>}} or an `<alt:...>` inline modifier, or if the template affix contains a piped or embedded
link.
*# If no entry is found, the affix is then looked up in a modified link form (specifically, the modified display
form passed through the language's `stripDiacritics()` function, which strips out certain diacritics, but with the
lookup hyphen re-added if it was stripped out, as in the case of tatweel in many Arabic-script languages).
The reason for this double lookup procedure is to allow for mappings that are sensitive to the extra diacritics, but
also allow for mappings that are not sensitive in this fashion (e.g. Russian {{m|ru|-ливый}} occurs both stressed and
unstressed, but is the same prefix either way).
* A "category affix" is the affix as it appears in categories such as [[:Category:Finnish terms suffixed with -kas|
Category:Finnish terms suffixed with ''-kas'']]. The category affix is currently always the same as the stripped link
affix. This means that for Arabic-script languages, it may or may not have a tatweel, even if the correponding display
affix and regular link affix have a tatweel. As mentioned above, stripDiacritics() strips tatweel for Arabic, Persian
and Urdu, but not for Ottoman Turkish. Hence affix categories for Arabic, Persian and Urdu will be missing the
tatweel, but affix categories for Ottoman Turkish will have it. An additional complication is that if the template
affix contains a ZWNJ, the display (and hence the link and category affixes) will have no hyphen attached in any case.
]==]
-----------------------------------------------------------------------------------------
-- Template and display hyphens --
-----------------------------------------------------------------------------------------
--[=[
Per-script template hyphens. The template hyphen is what appears in the {{affix}}/{{prefix}}/{{suffix}}/etc. template
(in the wikicode). See above.
They key below is a script code, after removing a hyphen and anything preceding. Hence, script codes like 'mnc-Mong'
and 'xwo-Mong' will match 'Mong'.
The value below is a string consisting of one or more hyphen characters. If there is more than one character, the
default hyphen must come last and a non-default function must be specified for the script in display_hyphens[] so
the correct display hyphen will be specified when no template hyphen is given (in {{suffix}}/{{prefix}}/etc.).
Script detection is normally done when linking, but we need to do it earlier. However, under most circumstances we
don't need to do script detection. Specifically, we only need to do script detection for a given language if
(a) the language has multiple scripts; and
(b) at least one of those scripts is listed below or in display_hyphens.
]=]
local ZWNJ = u(0x200C) -- zero-width non-joiner
local template_hyphens = {
-- This covers all Arabic scripts. See above.
["Arab"] = "ـ" .. ZWNJ .. "-", -- tatweel + zero-width non-joiner + regular hyphen
["Aran"] = "ـ" .. ZWNJ .. "-", -- tatweel + zero-width non-joiner + regular hyphen
["Hebr"] = "־", -- Hebrew-specific hyphen termed "maqqef"
["Mong"] = "᠊",
-- FIXME! What about the following right-to-left scripts?
-- Adlm (Adlam)
-- Armi (Imperial Aramaic)
-- Avst (Avestan)
-- Cprt (Cypriot)
-- Khar (Kharoshthi)
-- Mand (Mandaic/Mandaean)
-- Mani (Manichaean)
-- Mend (Mende/Mende Kikakui)
-- Narb (Old North Arabian)
-- Nbat (Nabataean/Nabatean)
-- Nkoo (N'Ko)
-- Orkh (Orkhon runes)
-- Phli (Inscriptional Pahlavi)
-- Phlp (Psalter Pahlavi)
-- Phlv (Book Pahlavi)
-- Phnx (Phoenician)
-- Prti (Inscriptional Parthian)
-- Rohg (Hanifi Rohingya)
-- Samr (Samaritan)
-- Sarb (Old South Arabian)
-- Sogd (Sogdian)
-- Sogo (Old Sogdian)
-- Syrc (Syriac)
-- Thaa (Thaana)
}
-- Hyphens used when looking up an affix in a lang-specific affix mapping. Defaults to regular hyphen (-). The keys
-- are script codes, after removing a hyphen and anything preceding. Hence, script codes like 'mnc-Mong' and 'xwo-Mong'
-- will match 'Mong'. The value should be a single character.
local lookup_hyphens = {
["Hebr"] = "־",
-- This covers all Arabic scripts. See above.
["Arab"] = "ـ",
["Aran"] = "ـ",
}
-- Default display-hyphen function.
local function default_display_hyphen(script, hyph)
if not hyph then
return template_hyphens[script] or "-"
end
return hyph
end
local function arab_get_display_hyphen(_script, hyph)
if not hyph then
return "ـ" -- tatweel
elseif hyph == ZWNJ then
return ""
else
return hyph
end
end
local function no_display_hyphen(_script, _hyph)
return ""
end
-- Per-script function to return the correct display hyphen given the script and template hyphen. The function should
-- also handle the case where the passed-in template hyphen is nil, corresponding to the situation in
-- {{prefix}}/{{suffix}}/etc. where no template hyphen is specified. The key is the script code after removing a hyphen
-- and anything preceding, so 'mnc-Mong', 'xwo-Mong' etc. will match 'Mong'.
local display_hyphens = {
-- This covers all Arabic scripts. See above.
["Arab"] = arab_get_display_hyphen,
["Aran"] = arab_get_display_hyphen,
["Bopo"] = no_display_hyphen,
["Hani"] = no_display_hyphen,
["Hans"] = no_display_hyphen,
["Hant"] = no_display_hyphen,
-- The following is a mixture of several scripts. Hopefully the specs here are correct!
["Jpan"] = no_display_hyphen,
["Jurc"] = no_display_hyphen,
["Kitl"] = no_display_hyphen,
["Kits"] = no_display_hyphen,
["Laoo"] = no_display_hyphen,
["Nshu"] = no_display_hyphen,
["Shui"] = no_display_hyphen,
["Tang"] = no_display_hyphen,
["Thaa"] = no_display_hyphen,
["Thai"] = no_display_hyphen,
["Tibt"] = no_display_hyphen,
}
-----------------------------------------------------------------------------------------
-- Basic Utility functions --
-----------------------------------------------------------------------------------------
local function glossary_link(entry, text)
text = text or entry
return "[[Lampiran:Glosari#" .. entry .. "|" .. text .. "]]"
end
local function track(page)
if type(page) == "table" then
for i, pg in ipairs(page) do
page[i] = "affix/" .. pg
end
else
page = "affix/" .. page
end
require("Module:debug/track")(page)
end
local function ine(val)
return val ~= "" and val or nil
end
-----------------------------------------------------------------------------------------
-- Compound types --
-----------------------------------------------------------------------------------------
local function make_compound_type(typ, alttext)
return {
text = glossary_link(typ, alttext) .. " majmuk",
cat = typ .. " majmuk",
}
end
-- Make a compound type entry with a simple rather than glossary link.
-- These should be replaced with a glossary link when the entry in the glossary
-- is created.
local function make_non_glossary_compound_type(typ, alttext)
local link = alttext and "[[" .. typ .. "|" .. alttext .. "]]" or "[[" .. typ .. "]]"
return {
text = link .. " majmuk",
cat = typ .. " majmuk",
}
end
local function make_raw_compound_type(typ, alttext)
return {
text = glossary_link(typ, alttext),
cat = pluralize(typ),
}
end
local function make_borrowing_type(typ, alttext)
return {
text = glossary_link(typ, alttext),
borrowing_type = pluralize(typ),
}
end
export.etymology_types = {
["adapted borrowing"] = make_borrowing_type("adapted borrowing"),
["adap"] = "adapted borrowing",
["abor"] = "adapted borrowing",
["alliterative"] = make_non_glossary_compound_type("alliterative"),
["allit"] = "alliterative",
["antonymous"] = make_non_glossary_compound_type("antonymous"),
["ant"] = "antonymous",
["bahuvrihi"] = make_compound_type("bahuvrihi", "bahuvrīhi"),
["bahu"] = "bahuvrihi",
["bv"] = "bahuvrihi",
["coordinative"] = make_compound_type("coordinative"),
["coord"] = "coordinative",
["descriptive"] = make_compound_type("descriptive"),
["desc"] = "descriptive",
["determinative"] = make_compound_type("determinative"),
["det"] = "determinative",
["dvandva"] = make_compound_type("dvandva"),
["dva"] = "dvandva",
["dvigu"] = make_compound_type("dvigu"),
["dvi"] = "dvigu",
["endocentric"] = make_compound_type("endocentric"),
["endo"] = "endocentric",
["exocentric"] = make_compound_type("exocentric"),
["exo"] = "exocentric",
["izafet I"] = make_compound_type("izafet I"),
["iz1"] = "izafet I",
["izafet II"] = make_compound_type("izafet II"),
["iz2"] = "izafet II",
["izafet III"] = make_compound_type("izafet III"),
["iz3"] = "izafet III",
["karmadharaya"] = make_compound_type("karmadharaya", "karmadhāraya"),
["karma"] = "karmadharaya",
["kd"] = "karmadharaya",
["kenning"] = make_raw_compound_type("kenning"),
["ken"] = "kenning",
["rhyming"] = make_non_glossary_compound_type("rhyming"),
["rhy"] = "rhyming",
["synonymous"] = make_non_glossary_compound_type("synonymous"),
["syn"] = "synonymous",
["tatpurusa"] = make_compound_type("tatpurusa", "tatpuruṣa"),
["tat"] = "tatpurusa",
["tp"] = "tatpurusa",
}
local function process_etymology_type(typ, nocap, notext, has_parts)
local text_sections = {}
local categories = {}
local borrowing_type
if typ then
local typdata = export.etymology_types[typ]
if type(typdata) == "string" then
typdata = export.etymology_types[typdata]
end
if not typdata then
error("Internal error: Unrecognized type '" .. typ .. "'")
end
local text = typdata.text
if not nocap then
text = ucfirst(text)
end
local cat = typdata.cat
borrowing_type = typdata.borrowing_type
local oftext = typdata.oftext or " of"
if not notext then
table.insert(text_sections, text)
if has_parts then
table.insert(text_sections, oftext)
table.insert(text_sections, " ")
end
end
if cat then
table.insert(categories, cat)
end
end
return text_sections, categories, borrowing_type
end
-----------------------------------------------------------------------------------------
-- Utility functions --
-----------------------------------------------------------------------------------------
-- Iterate an array up to the greatest integer index found.
local function ipairs_with_gaps(t)
local indices = m_table.numKeys(t)
local max_index = #indices > 0 and math.max(unpack(indices)) or 0
local i = 0
return function()
if i < max_index then
i = i + 1
return i, t[i]
end
end
end
export.ipairs_with_gaps = ipairs_with_gaps
--[==[
Join formatted parts (in `parts_formatted`) together with any overall {{para|lit}} spec (in `lit`) plus categories,
which are formatted by prepending the language name as found in `lang`. The value of an entry in `categories` can be
either a string (which is formatted using `sort_key`) or a table of the form `{ {cat=<var>category</var>,
sort_key=<var>sort_key</var>, sort_base=<var>sort_base</var>}`, specifying the sort key and sort base to use when
formatting the category. If `nocat` is given, no categories are added; otherwise, `force_cat` causes categories to be
added even on userspace pages.
]==]
function export.join_formatted_parts(data)
local cattext
local lang = data.data.lang
local force_cat = data.data.force_cat or debug_force_cat
if data.data.nocat then
cattext = ""
else
for i, cat in ipairs(data.categories) do
if type(cat) == "table" then
data.categories[i] = require(utilities_module).format_categories(cat .. " bahasa " .. lang:getFullName().cat,
lang, cat.sort_key, cat.sort_base, force_cat)
else
data.categories[i] = require(utilities_module).format_categories(cat .. " bahasa " .. lang:getFullName(), lang,
data.data.sort_key, nil, force_cat)
end
end
cattext = table.concat(data.categories)
end
local result = table.concat(data.parts_formatted, not data.separator_already_added and " +‎ " or nil) ..
(data.data.lit and ", secara harfiah " .. m_links.mark(data.data.lit, "gloss") or "")
local q = data.data.q
local qq = data.data.qq
local l = data.data.l
local ll = data.data.ll
local infl = data.data.infl
if q and q[1] or qq and qq[1] or l and l[1] or ll and ll[1] or infl and infl[1] then
result = require(pron_qualifier_module).format_qualifiers {
lang = lang,
text = result,
q = q,
qq = qq,
l = l,
ll = ll,
infl = infl,
}
end
return result .. cattext
end
-- Remove links and call lang:stripDiacritics(term).
local function strip_diacritics_no_links(lang, term)
return lang:stripDiacritics(m_links.remove_links(term))
end
--[=[
Convert a raw part as passed into an entry point into a part ready for linking. `lang` and `sc` are the overall
language and script objects. This uses the overall language and script objects as defaults for the part and parses off
any fragment from the term. We need to do the latter so that fragments don't end up in categories and so that we
correctly do affix mapping even in the presence of fragments.
]=]
local function canonicalize_part(part, lang, sc)
if not part then
return
end
-- Save the original (user-specified, part-specific) value of `lang`. If such a value is specified, we don't insert
-- a '*fixed with' category, and we format the part using format_derived() in [[Module:etymology]] rather than
-- full_link() in [[Module:links]].
part.part_lang = part.lang
part.lang = part.lang or lang
part.sc = part.sc or sc
local term = part.term
if not term then
return
elseif not part.fragment then
part.term, part.fragment = m_links.get_fragment(term)
else
part.term = m_links.get_fragment(term)
end
end
--[==[
Construct a single linked part based on the information in `part`, for use by `show_affix()` and other entry points.
This should be called after `canonicalize_part()` is called on the part. This is a thin wrapper around `full_link()` in
[[Module:links]] unless `part.part_lang` is specified (indicating that a part-specific language was given), in which
case `format_derived()` in [[Module:etymology]] is called to display a term in a language other than the language of
the overall term (specified in `data.lang`). `data` contains the entire object passed into the entry point and is used
to access information for constructing the categories added by `format_derived()`.
]==]
function export.link_term(part, data, include_separator)
local result
if part.part_lang then
result = require(etymology_module).format_derived {
lang = data.lang,
terms = {part},
sources = {part.lang},
sort_key = data.sort_key,
nocat = data.nocat,
template_name = "affix",
qualifiers_labels_on_outside = true,
borrowing_type = data.borrowing_type,
force_cat = data.force_cat or debug_force_cat,
}
else
result = m_links.full_link(part, "perkataan", nil, "show qualifiers")
end
if include_separator and part.separator then
return part.separator .. result
else
return result
end
end
local function canonicalize_script_code(scode)
-- Convert 'mnc-Mong', 'xwo-Mong' etc. to 'Mong'.
return (scode:gsub("^.*%-", ""))
end
-----------------------------------------------------------------------------------------
-- Affix-handling functions --
-----------------------------------------------------------------------------------------
-- Figure out the appropriate script for the given affix and language (unless the script is explicitly passed in), and
-- return the values of template_hyphens[], display_hyphens[] and lookup_hyphens[] for that script, substituting
-- default values as appropriate. Four values are returned:
-- DETECTED_SCRIPT, TEMPLATE_HYPHEN, DISPLAY_HYPHEN, LOOKUP_HYPHEN
local function detect_script_and_hyphens(text, lang, sc)
local scode
-- 1. If the script is explicitly passed in, use it.
if sc then
scode = sc:getCode()
else
local possible_script_codes = lang:getScriptCodes()
-- YUCK! `possible_script_codes` comes from loadData() so #possible_scripts doesn't work (always returns 0).
local num_possible_script_codes = m_table.length(possible_script_codes)
if num_possible_script_codes == 0 then
-- This shouldn't happen; if the language has no script codes,
-- the list {"None"} should be returned.
error("Something is majorly wrong! Language " .. lang:getCanonicalName() .. " has no script codes.")
end
if num_possible_script_codes == 1 then
-- 2. If the language has only one possible script, use it.
scode = possible_script_codes[1]
else
-- 3. Check if any of the possible scripts for the language have non-default values for template_hyphens[]
-- or display_hyphens[]. If so, we need to do script detection on the text. If not, just use "Latn",
-- which may not be technically correct but produces the right results because Latn has all default
-- values for template_hyphens[] and display_hyphens[].
local may_have_nondefault_hyphen = false
for _, script_code in ipairs(possible_script_codes) do
script_code = canonicalize_script_code(script_code)
if template_hyphens[script_code] or display_hyphens[script_code] then
may_have_nondefault_hyphen = true
break
end
end
if not may_have_nondefault_hyphen then
scode = "Latn"
else
scode = lang:findBestScript(text):getCode()
end
end
end
scode = canonicalize_script_code(scode)
local template_hyphen = template_hyphens[scode] or "-"
local lookup_hyphen = lookup_hyphens[scode] or "-"
local display_hyphen = display_hyphens[scode] or default_display_hyphen
return scode, template_hyphen, display_hyphen, lookup_hyphen
end
--[=[
Given a template affix `term` and an affix type `affix_type`, change the relevant template hyphen(s) in the affix to
the display or lookup hyphen specified in `new_hyphen`, or add them if they are missing. `new_hyphen` can be a string,
specifying a fixed hyphen, or a function of two arguments (the script code `scode` and the discovered template hyphen,
or nil of no relevant template hyphen is present). `thyph_re` is a Lua pattern (which must be enclosed in parens) that
matches the possible template hyphens. Note that not all template hyphens present in the affix are changed, but only
the "relevant" ones (e.g. for a prefix, a relevant template hyphen is one coming at the end of the affix).
]=]
local function reconstruct_term_per_hyphens(term, affix_type, scode, thyph_re, new_hyphen)
local function get_hyphen(hyph)
if type(new_hyphen) == "string" then
return new_hyphen
end
return new_hyphen(scode, hyph)
end
if affix_type == "non-affix" then
return term
elseif affix_type == "apitan" then
local before, before_hyphen, after_hyphen, after = rmatch(term, "^(.*)" .. thyph_re .. " " .. thyph_re
.. "(.*)$")
if not before or ulen(term) <= 3 then
-- Unlike with other types of affixes, don't try to add hyphens in the middle of the term to convert it to
-- a circumfix. Also, if the term is just hyphen + space + hyphen, return it.
return term
end
return before .. get_hyphen(before_hyphen) .. " " .. get_hyphen(after_hyphen) .. after
elseif affix_type == "sisipan" or affix_type == "jalinan" then
local before_hyphen, middle, after_hyphen = rmatch(term, "^" .. thyph_re .. "(.*)" .. thyph_re .. "$")
if before_hyphen and ulen(term) <= 1 then
-- If the term is just a hyphen, return it.
return term
end
return get_hyphen(before_hyphen) .. (middle or term) .. get_hyphen(after_hyphen)
elseif affix_type == "awalan" then
local middle, after_hyphen = rmatch(term, "^(.*)" .. thyph_re .. "$")
if middle and ulen(term) <= 1 then
-- If the term is just a hyphen, return it.
return term
end
return (middle or term) .. get_hyphen(after_hyphen)
elseif affix_type == "akhiran" then
local before_hyphen, middle = rmatch(term, "^" .. thyph_re .. "(.*)$")
if before_hyphen and ulen(term) <= 1 then
-- If the term is just a hyphen, return it.
return term
end
return get_hyphen(before_hyphen) .. (middle or term)
else
error(("Internal error: Unrecognized affix type '%s'"):format(affix_type))
end
end
--[=[
Look up a mapping from a given affix variant to the canonical form used in categories and links. The lookup tables are
language-specific according to `lang`, and may be ID-specific according to `affix_id`. The affixes as they appear in the
lookup tables (both the variant and the canonical form) are in "lookup affix" format (approximately speaking, they use a
regular hyphen for most scripts, but a tatweel for Arabic-script entries and a maqqef for Hebrew-script entries), but
the passed-in `affix` param is in "template affix" format (which differs from the lookup affix for Arabic-script
entries, because more types of hyphens are allowed in template affixes; see the comments at the top of the file). The
remaining parameters to this function are used to convert from template affixes to lookup affixes; see the
reconstruct_term_per_hyphens() function above.
If the affix contains brackets, no lookup is done. Otherwise, a two-stage process is used, first looking up the affix
directly and then stripping diacritics and looking it up again. The reason for this is documented above in the comments
at the top of the file (specifically, the comments describing lookup affixes).
The value of a mapping can either be a string (do the mapping regardless of affix ID) or a table indexed by affix ID
(where the special value `false` indicates no affix ID). The values of entries in this table can also be strings, or
tables with keys `affix` and `id` (again, use `false` to indicate no ID). This allows an affix mapping to map from one
ID to another (for example, this is used in English to map the [[an-]] prefix with no ID to the [[a-]] prefix with the
ID 'not').
The Given a template affix `term` and an affix type `affix_type`, change the relevant template hyphen(s) in the affix to
the display or lookup hyphen specified in `new_hyphen`, or add them if they are missing. `new_hyphen` can be a string,
specifying a fixed hyphen, or a function of two arguments (the script code `scode` and the discovered template hyphen,
or nil of no relevant template hyphen is present). `thyph_re` is a Lua pattern (which must be enclosed in parens) that
matches the possible template hyphens. Note that not all template hyphens present in the affix are changed, but only
the "relevant" ones (e.g. for a prefix, a relevant template hyphen is one coming at the end of the affix).
]=]
local function lookup_affix_mapping(affix, affix_type, lang, scode, thyph_re, lookup_hyph, affix_id)
local function do_lookup(afx)
-- Ensure that the affix uses lookup hyphens regardless of whether it used a different type of hyphens before
-- or no hyphens.
local lookup_affix = reconstruct_term_per_hyphens(afx, affix_type, scode, thyph_re, lookup_hyph)
local function do_lookup_for_langcode(langcode)
if export.langs_with_lang_specific_data[langcode] then
local langdata = mw.loadData(export.affix_lang_data_module_prefix .. langcode)
if langdata.affix_mappings then
local mapping = langdata.affix_mappings[lookup_affix]
if mapping then
if type(mapping) == "table" then
mapping = mapping[affix_id] or mapping.default or mapping[affix_id or false]
if mapping then
return mapping
end
else
return mapping
end
end
end
end
end
-- If `lang` is an etymology-only language, look for a mapping both for it and its full parent.
local langcode = lang:getCode()
local mapping = do_lookup_for_langcode(langcode)
if mapping then
return mapping
end
local full_langcode = lang:getFullCode()
if full_langcode ~= langcode then
mapping = do_lookup_for_langcode(full_langcode)
if mapping then
return mapping
end
end
return nil
end
if affix:find("%[%[") then
return nil
end
return do_lookup(affix) or do_lookup(lang:stripDiacritics(affix)) or nil
end
--[==[
For a given template term in a given language (see the definition of "template affix" near the top of the file),
possibly in an explicitly specified script `sc` (but usually nil), return the term's affix type ({"awalan"},
{"jalinan"}, {"akhiran"}, {"apitan"} or {"non-affix"}) along with the corresponding link and display affixes
(see definitions near the top of the file); also the corresponding lookup affix (if `return_lookup_affix` is specified).
The term passed in should already have any fragment (after the # sign) parsed off of it. Four values are returned:
`affix_type`, `link_term`, `display_term` and `lookup_term`. The affix type can be passed in instead of autodetected; in
this case, the template term need not have any attached hyphens, and the appropriate hyphens will be added in the
appropriate places. If `do_affix_mapping` is specified, look up the affix in the lang-specific affix mappings, as
described in the comment at the top of the file; otherwise, the link and display terms will always be the same. (They
will be the same in any case if the template term has a bracketed link in it or is not an affix.) If
`return_lookup_affix` is given, the fourth return value contains the term with appropriate lookup hyphens in the
appropriate places; otherwise, it is the same as the display term. (This functionality is used in
[[Module:category tree/affixes and compounds]] to convert link affixes into lookup affixes so that they can be looked up
in the affix mapping tables.)
Exported because used by [[Module:headword utilities]] to determine the affix type of a given pagename.
]==]
function export.parse_term_for_affixes(term, lang, sc, affix_type, do_affix_mapping, return_lookup_affix, affix_id)
if not term then
return "non-affix", nil, nil, nil
end
if term == "^" then
-- Indicates a null term to emulate the behavior of {{suffix|foo||bar}}.
term = ""
return "non-affix", term, term, term
end
if term:find("^%^") then
-- HACK! ^ at the beginning of Korean languages has a special meaning, triggering capitalization of the
-- transliteration. Don't interpret it as "force non-affix" for those languages.
local langcode = lang:getCode()
if langcode ~= "ko" and langcode ~= "okm" and langcode ~= "jje" then
-- Formerly we allowed ^ to force non-affix type; this is now handled using an inline modifier
-- <naf>, <root>, etc. Throw an error for the moment when the old way is encountered.
error("Use of ^ to force non-affix status is no longer supported; use an inline modifier <naf> or <root> " ..
"after the component")
end
end
-- Remove an asterisk if the morpheme is reconstructed and add it back at the end.
local reconstructed = ""
if term:find("^%*") then
reconstructed = "*"
term = term:gsub("^%*", "")
end
local scode, thyph, dhyph, lhyph = detect_script_and_hyphens(term, lang, sc)
thyph = "([" .. thyph .. "])"
if not affix_type then
if rfind(term, thyph .. " " .. thyph) then
affix_type = "apitan"
else
local has_beginning_hyphen = rfind(term, "^" .. thyph)
local has_ending_hyphen = rfind(term, thyph .. "$")
if has_beginning_hyphen and has_ending_hyphen then
affix_type = "jalinan"
elseif has_ending_hyphen then
affix_type = "awalan"
elseif has_beginning_hyphen then
affix_type = "akhiran"
else
affix_type = "non-affix"
end
end
end
local link_term, display_term, lookup_term
if affix_type == "non-affix" then
link_term = term
display_term = term
lookup_term = term
else
display_term = reconstruct_term_per_hyphens(term, affix_type, scode, thyph, dhyph)
if do_affix_mapping then
link_term = lookup_affix_mapping(term, affix_type, lang, scode, thyph, lhyph, affix_id)
-- The return value of lookup_affix_mapping() may be an affix mapping with lookup hyphens if a mapping
-- was found, otherwise nil if a mapping was not found. We need to convert to display hyphens in
-- either case, but in the latter case we can reuse the display term, which has already been converted.
if link_term then
link_term = reconstruct_term_per_hyphens(link_term, affix_type, scode, thyph, dhyph)
else
link_term = display_term
end
else
link_term = display_term
end
if return_lookup_affix then
lookup_term = reconstruct_term_per_hyphens(term, affix_type, scode, thyph, lhyph)
else
lookup_term = display_term
end
end
link_term = reconstructed .. link_term
display_term = reconstructed .. display_term
lookup_term = reconstructed .. lookup_term
return affix_type, link_term, display_term, lookup_term
end
--[==[
Add a hyphen to a term in the appropriate place, based on the specified affix type, stripping off any existing hyphens
in that place. For example, if `affix_type` == {"awalan"}, we'll add a hyphen onto the end if it's not already there (or
is of the wrong type). Three values are returned: the link term, display term and lookup term. This function is a thin
wrapper around `parse_term_for_affixes`; see the comments above that function for more information. Note that this
function is exposed externally because it is called by [[Module:category tree/affixes and compounds]]; see the comment
in `parse_term_for_affixes` for more information.
]==]
function export.make_affix(term, lang, sc, affix_type, do_affix_mapping, return_lookup_affix, affix_id)
if not (affix_type == "awalan" or affix_type == "akhiran" or affix_type == "apitan" or affix_type == "sisipan" or
affix_type == "jalinan" or affix_type == "non-affix") then
error("Internal error: Invalid affix type " .. (affix_type or "(nil)"))
end
local _, link_term, display_term, lookup_term = export.parse_term_for_affixes(term, lang, sc, affix_type,
do_affix_mapping, return_lookup_affix, affix_id)
return link_term, display_term, lookup_term
end
-----------------------------------------------------------------------------------------
-- Main entry points --
-----------------------------------------------------------------------------------------
--[==[
Core categorization logic for affixes. This is shared between show_affix(), show_compound_like() and
get_affix_categories_only(). Returns the categories array and other metadata needed for formatting.
]==]
local function generate_affix_categories(data)
data.pos = data.pos or default_pos
data.pos = pluralize(data.pos)
local text_sections, categories, borrowing_type =
process_etymology_type(data.type, data.surface_analysis or data.nocap, data.notext, #data.parts > 0)
data.borrowing_type = borrowing_type
-- Process each part
local whole_words = 0
local is_affix_or_compound = false
-- Canonicalize and generate links for all the parts first; then do categorization in a separate step, because when
-- processing the first part for categorization, we may access the second part and need it already canonicalized.
for i, part in ipairs_with_gaps(data.parts) do
part = part or {}
data.parts[i] = part
canonicalize_part(part, data.lang, data.sc)
-- Determine affix type and get link and display terms (see text at top of file). Store them in the part
-- (in fields that won't clash with fields used by full_link() in [[Module:links]] or link_term()), so they
-- can be used in the loop below when categorizing.
part.affix_type, part.affix_link_term, part.affix_display_term = export.parse_term_for_affixes(part.term,
part.lang, part.sc, part.type, not part.alt, nil, part.id)
-- If link_term is an empty string, either a bare ^ was specified or an empty term was used along with inline
-- modifiers. The intention in either case is not to link the term.
part.term = ine(part.affix_link_term)
-- If part.alt would be the same as part.term, make it nil, so that it isn't erroneously tracked as being
-- redundant alt text.
part.alt = part.alt or (part.affix_display_term ~= part.affix_link_term and part.affix_display_term) or nil
end
if not data.noaffixcat then
-- Now do categorization.
for i, part in ipairs_with_gaps(data.parts) do
local affix_type = part.affix_type
if affix_type ~= "non-affix" then
is_affix_or_compound = true
-- Make a sort key. For the first part, use the second part as the sort key; the intention is that if the
-- term has a prefix, sorting by the prefix won't be very useful so we sort by what follows, which is
-- presumably the root.
local part_sort_base = nil
local part_sort = part.sort or data.sort_key
if i == 1 and data.parts[2] and data.parts[2].term then
local part2 = data.parts[2]
-- If the second-part link term is empty, the user requested an unlinked term; avoid a wikitext error
-- by using the alt value if available.
part_sort_base = ine(part2.affix_link_term) or ine(part2.alt)
if part_sort_base then
part_sort_base = strip_diacritics_no_links(part2.lang, part_sort_base)
end
end
if part.pos and rfind(part.pos, "patronym") then
table.insert(categories, {cat = "patronim", sort_key = part_sort, sort_base = part_sort_base})
end
if data.pos ~= "terms" and part.pos and rfind(part.pos, "diminutive") then
table.insert(categories, {cat = data.pos .. " diminutif", sort_key = part_sort,
sort_base = part_sort_base})
end
-- Don't add a '*fixed with' category if the link term is empty or is in a different language.
if ine(part.affix_link_term) and not part.part_lang then
table.insert(categories, {cat = data.pos .. " ber" .. affix_type .. " dengan " ..
strip_diacritics_no_links(part.lang, part.affix_link_term) ..
(part.id and " (" .. part.id .. ")" or ""),
sort_key = part_sort, sort_base = part_sort_base})
end
else
whole_words = whole_words + 1
if whole_words == 2 then
is_affix_or_compound = true
table.insert(categories, "majmuk " .. data.pos)
end
end
end
-- Make sure there was either an affix or a compound (two or more non-affix terms).
if not is_affix_or_compound and not data.allow_no_affixes_or_compounds then
error("The parameters did not include any affixes, and the term is not a compound. Please provide at least one affix.")
end
end
return text_sections, categories, borrowing_type
end
--[==[
Implementation of {{tl|affix}} and {{tl|surface analysis}}. `data` contains all the information describing the affixes to
be displayed, and contains the following:
* `.lang` ('''required'''): Overall language object. Different from term-specific language objects (see `.parts` below).
* `.sc`: Overall script object (usually omitted). Different from term-specific script objects.
* `.parts` ('''required'''): List of objects describing the affixes to show. The general format of each object is as would
be passed to `full_link()`, except that the `.lang` field should be missing unless the term is of a language
different from the overall `.lang` value (in such a case, the language name is shown along with the term and
an additional "derived from" category is added). '''WARNING''': The data in `.parts` will be destructively
modified.
* `.pos`: Overall part of speech (used in categories, defaults to {"terms"}). Different from term-specific part of speech.
* `.sort_key`: Overall sort key. Normally omitted except e.g. in Japanese.
* `.type`: Type of compound, if the parts in `.parts` describe a compound. Strictly optional, and if supplied, the
compound type is displayed before the parts (normally capitalized, unless `.nocap` is given).
* `.nocap`: Don't capitalize the first letter of text displayed before the parts (relevant only if `.type` or
`.surface_analysis` is given).
* `.notext`: Don't display any text before the parts (relevant only if `.type` or `.surface_analysis` is given).
* `.nocat`: Disable all categorization.
* `.noaffixcat`: Disable affix (and compound) categorization. Relevant for e.g. blends, which may otherwise
be incorrectly categorized as compound terms.
* `.lit`: Overall literal definition. Different from term-specific literal definitions.
* `.force_cat`: Always display categories, even on userspace pages.
* `.surface_analysis`: Implement {{surface analysis}}; adds `By surface analysis, ` before the parts.
'''WARNING''': This destructively modifies both `data` and the individual structures within `.parts`.
]==]
function export.show_affix(data)
local text_sections, categories, _ = generate_affix_categories(data)
-- Process each part for display
local parts_formatted = {}
for i, part in ipairs_with_gaps(data.parts) do
-- Make a link for the part
table.insert(parts_formatted, export.link_term(part, data, "include_separator"))
end
if data.surface_analysis then
local text = "dengan " .. glossary_link("surface analysis") .. ", "
if not data.nocap then
text = ucfirst(text)
end
table.insert(text_sections, 1, text)
end
table.insert(text_sections, export.join_formatted_parts { data = data, parts_formatted = parts_formatted,
categories = categories, separator_already_added = true })
return table.concat(text_sections)
end
--[==[
Get only the categories that would be generated by show_affix(), without any text output or formatting.
This is used by Module:etymon to get affix categorization.
Returns an array of category objects, where
each entry is either a string (simple category name) or a table with keys `cat`, `sort_key`,
and `sort_base` for more complex categorization.
`data` should have the same structure as passed to show_affix():
* `.lang` (required): Overall language object
* `.parts` (required): Array of affix part objects with `.term`, `.lang`, `.id`, etc.
* `.pos`: Part of speech (defaults to "terms")
* `.sort_key`: Overall sort key for categories
'''WARNING''': This destructively modifies both `data` and the individual structures within `.parts`.
]==]
function export.get_affix_categories_only(data)
local _, categories, _ = generate_affix_categories(data)
return categories
end
function export.show_surface_analysis(data)
data.surface_analysis = true
data.allow_no_affixes_or_compounds = true
return export.show_affix(data)
end
--[==[
Implementation of {{tl|compound}}.
'''WARNING''': This destructively modifies both `data` and the individual structures within `.parts`.
]==]
function export.show_compound(data)
data.pos = data.pos or default_pos
data.pos = pluralize(data.pos)
local text_sections, categories, borrowing_type =
process_etymology_type(data.type, data.nocap, data.notext, #data.parts > 0)
data.borrowing_type = borrowing_type
local parts_formatted = {}
table.insert(categories, "majmuk " .. data.pos)
-- Make links out of all the parts
local whole_words = 0
for i, part in ipairs(data.parts) do
canonicalize_part(part, data.lang, data.sc)
-- Determine affix type and get link and display terms (see text at top of file).
local affix_type, link_term, display_term = export.parse_term_for_affixes(part.term, part.lang, part.sc,
part.type, not part.alt, nil, part.id)
-- If the term is an interfix or the type was explicitly given, recognize it as such (which means e.g. that we
-- will display the term without hyphens for East Asian languages). Otherwise, ignore the fact that it looks
-- like an affix and display as specified in the template (but pay attention to the detected affix type for
-- certain tracking purposes).
if affix_type == "jalinan" or (part.type and part.type ~= "non-affix") then
-- If link_term is an empty string, either a bare ^ was specified or an empty term was used along with
-- inline modifiers. The intention in either case is not to link the term. Don't add a '*fixed with'
-- category in this case, or if the term is in a different language.
-- If part.alt would be the same as part.term, make it nil, so that it isn't erroneously tracked as being
-- redundant alt text.
if link_term and link_term ~= "" and not part.part_lang then
table.insert(categories, {cat = data.pos .. " ber" .. affix_type .. " dengan " ..
strip_diacritics_no_links(part.lang, link_term), sort_key = part.sort or data.sort_key})
end
part.term = link_term ~= "" and link_term or nil
part.alt = part.alt or (display_term ~= link_term and display_term) or nil
else
if affix_type ~= "non-affix" then
local langcode = data.lang:getCode()
-- If `data.lang` is an etymology-only language, track both using its code and its full parent's code.
track { affix_type, affix_type .. "/lang/" .. langcode }
local full_langcode = data.lang:getFullCode()
if langcode ~= full_langcode then
track(affix_type .. "/lang/" .. full_langcode)
end
else
whole_words = whole_words + 1
end
end
table.insert(parts_formatted, export.link_term(part, data, "include_separator"))
end
if whole_words == 1 then
track("one whole word")
elseif whole_words == 0 then
track("looks like confix")
end
table.insert(text_sections, export.join_formatted_parts { data = data, parts_formatted = parts_formatted,
categories = categories, separator_already_added = true })
return table.concat(text_sections)
end
--[==[
Implementation of {{tl|blend}}, {{tl|univerbation}} and similar "compound-like" templates.
'''WARNING''': This destructively modifies both `data` and the individual structures within `.parts`.
]==]
function export.show_compound_like(data)
data.allow_no_affixes_or_compounds = true
local text_sections, categories, _ = generate_affix_categories(data)
if data.cat then
table.insert(categories, data.cat)
end
-- Process each part for display
local parts_formatted = {}
for i, part in ipairs_with_gaps(data.parts) do
-- Make a link for the part
table.insert(parts_formatted, export.link_term(part, data, "include_separator"))
end
if #data.parts > 0 and data.oftext then
table.insert(text_sections, 1, " " .. data.oftext .. " ")
end
if data.text then
table.insert(text_sections, 1, data.text)
end
table.insert(text_sections, export.join_formatted_parts { data = data, parts_formatted = parts_formatted,
categories = categories, separator_already_added = true })
return table.concat(text_sections)
end
--[==[
Make `part` (a structure holding information on an affix part) into an affix of type `affix_type`, and apply any
relevant affix mappings. For example, if the desired affix type is "akhiran", this will (in general) add a hyphen onto
the beginning of the term, alt, tr and ts components of the part if not already present. The hyphen that's added is the
"display hyphen" (see above) and may be script-specific. (In the case of East Asian scripts, the display hyphen is an
empty string whereas the template hyphen is the regular hyphen, meaning that any regular hyphen at the beginning of the
part will be effectively removed.) `lang` and `sc` hold overall language and script objects.
Note that this also applies any language-specific affix mappings, so that e.g. if the language is Finnish and the user
specified [[-käs]] in the affix and didn't specify an `.alt` value, `part.term` will contain [[-kas]] and `part.alt` will
contain [[-käs]].
This function is used by the "legacy" templates ({{tl|prefix}}, {{tl|suffix}}, {{tl|confix}}, etc.) where the nature of
the affix is specified by the template itself rather than auto-determined from the affix, as is the case with
{{tl|affix}}.
'''WARNING''': This destructively modifies `part`.
]==]
local function make_part_into_affix(part, lang, sc, affix_type)
canonicalize_part(part, lang, sc)
local link_term, display_term = export.make_affix(part.term, part.lang, part.sc, affix_type, not part.alt, nil, part.id)
part.term = link_term
-- When we don't specify `do_affix_mapping` to make_affix(), link and display terms (first and second retvals of
-- make_affix()) are the same.
-- If part.alt would be the same as part.term, make it nil, so that it isn't erroneously tracked as being
-- redundant alt text.
part.alt = part.alt and export.make_affix(part.alt, part.lang, part.sc, affix_type) or (display_term ~= link_term and display_term) or nil
local Latn = require(scripts_module).getByCode("Latn")
part.tr = export.make_affix(part.tr, part.lang, Latn, affix_type)
part.ts = export.make_affix(part.ts, part.lang, Latn, affix_type)
end
local function track_wrong_affix_type(template, part, expected_affix_type)
if part and not part.type then
local affix_type = export.parse_term_for_affixes(part.term, part.lang, part.sc)
if affix_type ~= expected_affix_type then
local part_name = expected_affix_type or "base"
local langcode = part.lang:getCode()
local full_langcode = part.lang:getFullCode()
require("Module:debug/track") {
template,
template .. "/" .. part_name,
template .. "/" .. part_name .. "/" .. (affix_type or "none"),
template .. "/" .. part_name .. "/" .. (affix_type or "none") .. "/lang/" .. langcode
}
-- If `part.lang` is an etymology-only language, track both using its code and its full parent's code.
if full_langcode ~= langcode then
require("Module:debug/track")(
template .. "/" .. part_name .. "/" .. (affix_type or "none") .. "/lang/" .. full_langcode
)
end
end
end
end
local function insert_affix_category(categories, pos, affix_type, part, sort_key, sort_base)
-- Don't add a '*fixed with' category if the link term is empty or is in a different language.
if part.term and not part.part_lang then
local cat = pos .. " ber" .. affix_type .. " dengan " .. strip_diacritics_no_links(part.lang, part.term) ..
(part.id and " (" .. part.id .. ")" or "")
if sort_key or sort_base then
table.insert(categories, {cat = cat, sort_key = sort_key, sort_base = sort_base})
else
table.insert(categories, cat)
end
end
end
--[==[
Implementation of {{tl|circumfix}}.
'''WARNING''': This destructively modifies both `data` and `.prefix`, `.base` and `.suffix`.
]==]
function export.show_circumfix(data)
data.pos = data.pos or default_pos
data.pos = pluralize(data.pos)
canonicalize_part(data.base, data.lang, data.sc)
-- Hyphenate the affixes and apply any affix mappings.
make_part_into_affix(data.prefix, data.lang, data.sc, "awalan")
make_part_into_affix(data.suffix, data.lang, data.sc, "akhiran")
track_wrong_affix_type("apitan", data.prefix, "awalan")
track_wrong_affix_type("apitan", data.base, nil)
track_wrong_affix_type("apitan", data.suffix, "akhiran")
-- Create circumfix term.
local circumfix = nil
if data.prefix.term and data.suffix.term then
circumfix = data.prefix.term .. " " .. data.suffix.term
data.prefix.alt = data.prefix.alt or data.prefix.term
data.suffix.alt = data.suffix.alt or data.suffix.term
data.prefix.term = circumfix
data.suffix.term = circumfix
end
-- Make links out of all the parts.
local parts_formatted = {}
local categories = {}
local sort_base
if data.base.term then
sort_base = strip_diacritics_no_links(data.base.lang, data.base.term)
end
table.insert(parts_formatted, export.link_term(data.prefix, data))
table.insert(parts_formatted, export.link_term(data.base, data))
table.insert(parts_formatted, export.link_term(data.suffix, data))
-- Insert the categories, but don't add a '*fixed with' category if the link term is in a different language.
if not data.prefix.part_lang then
table.insert(categories, {cat=data.pos .. " dengan apitan " .. strip_diacritics_no_links(data.prefix.lang,
circumfix), sort_key=data.sort_key, sort_base=sort_base})
end
return export.join_formatted_parts { data = data, parts_formatted = parts_formatted, categories = categories }
end
--[==[
Implementation of {{tl|confix}}.
'''WARNING''': This destructively modifies both `data` and `.prefix`, `.base` and `.suffix`.
]==]
function export.show_confix(data)
data.pos = data.pos or default_pos
data.pos = pluralize(data.pos)
canonicalize_part(data.base, data.lang, data.sc)
-- Hyphenate the affixes and apply any affix mappings.
make_part_into_affix(data.prefix, data.lang, data.sc, "awalan")
make_part_into_affix(data.suffix, data.lang, data.sc, "akhiran")
track_wrong_affix_type("confix", data.prefix, "awalan")
track_wrong_affix_type("confix", data.base, nil)
track_wrong_affix_type("confix", data.suffix, "akhiran")
-- Make links out of all the parts.
local parts_formatted = {}
local prefix_sort_base
if data.base and data.base.term then
prefix_sort_base = strip_diacritics_no_links(data.base.lang, data.base.term)
elseif data.suffix.term then
prefix_sort_base = strip_diacritics_no_links(data.suffix.lang, data.suffix.term)
end
-- Insert the categories and parts.
local categories = {}
table.insert(parts_formatted, export.link_term(data.prefix, data))
insert_affix_category(categories, data.pos, "awalan", data.prefix, data.sort_key, prefix_sort_base)
if data.base then
table.insert(parts_formatted, export.link_term(data.base, data))
end
table.insert(parts_formatted, export.link_term(data.suffix, data))
-- FIXME, should we be specifying a sort base here?
insert_affix_category(categories, data.pos, "akhiran", data.suffix)
return export.join_formatted_parts { data = data, parts_formatted = parts_formatted, categories = categories }
end
--[==[
Implementation of {{tl|infix}}.
'''WARNING''': This destructively modifies both `data` and `.base` and `.infix`.
]==]
function export.show_infix(data)
data.pos = data.pos or default_pos
data.pos = pluralize(data.pos)
canonicalize_part(data.base, data.lang, data.sc)
-- Hyphenate the affixes and apply any affix mappings.
make_part_into_affix(data.infix, data.lang, data.sc, "sisipan")
track_wrong_affix_type("sisipan", data.base, nil)
track_wrong_affix_type("sisipan", data.infix, "sisipan")
-- Make links out of all the parts.
local parts_formatted = {}
local categories = {}
table.insert(parts_formatted, export.link_term(data.base, data))
table.insert(parts_formatted, export.link_term(data.infix, data))
-- Insert the categories.
-- FIXME, should we be specifying a sort base here?
insert_affix_category(categories, data.pos, "sisipan", data.infix)
return export.join_formatted_parts { data = data, parts_formatted = parts_formatted, categories = categories }
end
--[==[
Implementation of {{tl|prefix}}.
'''WARNING''': This destructively modifies both `data` and the structures within `.prefixes`, as well as `.base`.
]==]
function export.show_prefix(data)
data.pos = data.pos or default_pos
data.pos = pluralize(data.pos)
canonicalize_part(data.base, data.lang, data.sc)
-- Hyphenate the affixes and apply any affix mappings.
for i, prefix in ipairs(data.prefixes) do
make_part_into_affix(prefix, data.lang, data.sc, "awalan")
end
for i, prefix in ipairs(data.prefixes) do
track_wrong_affix_type("awalan", prefix, "awalan")
end
track_wrong_affix_type("awalan", data.base, nil)
-- Make links out of all the parts.
local parts_formatted = {}
local first_sort_base = nil
local categories = {}
if data.prefixes[2] then
first_sort_base = ine(data.prefixes[2].term) or ine(data.prefixes[2].alt)
if first_sort_base then
first_sort_base = strip_diacritics_no_links(data.prefixes[2].lang, first_sort_base)
end
elseif data.base then
first_sort_base = ine(data.base.term) or ine(data.base.alt)
if first_sort_base then
first_sort_base = strip_diacritics_no_links(data.base.lang, first_sort_base)
end
end
for i, prefix in ipairs(data.prefixes) do
table.insert(parts_formatted, export.link_term(prefix, data))
insert_affix_category(categories, data.pos, "awalan", prefix, data.sort_key, i == 1 and first_sort_base or nil)
end
if data.base then
table.insert(parts_formatted, export.link_term(data.base, data))
else
table.insert(parts_formatted, "")
end
return export.join_formatted_parts { data = data, parts_formatted = parts_formatted, categories = categories }
end
--[==[
Implementation of {{tl|suffix}}.
'''WARNING''': This destructively modifies both `data` and the structures within `.suffixes`, as well as `.base`.
]==]
function export.show_suffix(data)
local categories = {}
data.pos = data.pos or default_pos
data.pos = pluralize(data.pos)
canonicalize_part(data.base, data.lang, data.sc)
-- Hyphenate the affixes and apply any affix mappings.
for i, suffix in ipairs(data.suffixes) do
make_part_into_affix(suffix, data.lang, data.sc, "akhiran")
end
track_wrong_affix_type("akhiran", data.base, nil)
for i, suffix in ipairs(data.suffixes) do
track_wrong_affix_type("akhiran", suffix, "akhiran")
end
-- Make links out of all the parts.
local parts_formatted = {}
if data.base then
table.insert(parts_formatted, export.link_term(data.base, data))
else
table.insert(parts_formatted, "")
end
for i, suffix in ipairs(data.suffixes) do
table.insert(parts_formatted, export.link_term(suffix, data))
end
-- Insert the categories.
for i, suffix in ipairs(data.suffixes) do
-- FIXME, should we be specifying a sort base here?
insert_affix_category(categories, data.pos, "akhiran", suffix)
if suffix.pos and rfind(suffix.pos, "patronym") then
table.insert(categories, "patronim")
end
end
return export.join_formatted_parts { data = data, parts_formatted = parts_formatted, categories = categories }
end
return export
s7kmw5wbuf92mag2dai5tc4752oaj51
Modul:en-utilities
828
57855
373466
229725
2026-09-10T08:43:53Z
SNN95
2113
kemaskini
373466
Scribunto
text/plain
local export = {}
local add_suffix -- Defined below.
local find = string.find
local is_regular_plural -- Defined below.
local match = string.match
local remove_possessive -- Defined below.
local reverse = string.reverse
local sub = string.sub
local toNFD = mw.ustring.toNFD
local ugsub = mw.ustring.gsub
local ulower = mw.ustring.lower
local umatch = mw.ustring.match
local usub = mw.ustring.sub
local uupper = mw.ustring.upper
local vowels = "aæᴀᴁɐɑɒ@eᴇǝⱻəɛɘɜɞɤiıɪɨᵻoøœᴏɶɔᴐɵuᴜʉᵾɯꟺʊʋʌyʏ"
local hyphens = "%-‐‑‒–—"
--[==[
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 diacritics
local function get_diacritics()
diacritics, get_diacritics = mw.loadData("Module:headword/data").page.comb_chars.diacritics_all .. "+", nil
return diacritics
end
-- Normalize a string, so that case and diacritics are ignored. By default, "gu"
-- and "qu" are normalized to "g" and "q", because they behave like consonants
-- under certain conditions (e.g. final "y" does not usually have the plural
-- "ies" after a vowel, but it's regular for "quy" to become "quies". The flag
-- `not_gu` prevents this happening to "gu", and is needed because terms ending
-- "-guy" are almost always compounds of "guy" (→ "guys").
local function normalize(str, followed_by, not_gu)
if not followed_by then
followed_by = ""
end
str = ugsub(toNFD(str) .. followed_by, "([" .. (not_gu and "" or "Gg") .. "Qq])u([".. vowels .. "])", "%1%2")
return ulower(ugsub(sub(str, 1, #str - #followed_by), diacritics or get_diacritics(), ""))
end
local function epenthetic_e_default(stem)
return sub(stem, -1) ~= "e"
end
local function epenthetic_e_for_s(stem, term)
-- If the stem is different, it must be from "y" → "i".
if stem ~= term then
return true
end
local final
if match(stem, "^[^\128-\255]*$") then
final = sub(stem, -1)
else
stem = ugsub(toNFD(stem), diacritics or get_diacritics(), "")
final = usub(stem, -1)
end
-- Epenthetic "e" is added after a sibilant or sibilant-affricate. The vast
-- majority of these are spelled "s", "x", "z", "ch" and "sh", but "dg"
-- (→ "dge") and "ß" (→ "ss") can be found in obsolete spellings, "shh" in
-- onomatopoeia, and "zh", "dj", "jj" (and more) in loanwords.
return (
final == "g" and sub(stem, -2, -2) == "d" or
final == "h" and match(stem, "[csz]h+$") or
final == "j" and umatch(stem, "[^" .. vowels .. "]j$") or
final == "s" or
final == "u" and umatch(stem, "%f[%w']u$") or
final == "x" or
final == "z" or
final == "ß"
)
end
function export.remove_possessive(stem)
return match(stem, "^(.*)'s$") or match(stem, "^(.*s)'$") or stem
end
remove_possessive = export.remove_possessive
local suffixes = {}
suffixes["'s"] = {
truncated = function(stem)
return sub(stem, -1) == "s" and "'" or "'s"
end,
}
suffixes["s.plural"] = {
final_y_is_i = true,
epenthetic_e = epenthetic_e_for_s,
modifies_possessive = true,
}
suffixes["s.verb"] = {
final_y_is_i = true,
final_consonant_is_doubled = true,
epenthetic_e = epenthetic_e_for_s
}
suffixes["ing"] = {
final_consonant_is_doubled = true,
remove_silent_e = true,
}
suffixes["d"] = {
final_y_is_i = true,
final_consonant_is_doubled = true,
epenthetic_e = epenthetic_e_default,
}
suffixes["dst"] = suffixes["d"]
suffixes["st.verb"] = suffixes["d"]
suffixes["th"] = suffixes["d"]
suffixes["n"] = {
final_y_is_i = true,
final_y_is_i_after_vowel = true,
final_guy_is_gui = true,
final_consonant_is_doubled = true,
-- No epenthetic "e" after an "e", or an "i", "r" or "w" preceded by a vowel.
epenthetic_e = function(stem)
return not (
sub(stem, -1) == "e" or
umatch(normalize(stem), "[" .. vowels .. "][irw]$")
)
end,
}
suffixes["r"] = {
final_y_is_i = true,
final_ey_is_i = true,
final_guy_is_gui = true,
final_consonant_is_doubled = true,
epenthetic_e = epenthetic_e_default
}
suffixes["st.superlative"] = suffixes["r"]
-- Returns the stem used for suffixes that sometimes convert final "y" into "i",
-- such as "-es" ("-ies"), e.g. "penny" → "penni" ("pennies"). If
-- `final_ey_is_i` is true, final "ey" may also be converted, e.g. "plaguey" →
-- "plagui"; this is needed for "-er" ("-ier") and "-est" ("-iest"). If `not_gu`
-- is true, then normalize() will be called with the `not_gu` flag (see there
-- for more info); this is true in most cases.
local function convert_final_y_to_i(str, not_gu, final_ey_is_i, final_y_is_i_after_vowel)
local final3 = usub(str, -3)
-- Special case: treat "eey" as "ee" + "y" (e.g. "treey" → "treeiest").
-- "oey" and "uey" are usually vowel + "ey", but examples of "oe" + "y" and
-- "ue" = "y" do also exist: compare "go" → "goey" → "goier" with "doe" →
-- "doey" → "doeier"; "flu" → "fluey" → "fluiest" and "flue" → "fluey" →
-- "flueiest" form a theoretically possible minimal pair.
if final3 == "eey" then
return sub(str, 1, -2) .. "i"
end
local final2 = usub(str, -2)
-- If `final_ey_is_i` is true, treat final "-ey" can also be reduced.
if final_ey_is_i and final2 == "ey" then
-- Remove "ey" to get the base stem.
local base_stem = sub(str, 1, -3)
-- Special case: allow final "-ey" ("potato-ey" → "potato-iest").
if umatch(final3, "[" .. hyphens .. "]ey") then
return base_stem .. "i"
end
-- Final "ey" becomes "i" iff the term is polysyllabic (e.g. not
-- "grey"). "ey" is common if the base stem ends in a vowel ("echo →
-- "echoey"), so the presence of a vowel anywhere in the base stem is
-- sufficient to deem it polysyllabic. ("echoey" → "echo" → "echoiest",
-- "beigey" → "beig" → "beigiest", but "grey" → "gr" → "greyest"). The
-- first "y" in "-yey" can be treated as a vowel as long as it's
-- preceded by something ("clayey" → "clay" → "clayiest", "cryey" →
-- "cry" → "cryiest", but "*yey" → "*y" → "*yeyest"), so it needs to be
-- treated as a special case.
local normalized = normalize(base_stem, "ey")
if sub(normalized, -1) == "y" then
if umatch(normalized, "[%w@][yY]$") then
return base_stem .. "i"
end
elseif umatch(normalized, "[" .. vowels .. "%d]%w*$") then
return base_stem .. "i"
end
-- Special cases:
-- Final "quy" ("soliloquy" → "soliloquies").
-- Final "guy" iff `not_gu` is false ("roguy" → "roguiest").
-- Final "y" after a vowel iff `final_y_is_i_after_vowel` is true ("slay" →
-- "slain").
-- Final "-y" ("bro-y" → "bro-iest"), accounting for hyphen variation.
elseif umatch(final2, "[" .. hyphens .. "]y") then
-- Replace final "y" with "i".
return sub(str, 1, -2) .. "i"
-- Otherwise, final "y" becomes "i" iff it's not preceded by a vowel
-- ("shy" → "shiest", "horsy" → "horsies", but "day" → "days", "coy" →
-- "coyest").
else
-- Remove "y" to get the base stem.
local base_stem = sub(str, 1, -2)
if umatch(normalize(base_stem, "y", not_gu), "[^%s%p" .. (final_y_is_i_after_vowel and "" or vowels) .. "]$") then
return base_stem .. "i"
end
end
return str
end
local function double_final_consonant(str, final)
local initial = umatch(normalize(sub(str, 1, -2), final), "^.*%f[^%z%s" .. hyphens .. "…]([%l%p]*)[" .. vowels .. "]$")
return initial and (
initial == "" or
initial == "y" or
match(initial, "^.[\128-\191]*$") and umatch(initial, "[^" .. vowels .. "]") or
umatch(initial, "^[^" .. vowels .. "]*%f[^%l]$")
) and (str .. final) or str
end
local function remove_silent_e(str)
local final2 = sub(str, -2)
if final2 == "ie" then
-- Replace "ie" with "y", unless it follows another "y" (e.g.
-- "spulyie" → "spulyieing").
return ugsub(str, "([^yY%s%p])ie$", "%1y")
end
local base_stem = sub(str, 1, -2)
-- Silent "e" occurs after "u" or a consonant (cluster) preceded by a vowel.
return (
final2 == "ue" or
umatch(normalize(base_stem, "e"), "[" .. vowels .. "][^" .. vowels .. "]+$")
) and base_stem or str
end
function export.add_suffix(term, suffix, pos)
local data, possessive = suffixes[suffix]
-- If modifies_possessive is set, check for and remove any possessive
-- suffix, which will be re-added again at the end.
if data.modifies_possessive then
local new = remove_possessive(term)
if new ~= term then
term, possessive = new, true
end
end
suffix = match(suffix, "^([^.]*)")
local final, stem = sub(term, -1)
-- Proper nouns don't have a final "y" changed to "i" (e.g. "the Gettys",
-- "the public Ivys").
if data.final_y_is_i and final == "y" and pos ~= "proper noun" then
stem = convert_final_y_to_i(term, not data.final_guy_is_gui, data.final_ey_is_i, data.final_y_is_i_after_vowel)
elseif data.remove_silent_e and final == "e" then
stem = remove_silent_e(term)
else
stem = term
end
local epenthetic_e = data.epenthetic_e
if epenthetic_e and epenthetic_e(stem, term) then
suffix = "e" .. suffix
end
if (
data.final_consonant_is_doubled and
match(final, "^[bcdfgjklmnpqrstvz]$") and -- Only double regular consonants.
umatch(suffix, "^[" .. vowels .. "]")
) then
stem = double_final_consonant(term, final)
end
local truncated = data.truncated
if truncated then
suffix = truncated(stem)
end
local output = stem .. suffix
-- Re-add the possessive suffix, if applicable.
if possessive then
output = add_suffix(output, "'s", pos)
end
return output
end
add_suffix = export.add_suffix
--[==[
Pluralize a word in a smart fashion, according to normal English rules.
# If the word ends in a consonant or "qu" + "-y", replace "-y" with "-ies".
# If the word ends in "s", "x", "z", "ch", "sh" or "zh", add "-es".
# Otherwise, add "-s".
This handles links correctly:
# If a piped link, change the second part appropriately.
# If a non-piped link and rule #1 above applies, convert to a piped link with the second part containing the plural.
# If a non-piped link and rules #2 or #3 above apply, add the plural outside the link.
]==]
function export.pluralize(str)
-- Treat as a link if a "[[" is present and the string ends with "]]".
if not (find(str, "[[", 1, true) and sub(str, -2) == "]]") then
return add_suffix(str, "s.plural")
end
-- Find the last "[[" (in case there is more than one) by reversing
-- the string.
local str_rev = reverse(str)
local open = find(str_rev, "[[", 3, true)
-- If the last "[[" is followed by a "]]" which isn't at the end,
-- then the final "]]" is just plaintext (e.g. "[[foo]]bar]]").
local bad_close = find(str_rev, "]]", 3, true)
-- Note: the bad "]]" will have a lower index than the last "[[" in
-- the reversed string.
if bad_close and bad_close < open then
return add_suffix(str, "s.plural")
end
open = #str - open + 2
-- Get the target and display text by searching from just after "[[".
local target, display = match(str, "([^|]*)|?(.*)%]%]$", open)
display = add_suffix(display ~= "" and display or target, "s.plural")
-- If the link target is a substring of the display text, then
-- use a trail (e.g. "[[foo]]" → "[[foo]]s", since "foo" is a substring
-- of "foos").
local index, trail = find(display, target, 1, true)
if index == 1 then
return sub(str, 1, open - 1) .. target .. "]]" .. sub(display, trail + 1)
end
-- Otherwise, return a piped link.
return sub(str, 1, open - 1) .. target .. "|" .. display .. "]]"
end
--[==[
Returns true if `plural` is an expected, regular plural of `term`.
The optional parameter `pos` can be used to specify the part of speech,
which is necessary because proper nouns do not change a {"-y"} suffix to {"-ies"}
(e.g. {"Abby"} → {"Abbys"}). By default, `pos` is set to {"noun"}. In addition to
{"proper noun"}, it can also take the special value {"noun+"}, which means that
the function will first attempt the check with the {"noun"} setting, and will
then attempt it with the {"proper noun"} setting iff the term begins with a
capital letter.
]==]
function export.is_regular_plural(plural, term, pos)
local init_plural, init_term, try_as_proper_noun = plural, term
if pos == "noun+" then
pos, try_as_proper_noun = "noun", true
end
-- Ignore any final punctuation that occurs in both forms, which is common
-- in abbreviations (e.g. "abbr." → "abbrs.").
local final_punc = umatch(term, "%p*$")
local final_punc_len = #final_punc
if sub(plural, -final_punc_len) == final_punc then
term = sub(term, 1, -final_punc_len - 1)
plural = sub(plural, 1, -final_punc_len - 1)
end
if plural == add_suffix(term, "s.plural", pos) then
return true
end
local final = sub(term, -1)
if (
-- Doubled final consonants in "s" and "z".
final == "s" and plural == term .. "ses" or -- e.g. "busses"
final == "z" and plural == term .. "zes" or -- e.g. "quizzes"
-- convert_final_y_to_i() without the `not_gu` flag set, to catch
-- "-guy" → "-guies", but not "day" → "daies".
final == "y" and plural == convert_final_y_to_i(term) .. "es" or
-- Capitalized terms like "$DEITY" → "$DEITIES (should we treat this as regular?)
final == "Y" and ulower(plural) == convert_final_y_to_i(ulower(term)) .. "es"
) then
return true
elseif try_as_proper_noun then
local init = umatch(init_term, "^[^%w%s]*(%w)")
return init and uupper(init) == init and ulower(init) ~= init and
is_regular_plural(init_plural, init_term, "proper noun") or
false
end
return false
end
is_regular_plural = export.is_regular_plural
do
local function do_singularize(str)
local sing = match(str, "^(.-)ies$")
if sing then
return sing .. "y"
end
-- Handle cases like "[[parish]]es"
return match(str, "^(.-[cs]h%]*)es$") or -- not -zhes
-- Handle cases like "[[box]]es"
match(str, "^(.-x%]*)es$") or -- not -ses or -zes
-- Handle regular plurals
match(str, "^(.-)s$") or
-- Otherwise, return input
str
end
local function collapse_link(link, linktext)
if link == linktext then
return "[[" .. link .. "]]"
end
return "[[" .. link .. "|" .. linktext .. "]]"
end
--[==[
Singularize a word in a smart fashion, according to normal English rules. Works analogously to {pluralize()}.
'''NOTE''': This doesn't always work as well as {pluralize()}. Beware. It will mishandle cases like "passes" -> "passe", "eyries" -> "eyry".
# If word ends in -ies, replace -ies with -y.
# If the word ends in -xes, -shes, -ches, remove -es. [Does not affect -ses, cf. "houses", "impasses".]
# Otherwise, remove -s.
This handles links correctly:
# If a piped link, change the second part appropriately. Collapse the link to a simple link if both parts end up the same.
# If a non-piped link, singularize the link.
# A link like "[[parish]]es" will be handled correctly because the code that checks for -shes etc. allows ] characters between the
'sh' etc. and final -es.
]==]
function export.singularize(str)
if type(str) == "table" then
-- allow calling from a template
str = str.args[1]
end
-- Check for a link. This pattern matches both piped and unpiped links.
-- If the link is not piped, the second capture (linktext) will be empty.
local beginning, link, linktext = match(str, "^(.*)%[%[([^|%]]+)%|?(.-)%]%]$")
if not link then
return do_singularize(str)
elseif linktext ~= "" then
return beginning .. collapse_link(link, do_singularize(linktext))
end
return beginning .. "[[" .. do_singularize(link) .. "]]"
end
end
--[==[
Return the appropriate indefinite article to prefix to `str`. Correctly handles links and capitalized text.
Does not correctly handle words like [[union]], [[uniform]] and [[university]] that take "a" despite beginning with
a 'u'. The returned article will have its first letter capitalized if `ucfirst` is specified, otherwise lowercase.
]==]
function export.get_indefinite_article(str, ucfirst)
str = str or ""
-- If there's a link at the beginning, examine the first letter of the
-- link text. This pattern matches both piped and unpiped links.
-- If the link is not piped, the second capture (linktext) will be empty.
local link, linktext = match(str, "^%[%[([^|%]]+)%|?(.-)%]%]")
if match(link and (linktext ~= "" and linktext or link) or str, "^()[AEIOUaeiou]") then
return ucfirst and "An" or "an"
end
return ucfirst and "A" or "a"
end
get_indefinite_article = export.get_indefinite_article
--[==[
Prefix `text` with the appropriate indefinite article to prefix to `text`. Correctly handles links and capitalized
text. Does not correctly handle words like [[union]], [[uniform]] and [[university]] that take "a" despite beginning
with a 'u'. The returned article will have its first letter capitalized if `ucfirst` is specified, otherwise lowercase.
]==]
function export.add_indefinite_article(text, ucfirst)
return get_indefinite_article(text, ucfirst) .. " " .. text
end
export.vowels = vowels
export.vowel = "[" .. vowels .. "]"
return export
qmuwy34gf3az49fu17xn4hzrc6y4prc
Modul:etymon
828
57903
373456
344230
2026-09-10T08:18:42Z
SNN95
2113
373456
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",
parse_utilities = "Module:parse utilities",
references = "Module:references",
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 = {
nocat = { type = "boolean" },
sort = {},
y = {},
m = {},
fragment = {},
section = {},
box = { type = "boolean" },
noes = { type = "boolean" },
},
etystub = {
nocat = { type = "boolean" },
sort = {},
nocap = { type = "boolean" },
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.t
end
if term.g and term.g ~= "" then
local genders = M.string_utilities.split(term.g, ",")
for i = 1, #genders do
genders[i] = M.string_utilities.trim(genders[i])
end
link_params.genders = genders
end
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
local show_qualifiers
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
show_qualifiers = term.q or term.qq or term.l or term.ll
end
return M.links.full_link(link_params, "term", nil, show_qualifiers and true or nil)
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 = {
unc = { type = "boolean" },
ref = {},
text = { restrict = { keywords = { "from", "derived" } } },
lit = { restrict = { affix_group = true } },
conj = {}, -- conjunction for alternatives: "and", "or", "and/or", etc.
g = { restrict = { keywords = { "nominalization" } } },
surf = { type = "boolean" },
senseid = { restrict = { keywords = { "semantic loan" } } },
}
-- Term modifier definitions
EtymonParser.etymon_param_mods = {
id = {},
t = {},
tr = {},
ts = {},
q = {},
qq = {},
l = {},
ll = {},
pos = {},
ng = {},
alt = {},
g = {},
infl = { type = "form of tags" },
ety = {},
lit = {},
unc = { type = "boolean" },
ref = {},
aftype = { restrict = { affix_group = true } },
postype = {},
bor = { type = "boolean", restrict = { affix_group = true } },
slbor = { type = "boolean", restrict = { affix_group = true } },
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,
ref = parsed.ref,
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_forms = 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 = {}
local function parse_etymon_references(refs_text)
if not refs_text or refs_text == "" then
return ""
end
return M.references.parse_references(refs_text)
end
local function parse_tree_references(node)
if node.ref then
node.parsed_ref = parse_etymon_references(node.ref)
end
if node.children then
for _, container in ipairs(node.children) do
if container.terms then
for _, term in ipairs(container.terms) do
parse_tree_references(term)
end
end
end
end
if node.supplements then
for _, supplement in ipairs(node.supplements) do
if supplement.terms then
for _, term in ipairs(supplement.terms) do
parse_tree_references(term)
end
end
end
end
end
-- 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.t = etymon_data.t
term.tr = etymon_data.tr
term.ts = etymon_data.ts
term.alt = etymon_data.alt
term.g = etymon_data.g
term.pos = etymon_data.pos
term.ng = etymon_data.ng
term.infl = etymon_data.infl
term.ref = etymon_data.ref
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.t,
transliteration = node.tr,
transcription = node.ts,
alt = node.alt,
g = node.g,
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)
parse_tree_references(ety_data_tree)
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 == "Reconstruction" then title = "*" .. title end
end
local entry_pagename = page_data.pagename
if page_data.namespace == "Reconstruction" 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 = {
ref = { list = true, type = "references", allow_holes = true },
refn = { list = true, allow_holes = true },
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.ref and #parsed_etydate.ref > 0) and parsed_etydate.ref or nil,
})
end
TreeBuilder.append_term_supplement(ety_data_tree, lang, "doublet", doublet)
if ety_data_tree.supplements then
parse_tree_references(ety_data_tree)
end
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
d0thspkud5zawi6og8iirurr2pt34q1
Modul:etymon/styles.css
828
57904
373467
234445
2026-09-10T08:47:50Z
SNN95
2113
373467
sanitized-css
text/css
/* Main container */
.etytree {
width: max-content;
max-width: 100%;
overflow: hidden;
box-sizing: border-box;
}
.etytree .NavHead {
background: var(--wikt-palette-lightergrey);
}
.etytree .NavHead > div {
width: 25em;
}
.etytree .NavContent {
overflow: auto;
}
.etytree-body {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5em;
margin: auto;
width: fit-content;
}
.etytree-branch-group {
display: flex;
column-gap: 0.5em;
align-items: end;
position: relative;
}
.etytree-branch {
display: flex;
flex-direction: column;
align-items: center;
}
/* Term blocks */
.etytree-block {
position: relative;
padding: 5px 10px;
border: 1px solid var(--wikt-palette-lightgrey, #ddd);
border-radius: 4px;
background: var(--wikt-palette-beige);
text-align: center;
}
/* Termless keyword blocks (invisible wrapper for positioning) */
.etytree-termless-block {
height: 0 !important;
padding: 0 !important;
border: none !important;
background: transparent !important;
margin: 0 !important;
overflow: visible;
}
.etytree-block.etytree-duplicate {
border: 1px dashed var(--wikt-palette-grey);
}
.etytree-term {
display: inline-block;
}
/* Vertical connectors */
.etytree-connector-vertical,
.etytree-connector-vertical-short {
border-right: 2px solid var(--wikt-palette-grey, #999);
position: relative;
transform: translateX(1px);
}
.etytree-connector-vertical { height: 20px; }
.etytree-connector-vertical-short { height: 10px; }
.etytree-connector-dotted {
border-left: 2px dotted var(--wikt-palette-grey);
height: 20px;
display: block;
margin-left: 50%;
}
/* Branch connectors */
.etytree-branch-left,
.etytree-branch-right {
height: 10px;
width: calc(50% + 0.25em + 1px);
border-bottom: 2px solid var(--wikt-palette-grey, #999);
}
.etytree-branch-left {
border-left: 2px solid var(--wikt-palette-grey, #999);
border-bottom-left-radius: 4px;
transform: translateX(calc(50% - 0.4px));
}
.etytree-branch-right {
border-right: 2px solid var(--wikt-palette-grey, #999);
border-bottom-right-radius: 4px;
transform: translateX(calc(-50% + 2.4px));
}
.etytree-branch-mid {
border-bottom: 2px solid var(--wikt-palette-grey, #999);
width: calc(100% + 0.5em);
}
/* Duplicate connector (L-shaped with arrow) */
.etytree-duplicate-connector {
display: flex;
justify-content: center;
height: 20px;
}
.etytree-duplicate-connector > div {
position: relative;
width: 60px;
height: 100%;
}
.etytree-duplicate-connector .etytree-dup-right {
position: absolute;
right: 0;
top: 50%;
bottom: 0;
border-left: 2px dotted var(--wikt-palette-grey);
}
.etytree-duplicate-connector .etytree-dup-horiz {
position: absolute;
left: 0;
right: 0;
top: 50%;
border-top: 2px dotted var(--wikt-palette-grey);
}
.etytree-duplicate-connector .etytree-dup-left {
position: absolute;
left: 0;
top: 0;
bottom: 50%;
border-left: 2px dotted var(--wikt-palette-grey);
}
.etytree-duplicate-connector .etytree-dup-arrow {
position: absolute;
left: -5px;
top: -5px;
font-size: 10px;
color: var(--wikt-palette-grey);
}
/* Label containers */
.etytree-label-container {
z-index: 1;
position: absolute;
transform: translate(-50%);
top: calc(100% + 5px);
left: 50%;
line-height: 10px;
overflow: visible;
}
.etytree-group-label {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
height: 10px;
line-height: 10px;
}
/* Labels */
.etytree-label abbr {
font-size: 12px;
font-style: italic;
color: var(--wikt-palette-black);
background: var(--wikt-palette-cyan, #fff);
border-radius: 2px;
text-decoration: none;
}
/* Uncertainty marker */
.etytree-unc {
font-size: 10px;
font-weight: bold;
padding: 1px 2px;
background: var(--wikt-palette-pink);
border-radius: 2px;
text-decoration: none;
}
.etytree-label + .etytree-unc {
position: absolute;
left: calc(100% + 3px);
}
/* Final marker (for termless keywords) */
.etytree-final {
font-size: 10px;
font-weight: bold;
padding: 1px 2px;
background: var(--wikt-palette-grey) !important;
color: var(--wikt-palette-white) !important;
border-radius: 2px;
text-decoration: none;
display: inline-block;
line-height: 1;
}
.etytree-label-container .etytree-final {
position: absolute;
left: 50%;
top: -12px;
transform: translateX(-50%);
z-index: 3;
margin-left: 0;
}
94pxy0d6ov1puaxbux7krmz6ntarty0
Modul:etymon/tree
828
82357
373459
342818
2026-09-10T08:22:21Z
SNN95
2113
373459
Scribunto
text/plain
local export = {}
local html_create = mw.html.create
local max = math.max
local function create_vertical_connector()
return html_create('span'):addClass('etytree-connector-vertical')
end
local function create_abbr(text, title, glossary)
local abbr = html_create('abbr')
:attr('title', title)
:wikitext(text)
if glossary then
abbr = '[[Lampiran:Glosari#' .. glossary .. '|' .. tostring(abbr) .. ']]'
end
return html_create('span'):addClass('etytree-label'):node(abbr)
end
local function create_uncertainty_marker()
return html_create('abbr')
:addClass('etytree-unc')
:attr('title', 'uncertain')
:wikitext('?')
end
local function create_label_container()
return html_create('span'):addClass('etytree-label-container')
end
local function invisible_in_tree(inv)
return inv == "all" or inv == true or inv == "tree"
end
local function render_label(term_block, keyword_info, keyword_modifiers, is_uncertain, is_group_child, term_labels)
-- Skip label when invisible in tree
local has_label = keyword_info and keyword_info.abbrev and not is_group_child and not invisible_in_tree(keyword_info.invisible)
-- For group children, keyword uncertainty is shown on the group label, not on individual terms
local keyword_uncertain = keyword_modifiers and keyword_modifiers.unc and not is_group_child
local show_term_uncertainty = is_uncertain
-- Check if we have term-specific labels
local has_term_labels = term_labels and #term_labels > 0
if not has_label and not show_term_uncertainty and not keyword_uncertain and not has_term_labels then
return
end
local label_span = create_label_container()
if has_label then
local glossary_title = keyword_info.glossary
and keyword_info.glossary:gsub("_", " ")
or keyword_info.abbrev
label_span:node(create_abbr(
keyword_info.abbrev,
glossary_title,
keyword_info.glossary
))
-- Show uncertainty marker if term or keyword is uncertain
if show_term_uncertainty or keyword_uncertain then
label_span:node(create_uncertainty_marker())
end
else
-- No label, but term or keyword is uncertain
if show_term_uncertainty or keyword_uncertain then
label_span:node(create_uncertainty_marker())
end
end
-- Add term-specific labels
if has_term_labels then
for _, label_info in ipairs(term_labels) do
label_span:node(create_abbr(label_info.abbrev, label_info.title, label_info.glossary))
end
end
term_block:node(label_span)
end
local function render_term_block(node_data, format_term_func, is_toplevel)
local link_content = html_create()
link_content
:tag('span')
:addClass('etyl')
:wikitext(node_data.lang:getCanonicalName())
:done()
local term_text = format_term_func(node_data, is_toplevel)
if term_text then
link_content
:wikitext(' ')
:tag('span')
:addClass('etytree-term')
:wikitext(term_text)
:done()
end
local block = html_create('div'):addClass('etytree-block'):node(link_content)
-- Add duplicate styling if this is a duplicate node
if node_data.is_duplicate then
block:addClass('etytree-duplicate')
end
return block
end
local function create_dotted_connector()
return html_create('span'):addClass('etytree-connector-dotted')
end
-- Create an L-shaped connector for nodes with hidden ancestry (duplicate or no_child_categories)
local function create_duplicate_connector()
local container = html_create('div'):addClass('etytree-duplicate-connector')
local inner_wrapper = container:tag('div')
inner_wrapper:tag('span'):addClass('etytree-dup-right')
inner_wrapper:tag('span'):addClass('etytree-dup-horiz')
inner_wrapper:tag('span'):addClass('etytree-dup-left')
inner_wrapper:tag('span'):addClass('etytree-dup-arrow'):wikitext('▲')
return container
end
local function render_group_label(connecting_line, keyword_info, keyword_modifiers)
local has_abbrev = keyword_info and keyword_info.abbrev
local keyword_uncertain = keyword_modifiers and keyword_modifiers.unc
-- Nothing to show if no abbrev and no uncertainty
if not has_abbrev and not keyword_uncertain then
return
end
local label_span = connecting_line:tag('span'):addClass('etytree-group-label')
if has_abbrev then
local glossary_title = keyword_info.glossary
and keyword_info.glossary:gsub("_", " ")
or keyword_info.abbrev
label_span:node(create_abbr(keyword_info.abbrev, glossary_title, nil))
end
-- Add uncertainty marker if keyword has <unc> modifier
if keyword_uncertain then
label_span:node(create_uncertainty_marker())
end
end
local function add_branch_connector(column, index, total)
if index == 1 then
column:tag('span'):addClass('etytree-branch-left')
elseif index == total then
column:tag('span'):addClass('etytree-branch-right')
else
column:tag('span'):addClass('etytree-connector-vertical-short')
column:tag('span'):addClass('etytree-branch-mid')
end
end
function export.render(opts)
opts = opts or {}
local data_tree = opts.data_tree
local format_term_func = opts.format_term_func
-- Forward declaration
local render_term
-- Render a container (keyword + its terms)
local function render_container(container, is_toplevel)
local keyword_info = container.keyword_info
local keyword_modifiers = container.keyword_modifiers or {}
local is_group = keyword_info and keyword_info.is_group
local terms = container.terms or {}
-- Skip container entirely only when invisible = "all" (or true)
if keyword_info and invisible_in_tree(keyword_info.invisible) then
return nil, 0, 0
end
if #terms == 0 then
return nil, 0, 0
end
-- For no_child_categories keywords (calque, semantic loan, etc.), don't render term's children
local skip_child_rendering = keyword_info and keyword_info.no_child_categories
-- Render each term in the container
local rendered_terms = {}
local container_height = 0
local container_width = 0
for _, term in ipairs(terms) do
-- Collect term-specific labels
local term_labels = {}
if term.bor then
table.insert(term_labels, { abbrev = "bor.", title = "borrowed", glossary = "loanword" })
end
if term.slbor then
table.insert(term_labels, { abbrev = "slbor.", title = "semi-learned borrowing", glossary = "semi-learned_borrowing" })
end
if term.lbor then
table.insert(term_labels, { abbrev = "lbor.", title = "learned borrowing", glossary = "learned_borrowing" })
end
local term_tree, term_height, term_width = render_term(term, keyword_info, keyword_modifiers, is_group, false, skip_child_rendering, term_labels)
table.insert(rendered_terms, {
tree = term_tree,
height = term_height,
width = term_width,
is_uncertain = term.is_uncertain,
})
container_height = max(container_height, term_height)
container_width = container_width + term_width
end
local rendered_html
local has_connector = false
if #rendered_terms == 1 then
-- Single term: just return it directly
rendered_html = rendered_terms[1].tree
container_height = rendered_terms[1].height
container_width = rendered_terms[1].width
else
-- Multiple terms: group them together
local subtree_container = html_create('div'):addClass('etytree-branch-group')
for i, term_data in ipairs(rendered_terms) do
local column = html_create('div'):addClass('etytree-branch')
column:node(term_data.tree)
add_branch_connector(column, i, #rendered_terms)
subtree_container:node(column)
end
local connecting_line = create_vertical_connector()
-- Add group label for group keywords
if is_group and not invisible_in_tree(keyword_info.invisible) then
render_group_label(connecting_line, keyword_info, keyword_modifiers)
end
rendered_html = html_create()
:node(subtree_container)
:node(connecting_line)
has_connector = true
end
return rendered_html, container_height, container_width, has_connector
end
-- Render a term node
render_term = function(term_node, keyword_info, keyword_modifiers, is_group_child, is_toplevel_term, skip_child_rendering, term_labels)
local tree_width, tree_height = 0, 0
local subtrees = {}
-- Process term's children (which are containers)
local has_hidden_children = false
if not term_node.is_duplicate and not skip_child_rendering then
for _, container in ipairs(term_node.children or {}) do
local subtree, sub_height, sub_width, subtree_has_connector = render_container(container, is_toplevel_term)
if subtree then
table.insert(subtrees, {
tree = subtree,
height = sub_height,
width = sub_width,
has_connector = subtree_has_connector,
})
tree_height = max(tree_height, sub_height)
tree_width = tree_width + sub_width
end
end
elseif skip_child_rendering then
-- Check if there are any visible children
-- When stop_recursion is true, children aren't parsed, but has_visible_children flag is set
if term_node.has_visible_children then
has_hidden_children = true
elseif term_node.children and #term_node.children > 0 then
-- Fallback: check parsed children for visibility
for _, container in ipairs(term_node.children) do
local child_keyword_info = container.keyword_info
if not (child_keyword_info and (child_keyword_info.invisible == "all" or child_keyword_info.invisible == true)) then
has_hidden_children = true
break
end
end
end
end
local is_toplevel_node = (keyword_info == nil)
local term_block = render_term_block(term_node, format_term_func, is_toplevel_node)
render_label(term_block, keyword_info, keyword_modifiers, term_node.is_uncertain, is_group_child, term_labels or {})
local term_html = html_create()
if #subtrees == 0 then
local show_connector = (term_node.is_duplicate and term_node.original_has_children) or has_hidden_children
if show_connector then
term_html:node(create_duplicate_connector())
end
term_html:node(term_block)
tree_width = tree_width + 1
elseif #subtrees == 1 then
term_html:node(subtrees[1].tree)
if not subtrees[1].has_connector then
term_html:node(create_vertical_connector())
end
term_html:node(term_block)
else
-- Multiple containers: need to merge them
local subtree_container = html_create('div'):addClass('etytree-branch-group')
for i, subtree_data in ipairs(subtrees) do
local column = html_create('div'):addClass('etytree-branch')
column:node(subtree_data.tree)
add_branch_connector(column, i, #subtrees)
subtree_container:node(column)
end
local connecting_line = create_vertical_connector()
term_html
:node(subtree_container)
:node(connecting_line)
:node(term_block)
end
return term_html, tree_height + 1, tree_width
end
local final_tree, final_height, final_width = render_term(data_tree, nil, nil, false, true)
local container = html_create('div')
:addClass('etytree-body')
:node(final_tree)
return tostring(html_create('div')
:addClass('etytree NavFrame')
:attr('data-etytree-height', final_height)
:attr('data-etytree-width', final_width)
:tag('div')
:addClass('NavHead')
:tag('div')
:wikitext('Etymology tree')
:done()
:done()
:tag('div')
:addClass('NavContent')
:node(container)
:done())
end
return export
5yc8gbruezjpxwmlbbdye1nfr9lag3c
Modul:etymon/categories
828
82358
373461
342830
2026-09-10T08:26:15Z
SNN95
2113
letak dulu, terjemah kemudian
373461
Scribunto
text/plain
local export = {}
local M = require("Module:module loader").init({
require = {
etymology = "Module:etymology",
affix = "Module:affix",
etymology_specialized = "Module:etymology/specialized",
utilities = "Module:utilities",
roots = "Module:roots",
},
loadData = {
data = "Module:etymon/data",
},
})
-- Evaluate whether a keyword is transitive for a given term
local function is_transitive(transitive_mode, page_lang, term_lang)
if transitive_mode == M.data.TRANSITIVE.ALWAYS then
return true
elseif transitive_mode == M.data.TRANSITIVE.NEVER then
return false
elseif transitive_mode == M.data.TRANSITIVE.CROSS_LANG then
return page_lang:getCode() ~= term_lang:getCode()
elseif transitive_mode == M.data.TRANSITIVE.CROSS_LANG_NO_INTERNAL_SOURCE then
return page_lang:getCode() ~= term_lang:getCode()
end
error("Unknown transitive mode: " .. tostring(transitive_mode))
end
-- Get keyword config with language-specific overrides
local function get_keyword_config(keyword, lang_exc)
local base_config = M.data.keywords[keyword]
if not base_config then
return nil -- Invalid keyword
end
local overrides = lang_exc and lang_exc.keyword_overrides and lang_exc.keyword_overrides[keyword]
if not overrides then
return base_config
end
-- Merge overrides into base config
local merged = {}
for k, v in pairs(base_config) do
merged[k] = v
end
for k, v in pairs(overrides) do
merged[k] = v
end
return merged
end
function export.get_cat_name(source)
local _, cat_name = M.etymology.get_display_and_cat_name(source, true)
return cat_name
end
-- Normalize affix type aliases
local aftype_aliases = {
["pre"] = "prefix",
["suf"] = "suffix",
["in"] = "infix",
["inter"] = "interfix",
["circum"] = "circumfix",
["naf"] = "non-affix",
["root"] = "non-affix",
}
local function add_category(categories, cat_name, sort_key, sort_base)
if categories[cat_name] == nil then
categories[cat_name] = {
sort_key = sort_key,
sort_base = sort_base,
}
return
end
local existing = categories[cat_name]
if existing.sort_key == nil and sort_key ~= nil then
existing.sort_key = sort_key
end
if existing.sort_base == nil and sort_base ~= nil then
existing.sort_base = sort_base
end
end
-- Collect affix categories from top-level group containers
local function collect_affix_categories(node, page_lang, available_etymon_ids, senseid_parent_etymon, lang_exc)
local parts = {}
local part_index = 1
for _, container in ipairs(node.children or {}) do
local config = container.keyword_info
if config and config.affix_categories then
for _, term in ipairs(container.terms or {}) do
if not term.unknown_term then
local part_data = {
term = term.title,
tr = term.tr,
ts = term.ts,
alt = term.alt,
itemno = part_index,
orig_index = part_index
}
-- Determine affix type: explicit aftype > pos=root > auto-detect
local aftype = term.aftype
if aftype then
aftype = aftype_aliases[aftype] or aftype
part_data.type = aftype
elseif term.args and term.args.pos and term.args.pos == "root" then
part_data.type = "non-affix"
end
if term.lang:getCode() ~= page_lang:getCode() then
part_data.lang = term.lang
end
local target_ids = available_etymon_ids[term.target_key]
local has_multiple_ids = target_ids and #target_ids > 1
local id_exists_in_disambiguation = false
local matched_id = nil
-- Count available senseids for the target page
local senseid_count = 0
local target_prefix = term.target_key .. ":"
if senseid_parent_etymon then
for key, _ in pairs(senseid_parent_etymon) do
if key:sub(1, #target_prefix) == target_prefix then
senseid_count = senseid_count + 1
end
end
end
local has_multiple_senseids = senseid_count > 1
if term.id then
-- Check if user provided a valid senseid
local senseid_key = term.target_key .. ":" .. term.id
if senseid_parent_etymon and senseid_parent_etymon[senseid_key] then
if has_multiple_senseids then
-- Ambiguous senseid: use senseid
matched_id = term.id
id_exists_in_disambiguation = true
elseif has_multiple_ids then
-- Unique senseid but ambiguous etymon: use etymon ID
matched_id = term.etymon_id or term.id
id_exists_in_disambiguation = true
end
else
-- Check if user provided a valid etymon ID
if has_multiple_ids and target_ids then
for _, id_data in ipairs(target_ids) do
local stored_id = type(id_data) == "table" and id_data.id or id_data
if stored_id == term.id then
-- Ambiguous etymon: use etymon ID
id_exists_in_disambiguation = true
matched_id = term.id
break
end
end
end
-- Fallback: check resolved etymon_id (e.g. from previous steps)
if not id_exists_in_disambiguation and has_multiple_ids and term.etymon_id and target_ids then
for _, id_data in ipairs(target_ids) do
local stored_id = type(id_data) == "table" and id_data.id or id_data
if stored_id == term.etymon_id then
id_exists_in_disambiguation = true
matched_id = term.etymon_id
break
end
end
end
end
end
-- Use the matched ID if found
if term.override or id_exists_in_disambiguation then
part_data.id = matched_id or term.id
end
table.insert(parts, part_data)
part_index = part_index + 1
end
end
end
end
if #parts == 0 then return {} end
local affix_data = {
lang = page_lang,
parts = parts,
pos = "term",
sort_key = nil,
}
if #parts == 1 then
affix_data.allow_no_affixes_or_compounds = true
end
local affix_categories = M.affix.get_affix_categories_only(affix_data)
local result = {}
for _, cat in ipairs(affix_categories) do
if type(cat) == "table" then
table.insert(result, { cat = cat.cat, sort_key = cat.sort_key, sort_base = cat.sort_base })
else
table.insert(result, { cat = cat })
end
end
return result
end
local function lang_is_source(page_lang, source)
return page_lang:getCode() == source:getCode() or page_lang:hasParent(source)
end
local function is_borrowing_keyword_config(config)
return config and (config.borrowing_type or config.specialized_borrowing)
end
local function add_reborrow_category(categories, page_lang)
local lang_name = page_lang:getFullName()
add_category(categories, lang_name .. " terms borrowed back into " .. lang_name)
end
local function borrow_returns_to_page_lang(page_lang, source, in_foreign_branch)
if not in_foreign_branch then
return false
end
if source:getFullCode() == page_lang:getFullCode() then
return true
end
return page_lang:hasParent(source)
end
local function node_borrows_from_lang(node, page_lang, visited, in_foreign_branch)
visited = visited or {}
if not node or visited[node] then
return false
end
visited[node] = true
if node.is_duplicate then
if node.duplicate_of then
return node_borrows_from_lang(node.duplicate_of, page_lang, visited, in_foreign_branch)
end
return false
end
local node_is_foreign = in_foreign_branch
or (node.lang and node.lang:getFullCode() ~= page_lang:getFullCode())
for _, container in ipairs(node.children or {}) do
if is_borrowing_keyword_config(container.keyword_info) then
for _, child_term in ipairs(container.terms or {}) do
if borrow_returns_to_page_lang(page_lang, child_term.lang, node_is_foreign) then
return true
end
end
end
for _, child_term in ipairs(container.terms or {}) do
if child_term.status == M.data.STATUS.OK or child_term.status == M.data.STATUS.INLINE then
if node_borrows_from_lang(child_term, page_lang, visited, node_is_foreign) then
return true
end
end
end
end
return false
end
local function should_add_reborrow_category(page_lang, term)
if page_lang:getCode() == term.lang:getCode() then
return false
end
if term.status ~= M.data.STATUS.OK and term.status ~= M.data.STATUS.INLINE then
return false
end
return node_borrows_from_lang(term, page_lang, {}, false)
end
-- Add borrowing-related categories (top-level only)
local function collect_borrowing_categories(categories, page_lang, term, config, check_reborrow_path)
if check_reborrow_path and should_add_reborrow_category(page_lang, term) then
add_reborrow_category(categories, page_lang)
end
if config.borrowing_type == "borrowed" and not lang_is_source(page_lang, term.lang) then
local temp_categories = {}
M.etymology.insert_borrowed_cat(temp_categories, page_lang, term.lang)
for _, cat in ipairs(temp_categories) do
add_category(categories, cat)
end
end
if config.specialized_borrowing and not lang_is_source(page_lang, term.lang) then
local result = M.etymology_specialized.specialized_borrowing {
bortype = config.specialized_borrowing,
lang = page_lang,
sources = { term.lang },
terms = { { lang = term.lang, term = "-" } },
notext = true,
nocat = false,
}
for cat_name in result:gmatch("%[%[Category:([^%]]+)%]%]") do
add_category(categories, cat_name)
end
end
end
-- Add source-based derivation categories (top-level only)
local function collect_source_derivation_categories(categories, page_lang, term, config)
if not config.source_category_type then
return
end
local temp_categories = {}
M.etymology.insert_source_cat_get_display {
lang = page_lang,
source = term.lang,
categories = temp_categories,
borrowing_type = config.source_category_type,
nocat = false,
}
for _, cat in ipairs(temp_categories) do
add_category(categories, cat)
end
end
-- Add source language categories
local function collect_source_categories(categories, page_lang, term, chain, get_norm_lang_func)
if page_lang:getCode() == get_norm_lang_func(term.lang):getCode() then
return
end
local temp_categories = {}
M.etymology.insert_source_cat_get_display {
lang = page_lang,
source = term.lang,
categories = temp_categories,
nocat = false,
}
for _, cat in ipairs(temp_categories) do
add_category(categories, cat)
end
if chain.inherited then
temp_categories = {}
M.etymology.insert_source_cat_get_display {
lang = page_lang,
source = term.lang,
categories = temp_categories,
borrowing_type = "terms inherited",
nocat = false,
}
for _, cat in ipairs(temp_categories) do
add_category(categories, cat)
end
end
end
-- Add root/word categories
local function collect_pos_categories(categories, page_lang, root_title, term, available_etymon_ids, chain,
get_norm_lang_func, lang_exc, keyword)
local pos_types = { root = "root", word = "word" }
-- Determine pos: from term's postype, keyword's pos_override, or args.pos
local pos
local config = get_keyword_config(keyword, lang_exc)
if term.postype then
-- Term-level postype modifier takes highest priority
pos = term.postype
elseif config and config.pos_override then
pos = config.pos_override
elseif type(term.args) == "table" and term.args.pos then
pos = term.args.pos
end
local pos_type = pos_types[pos]
if not pos_type or term.unknown_term then
return
end
-- Skip root/word categories for descendants of affix groups
-- if pos_type then
-- return
-- end
local same_language = get_norm_lang_func(page_lang):getFullCode() == get_norm_lang_func(term.lang):getFullCode()
-- Skip self-references
if same_language and root_title == term.title then
return
end
local entry_name
if pos_type == "root" then
entry_name = term.title
M.roots.assert_root(term.lang, entry_name)
else
entry_name = term.lang:makeEntryName(term.title)
end
local lang_name = page_lang:getCanonicalName()
local cat_name
if chain.passed_through then
local etymon_lang_name = export.get_cat_name(term.lang)
cat_name = lang_name .. " terms derived from the " .. etymon_lang_name .. " " .. pos_type .. " " .. entry_name
else
cat_name = lang_name .. " terms belonging to the " .. pos_type .. " " .. entry_name
end
-- Add ID disambiguation if needed (for roots/words: use etymon_id if resolved via senseid, otherwise use id)
local target_ids = available_etymon_ids[term.target_key]
local effective_id = term.etymon_id or term.id -- etymon_id if senseid, otherwise id is already an etymon id
if target_ids and effective_id then
local same_pos_count = 0
for _, id_data in ipairs(target_ids) do
if type(id_data) == "table" and id_data.pos == pos then
same_pos_count = same_pos_count + 1
end
end
if same_pos_count > 1 then
cat_name = cat_name .. " (" .. effective_id .. ")"
end
end
add_category(categories, cat_name)
end
-- Compute chain state for a term based on parent chain and keyword config
-- Hyphen patterns for affix detection (regular hyphen + script-specific)
local AFFIX_HYPHEN_PATTERN = "[%-%־ـ᠊]" -- regular hyphen, Hebrew maqqef, Arabic tatweel, Mongolian hyphen
-- Check if a term is an actual affix (not a non-affix member of an affix group)
local function is_actual_affix(term)
-- Check explicit aftype modifier
if term.aftype then
local normalized = aftype_aliases[term.aftype] or term.aftype
return normalized ~= "non-affix"
end
-- Check if pos=root (treated as non-affix)
if term.args and term.args.pos and term.args.pos == "root" then
return false
end
-- Auto-detect by hyphen: prefix ends with -, suffix starts with -, etc.
if term.title then
local title = term.title
-- Strip leading * for reconstructed terms before checking hyphens
title = title:gsub("^%*", "")
-- Check for hyphens at start or end (handles script-specific hyphens too)
if title:match("^" .. AFFIX_HYPHEN_PATTERN) or title:match(AFFIX_HYPHEN_PATTERN .. "$") then
return true
end
end
-- Default: not an affix
return false
end
local function compute_category_chain(parent_chain, config, page_lang, term_lang, get_norm_lang_func, parent_term_lang, term)
-- Track if we're inside an actual affix (for suppressing root categories on descendants)
-- Only set if the term is an actual affix (prefix, suffix, etc.), not a non-affix member
local inside_affix = parent_chain.inside_affix
if config.affix_categories and term and is_actual_affix(term) then
inside_affix = true
end
-- If no_child_categories is set, disable everything
if config.no_child_categories then
return {
passed_through = parent_chain.passed_through or page_lang:getCode() ~= get_norm_lang_func(term_lang):getCode(),
inherited = false,
source = false,
pos = false,
recurse = false,
inside_affix = inside_affix,
}
end
local term_is_transitive = is_transitive(config.transitive, page_lang, term_lang)
local new_source = parent_chain.source and term_is_transitive
-- For CROSS_LANG_NO_INTERNAL_SOURCE: track internal derivation language context
-- Check if this term is internal relative to parent term's language (if parent_term_lang provided)
-- or relative to page language (if no parent_term_lang)
local internal_lang = parent_chain.internal_lang
local is_internal_in_context = false
if config.transitive == M.data.TRANSITIVE.CROSS_LANG_NO_INTERNAL_SOURCE then
local check_lang = parent_term_lang or page_lang
local term_lang_code = get_norm_lang_func(term_lang):getCode()
local check_lang_code = get_norm_lang_func(check_lang):getCode()
if internal_lang then
-- Already in an internal derivation context: check if this term is also internal
is_internal_in_context = term_lang_code == internal_lang
else
-- Check if this term is internal relative to parent term (or page if no parent)
is_internal_in_context = term_lang_code == check_lang_code
end
end
-- Source chain behavior for CROSS_LANG_NO_INTERNAL_SOURCE
if config.transitive == M.data.TRANSITIVE.CROSS_LANG_NO_INTERNAL_SOURCE then
if is_internal_in_context then
-- Internal derivation
new_source = false
internal_lang = get_norm_lang_func(term_lang):getCode()
else
-- Cross-language
new_source = parent_chain.source and term_is_transitive
internal_lang = nil
end
end
local new_pos = parent_chain.pos
return {
passed_through = parent_chain.passed_through or page_lang:getCode() ~= get_norm_lang_func(term_lang):getCode(),
inherited = parent_chain.inherited and config.inherited_chain,
source = new_source,
pos = new_pos,
internal_lang = internal_lang,
recurse = new_source or new_pos,
inside_affix = inside_affix,
}
end
function export.render(opts)
opts = opts or {}
local data_tree = opts.data_tree
local page_lang = opts.page_lang
local available_etymon_ids = opts.available_etymon_ids
local senseid_parent_etymon = opts.senseid_parent_etymon
local get_norm_lang_func = opts.get_norm_lang_func
local lang_exc = opts.lang_exc
local categories = {}
local seen = {}
local lang_name = page_lang:getCanonicalName()
local root_title = data_tree.title
-- Collect the tree recursively
local function collect(node, parent_chain, is_toplevel)
-- Avoid processing same node twice
if not node.unknown_term and node.title then
local key = node.lang:getFullCode() .. ":" .. (node.title or "") .. ":" .. (node.id or "")
if seen[key] then return end
seen[key] = true
end
-- Collect affix categories at top level only
if is_toplevel then
local affix_cats = collect_affix_categories(node, page_lang, available_etymon_ids, senseid_parent_etymon, lang_exc)
for _, cat in ipairs(affix_cats) do
add_category(categories, lang_name .. " " .. cat.cat, cat.sort_key, cat.sort_base)
end
if node.supplements then
for _, supplement in ipairs(node.supplements) do
local config = supplement.config
if config and config.toplevel_category then
add_category(categories, lang_name .. " " .. config.toplevel_category)
end
end
end
end
-- Process each container
for _, container in ipairs(node.children or {}) do
local keyword = container.keyword
local config = get_keyword_config(keyword, lang_exc)
-- Skip invalid keywords
if config then
-- Process each term in the container
for _, term in ipairs(container.terms or {}) do
local term_chain = compute_category_chain(parent_chain, config, page_lang, term.lang, get_norm_lang_func, node.lang, term)
local no_child_categories = config.no_child_categories == true
local term_is_transitive = is_transitive(config.transitive, page_lang, term.lang)
-- Top-level only processing
if is_toplevel then
-- Missing/ambiguous etymon tracking
if not term.unknown_term and (term.status == M.data.STATUS.MISSING or term.status == M.data.STATUS.REDLINK) then
add_category(categories, lang_name .. " entries referencing missing etymons")
end
if not term.unknown_term and term.status == M.data.STATUS.AMBIGUOUS then
add_category(categories, lang_name .. " entries referencing ambiguous etymons")
end
if term.missing_descendants_header then
add_category(categories, lang_name .. " entries referencing etymons without Descendants sections")
end
if term.missing_descendants_entry then
add_category(categories, lang_name .. " entries referencing etymons without this term in Descendants sections")
end
-- Top-level category (e.g., "undefined derivations")
if config.toplevel_category then
add_category(categories, lang_name .. " " .. config.toplevel_category)
end
-- Borrowing categories (bor, lbor, slbor, ubor, obor)
if config.borrowing_type or config.specialized_borrowing then
collect_borrowing_categories(categories, page_lang, term, config, true)
end
-- Borrowing categories from <bor>, <lbor>, or <slbor> modifiers on affix-group terms
local kw_config = M.data.keywords[keyword]
if kw_config and kw_config.affix_categories then
if term.bor then
local bor_config = { borrowing_type = "borrowed" }
collect_borrowing_categories(categories, page_lang, term, bor_config, true)
elseif term.lbor then
local bor_config = { specialized_borrowing = "learned" }
collect_borrowing_categories(categories, page_lang, term, bor_config, true)
elseif term.slbor then
local bor_config = { specialized_borrowing = "semi-learned" }
collect_borrowing_categories(categories, page_lang, term, bor_config, true)
end
end
-- Source-based derivation categories (sl, calque, pcal)
if config.source_category_type then
collect_source_derivation_categories(categories, page_lang, term, config)
end
-- Skip all child categorisation if no_child_categories is set
if not no_child_categories then
-- Source categories only if transitive
if term_is_transitive then
collect_source_categories(categories, page_lang, term, term_chain, get_norm_lang_func)
end
-- Pos categories always (unless no_child_categories)
collect_pos_categories(categories, page_lang, root_title, term, available_etymon_ids, term_chain,
get_norm_lang_func, lang_exc, keyword)
end
else
-- Below top level, respect the parent chain
if parent_chain.source then
collect_source_categories(categories, page_lang, term, term_chain, get_norm_lang_func)
end
if parent_chain.pos then
collect_pos_categories(categories, page_lang, root_title, term, available_etymon_ids, term_chain,
get_norm_lang_func, lang_exc, keyword)
end
end
-- Recurse into term's children if needed and status allows
if term_chain.recurse and (term.status == M.data.STATUS.OK or term.status == M.data.STATUS.INLINE) then
collect(term, term_chain, false)
end
end
end
end
end
-- Initial chain state
local initial_chain = {
passed_through = false,
inherited = true,
source = true,
pos = true,
internal_lang = nil,
recurse = true,
inside_affix = false,
}
collect(data_tree, initial_chain, true)
local cat_list = {}
for cat_name, sort_data in pairs(categories) do
if sort_data.sort_key ~= nil or sort_data.sort_base ~= nil then
table.insert(cat_list, {
name = cat_name,
sort_key = sort_data.sort_key,
sort_base = sort_data.sort_base,
})
else
table.insert(cat_list, cat_name)
end
end
return cat_list
end
function export.build(opts)
opts = opts or {}
local categories = {}
if not opts.suppress_categories and not opts.nocat then
categories = export.render({
data_tree = opts.data_tree,
page_lang = opts.page_lang,
available_etymon_ids = opts.available_etymon_ids,
senseid_parent_etymon = opts.senseid_parent_etymon,
get_norm_lang_func = opts.get_norm_lang_func,
lang_exc = opts.lang_exc,
})
end
local page_lang = opts.page_lang
if not page_lang then
return categories
end
local lang_name = page_lang:getCanonicalName()
table.insert(categories, "Pages with etymon")
table.insert(categories, lang_name .. " entries with etymon")
if opts.tree then
table.insert(categories, "Pages with etymology trees")
table.insert(categories, lang_name .. " entries with etymology trees")
end
if opts.text then
table.insert(categories, lang_name .. " entries with etymology texts")
end
if opts.exnihilo then
table.insert(categories, lang_name .. " terms coined ex nihilo")
end
if opts.toplevel_has_inline_etymology then
table.insert(categories, "Pages with inline etymon for redlinks")
end
if opts.toplevel_redundant_etymology then
table.insert(categories, "Pages with redundant inline etymon")
end
if opts.toplevel_idless_etymon then
table.insert(categories, "Pages using etymon with no ID")
end
if opts.has_mismatched_id then
table.insert(categories, lang_name .. " entries referencing etymons with mismatched IDs")
end
if opts.linked_page_multiple_etymons_idless then
table.insert(categories,
lang_name .. " entries referencing pages with multiple etymons missing IDs")
end
if opts.linked_page_partial_etymology_sections then
table.insert(categories,
lang_name .. " entries referencing pages with etymology sections missing etymons")
end
if opts.text_stop_lang_missing then
table.insert(categories, "Pages with etymology text stop language not in chain")
table.insert(categories, lang_name .. " entries with etymology text stop language not in chain")
end
return categories
end
function export.format(entries, lang)
if type(entries) ~= "table" or #entries == 0 then
return ""
end
local parts = {}
for _, category in ipairs(entries) do
if type(category) == "table" and type(category.name) == "string" then
table.insert(parts, M.utilities.format_categories({ category.name }, lang, category.sort_key, category.sort_base))
elseif type(category) == "string" then
table.insert(parts, M.utilities.format_categories({ category }, lang))
end
end
return table.concat(parts)
end
return export
rzmmziltpt0x9e9bh7pas72fpwpnrji
Modul:etymon/text
828
82359
373460
342819
2026-09-10T08:25:28Z
SNN95
2113
letak dulu, terjemah kemudian
373460
Scribunto
text/plain
local export = {}
local loader = require("Module:module loader")
local M = loader.init({
require = {
en_utilities = "Module:en-utilities",
references = "Module:references",
senseno = "Module:senseno",
},
loadData = {
data = "Module:etymon/data",
},
})
function export.render(opts)
opts = opts or {}
local data_tree = opts.data_tree
local format_term_func = opts.format_term_func
local max_depth = opts.max_depth
local stop_at_blue_link = opts.stop_at_blue_link
local curr_page = opts.curr_page
local nodot = opts.nodot and opts.nodot ~= "" and opts.nodot ~= "0"
local function explicit_dot_override()
if opts.dot == nil or opts.dot == false then
return nil
end
local d = mw.text.trim(tostring(opts.dot))
if d == "" or d == "." then
return nil
end
return d
end
local dot_override = explicit_dot_override()
local function find_deepest_last_part(tree)
if not tree or not tree.container_parts or #tree.container_parts == 0 then
return nil
end
local last_part = tree.container_parts[#tree.container_parts]
if last_part.continuation then
return find_deepest_last_part(last_part.continuation)
end
return last_part
end
local function apply_final_punctuation_override(tree, punct)
if not tree or punct == nil then
return
end
local last_part = find_deepest_last_part(tree)
if last_part then
last_part.punctuation = punct
end
end
local function apply_closing_punctuation_override(tree, is_last_segment)
if not is_last_segment then
return
end
local punct
if nodot then
punct = ""
elseif dot_override ~= nil then
punct = dot_override
else
return
end
apply_final_punctuation_override(tree, punct)
end
local function has_supplements()
return data_tree.supplements and #data_tree.supplements > 0
end
local stop_at_lang = opts.stop_at_lang
local stop_at_lang_or_bluelink = opts.stop_at_lang_or_bluelink
local lang_matches_stop_code = opts.lang_matches_stop_code
local stop_lang_reached = false
local function term_matches_stop_code(term_lang, stop_code)
if lang_matches_stop_code then
return lang_matches_stop_code(term_lang, stop_code)
end
return term_lang and term_lang:getCode() == stop_code
end
local children = data_tree.children
local function has_text_supplements()
if not data_tree.supplements then
return false
end
for _, supplement in ipairs(data_tree.supplements) do
if supplement.type == "doublet" and supplement.terms and #supplement.terms > 0 then
return true
end
if supplement.type == "etydate" and supplement.etydate_text and supplement.etydate_text ~= "" then
return true
end
end
return false
end
if (not children or #children == 0) and not has_text_supplements() then
if stop_at_lang then
return "", { stop_lang_reached = false }
end
return ""
end
local top_l2 = data_tree.lang:getFullCode() .. ":" .. curr_page
local entry_lang = data_tree.lang
local function lowercase_glossary_link_display(wikitext)
return wikitext:gsub("(%[%[Lampiran:Glosari#[^|]+|)([^%]])([^%]]*)%]%]", function(prefix, first, rest)
return prefix .. mw.ustring.lower(first) .. rest .. "]]"
end)
end
local function format_sl_senseid_intro(senseid, keyword_text, keyword_phrase, capitalize_senseno, is_uncertain)
local senseids = mw.text.split(senseid, "!!", true)
local senseno_parts = {}
for i, id in ipairs(senseids) do
id = mw.text.trim(id)
if id ~= "" then
table.insert(senseno_parts, M.senseno.link_text(entry_lang:getCode(), { id }, {
title = curr_page,
uc = (i == 1 and capitalize_senseno) or nil,
}))
end
end
if #senseno_parts == 0 then
return keyword_text, keyword_phrase
end
local senseno_text = mw.text.listToText(senseno_parts)
local glossary_link = (keyword_text or ""):gsub(" from$", "")
local glossary_lower = lowercase_glossary_link_display(glossary_link)
if #senseno_parts > 1 then
local plural_link = glossary_lower:gsub("|semantic loan%]%]", "|semantic loans]]")
if is_uncertain then
return senseno_text .. " are possibly " .. plural_link .. " from",
senseno_text .. " are possibly semantic loans from"
end
return senseno_text .. " are " .. plural_link .. " from",
senseno_text .. " are semantic loans from"
end
if is_uncertain then
return senseno_text .. " is possibly a " .. glossary_lower .. " from",
senseno_text .. " is possibly a semantic loan from"
end
return senseno_text .. " is a " .. glossary_lower .. " from",
senseno_text .. " is a semantic loan from"
end
-- Get refs for a term
local function get_term_refs(term, term_lang, depth)
local term_l2 = term_lang:getFullCode() .. ":" .. curr_page
if term.parsed_ref and (depth == 1 or term_l2 == top_l2) then
return M.references.format_references(term.parsed_ref)
end
return ""
end
-- Build a text part for a single term
local function build_term_part(term, current_lang, depth)
local text = ""
local new_lang = current_lang
local lang_changed = term.lang:getCanonicalName() ~= current_lang:getCanonicalName()
-- Use centralized format_term (handles suppress_term, unknown_term, and regular terms)
local term_text = format_term_func(term)
if lang_changed then
new_lang = term.lang
if term_text then
text = term.lang:makeWikipediaLink() .. " " .. term_text
elseif term.is_family then
text = M.en_utilities.add_indefinite_article(term.lang:makeWikipediaLink() .. " language", false)
else
-- suppress_term with language change: show only language
text = term.lang:makeWikipediaLink()
end
else
text = term_text or ""
end
return {
type = "term",
text = text,
refs = get_term_refs(term, new_lang, depth),
lang = new_lang,
is_uncertain = term.is_uncertain or false,
}
end
-- Build text parts for a container
local function build_container_part(container, node, depth, allow_continuation, fallback_to_bluelink)
local keyword_info = container.keyword_info
local keyword_modifiers = container.keyword_modifiers or {}
local terms = container.terms or {}
if not keyword_info or #terms == 0 then
return nil
end
-- Skip building text part when invisible in text ("all", "text", or true)
local inv = keyword_info.invisible
if inv == "all" or inv == true or inv == "text" then
return nil
end
local is_group = keyword_info.is_group
local keyword_uncertain = keyword_modifiers.unc or false
-- Determine text and phrase (allowing for overrides)
local intro_text = keyword_info.text
local phrase = keyword_info.phrase
local new_sentence = keyword_info.new_sentence or false
if keyword_modifiers.text then
-- User-provided override: assumed to be lowercase
phrase = keyword_modifiers.text
-- Auto-capitalize for intro text (e.g., "derived from" -> "Derived from")
intro_text = mw.ustring.upper(phrase:sub(1, 1)) .. phrase:sub(2)
end
-- Get keyword references
local keyword_refs = ""
if keyword_modifiers.ref then
local parsed_keyword_refs = M.references.parse_references(keyword_modifiers.ref)
if parsed_keyword_refs and parsed_keyword_refs ~= "" then
keyword_refs = M.references.format_references(parsed_keyword_refs)
end
end
-- Build term parts
local term_parts = {}
local current_lang = node.lang
for _, term in ipairs(terms) do
local term_part = build_term_part(term, current_lang, depth)
if term_part.text ~= "" then
table.insert(term_parts, term_part)
current_lang = term_part.lang
end
end
-- Check uncertainty distribution
local uncertain_count = 0
for _, term_part in ipairs(term_parts) do
if term_part.is_uncertain then
uncertain_count = uncertain_count + 1
end
end
-- If keyword itself is uncertain, treat all terms as uncertain
local all_uncertain = keyword_uncertain or (uncertain_count == #term_parts and #term_parts > 0)
if is_group and uncertain_count > 0 then
all_uncertain = true
end
local has_mixed_uncertainty = not all_uncertain and uncertain_count > 0
-- Check if there are more steps (only if continuation is allowed)
local has_more_steps = false
local next_node = nil
local first_term = terms[1]
-- Check if we should stop at this language
local reached_stop_lang = false
if stop_at_lang then
for _, term in ipairs(terms) do
if term.lang and term_matches_stop_code(term.lang, stop_at_lang) then
reached_stop_lang = true
stop_lang_reached = true
break
end
end
elseif stop_at_lang_or_bluelink then
-- Check if we should stop at this language, or at the first bluelink if it's a redlink
for _, term in ipairs(terms) do
if term.lang and term_matches_stop_code(term.lang, stop_at_lang_or_bluelink) then
if first_term.status == M.data.STATUS.OK then
reached_stop_lang = true
else
fallback_to_bluelink = true
end
break
end
end
if fallback_to_bluelink and first_term.status == M.data.STATUS.OK then
reached_stop_lang = true
end
end
if allow_continuation and not is_group and #terms == 1 and not reached_stop_lang then
local first_term_children = first_term.children
if first_term_children and #first_term_children > 0 and (not max_depth or depth < max_depth) then
local next_container = first_term_children[1]
local next_keyword_info = next_container and next_container.keyword_info
if not (next_keyword_info and next_keyword_info.invisible) then
if stop_at_blue_link then
if first_term.status ~= M.data.STATUS.OK then
has_more_steps = true
next_node = first_term
end
else
has_more_steps = true
next_node = first_term
end
end
end
end
return {
type = "container",
intro_text = intro_text,
phrase = phrase,
senseid = keyword_modifiers.senseid,
sl_keyword_text = keyword_modifiers.senseid and keyword_info.text or nil,
is_uncertain = all_uncertain,
has_mixed_uncertainty = has_mixed_uncertainty,
term_parts = term_parts,
is_group = is_group,
has_more_steps = has_more_steps,
next_node = next_node,
new_sentence = new_sentence,
separate_clause = keyword_info.separate_clause or false,
conj = keyword_modifiers.conj or keyword_info.default_conj, -- custom conjunction: "and", "or", "and/or", etc.
lit = keyword_modifiers.lit,
keyword_refs = keyword_refs,
fallback_to_bluelink = fallback_to_bluelink,
}
end
-- Build the full tree of text parts
local function build_text_tree(node, depth, allow_continuation, fallback_to_bluelink)
local containers = node.children
if not containers or #containers == 0 then
return nil
end
local container_parts = {}
-- Count containers that get a text part (invisible in text = "all", "text", or true)
local visible_container_count = 0
for _, container in ipairs(containers) do
local keyword_info = container.keyword_info
local inv = keyword_info and keyword_info.invisible
if not (inv == "all" or inv == true or inv == "text") then
visible_container_count = visible_container_count + 1
end
end
-- If there are multiple visible containers at this level, don't allow continuation for any
local has_multiple_containers = visible_container_count > 1
local should_allow_continuation = allow_continuation and not has_multiple_containers
for _, container in ipairs(containers) do
local part = build_container_part(container, node, depth, should_allow_continuation, fallback_to_bluelink)
if part then
-- Recursively build children if there are more steps
if part.has_more_steps and part.next_node then
part.continuation = build_text_tree(part.next_node, depth + 1, true, part.fallback_to_bluelink)
end
table.insert(container_parts, part)
end
end
if #container_parts == 0 then
return nil
end
return {
type = "tree",
container_parts = container_parts,
depth = depth,
}
end
-- Check if tree has mixed joining types
local function container_join_kind(part)
if part.type == "etydate" then
return nil
end
if part.new_sentence or part.separate_clause then
return "supplement"
end
return "or_join"
end
local function check_complexity(tree)
if not tree then return nil end
local parts = tree.container_parts
if #parts <= 1 then
-- Single container
if parts[1] and parts[1].continuation then
return check_complexity(parts[1].continuation)
end
return nil
end
-- Or-join containers must precede any supplemental (calque-like / influence) containers.
local seen_supplement = false
for _, part in ipairs(parts) do
local kind = container_join_kind(part)
if kind == "supplement" then
seen_supplement = true
elseif kind == "or_join" and seen_supplement then
error(
"Cannot generate etymology text: a main derivation step cannot follow a calque, semantic loan, or influence clause in the same list.")
end
end
for _, part in ipairs(parts) do
if part.continuation then
check_complexity(part.continuation)
end
end
return nil
end
-- Analyze tree and assign punctuation
local function analyze_punctuation(tree, is_toplevel)
if not tree then return end
local parts = tree.container_parts
local num_parts = #parts
for i, part in ipairs(parts) do
local is_first = (i == 1)
local is_last = (i == num_parts)
local next_part = parts[i + 1]
-- Analyze term punctuation within container
if part.term_parts then
-- Terms use Oxford comma style: "A, B, or C"
-- Custom conjunction can be specified via conj modifier (e.g., "and/or", "and")
local num_terms = #part.term_parts
local term_conj = part.conj or "or" -- default to "or"
for j, term_part in ipairs(part.term_parts) do
local is_last_term = (j == num_terms)
if part.is_group then
-- Group: terms joined with " + "
term_part.joiner = is_last_term and "" or " + "
elseif num_terms > 1 then
-- Multiple terms not in a group: Oxford comma style
if is_last_term then
term_part.joiner = ""
elseif j == num_terms - 1 then
-- Second to last term
if num_terms == 2 then
term_part.joiner = " " .. term_conj .. " "
else
term_part.joiner = ", " .. term_conj .. " "
end
else
term_part.joiner = ", "
end
else
-- Single term
term_part.joiner = ""
end
end
end
-- Determine container punctuation based on what comes next
if part.continuation then
-- Has continuation
part.punctuation = ","
-- Recursively analyze continuation
analyze_punctuation(part.continuation, false)
elseif is_last then
-- Last container at this level (may still continue in part.continuation)
part.punctuation = "."
elseif next_part and next_part.new_sentence then
-- Next container starts a new sentence
part.punctuation = "."
elseif next_part and next_part.separate_clause then
-- Next container is a separate clause
part.punctuation = ","
else
-- Not last, next is joined with "or"
-- Containers use repeated "or" style: "A, or B, or C"
part.punctuation = ","
end
-- Determine joiner to next part
-- Containers use repeated "or" style: ", or" between each
-- Custom conjunction can be specified via conj modifier
local container_conj = part.conj or "or" -- default to "or"
if not is_last then
if next_part and next_part.new_sentence then
-- New sentence
part.joiner = " "
elseif next_part and next_part.separate_clause then
-- Separate clause
part.joiner = " "
else
-- Same sentence: use custom conjunction or default "or"
part.joiner = " " .. container_conj .. " "
end
else
part.joiner = ""
end
-- Determine intro formatting
-- Capitalize if first at top level, OR if this container starts a new sentence
if (is_first and is_toplevel) or part.new_sentence then
part.intro_capitalized = true
part.use_full_intro = true
else
part.intro_capitalized = false
part.use_full_intro = false
end
end
end
-- Assemble text from analyzed tree
local function assemble_text(tree)
if not tree then return "" end
local result = ""
for i, part in ipairs(tree.container_parts) do
if part.type == "etydate" then
result = result .. part.etydate_text
if part.punctuation and part.punctuation ~= "" then
result = result .. part.punctuation
end
if part.etydate_refs and next(part.etydate_refs) then
result = result .. M.references.format_references(part.etydate_refs)
end
if part.joiner and part.joiner ~= "" then
result = result .. part.joiner
end
else
-- Build intro
local intro_text = part.intro_text
local phrase = part.phrase
if part.senseid then
intro_text, phrase = format_sl_senseid_intro(
part.senseid,
part.sl_keyword_text,
part.phrase,
part.intro_capitalized,
part.is_uncertain
)
end
local intro
if part.use_full_intro then
if part.is_uncertain and not part.senseid then
intro = "Possibly " .. phrase
else
intro = intro_text
end
else
if part.is_uncertain and not part.senseid then
intro = "possibly " .. phrase
else
intro = phrase
end
end
result = result .. intro
-- Build terms
if #part.term_parts > 0 then
result = result .. " "
for j, term_part in ipairs(part.term_parts) do
-- Add "possibly" prefix for uncertain terms when there's mixed uncertainty
if part.has_mixed_uncertainty and term_part.is_uncertain then
result = result .. "possibly "
end
result = result .. term_part.text
-- Add joiner between terms
if term_part.joiner ~= "" then
-- Check if joiner contains comma (punctuation)
local comma_pos = term_part.joiner:find(",")
if comma_pos then
-- Add up to and including comma
result = result .. term_part.joiner:sub(1, comma_pos)
-- Add refs after comma
if term_part.refs ~= "" then
result = result .. term_part.refs
end
-- Add rest of joiner
result = result .. term_part.joiner:sub(comma_pos + 1)
else
-- No comma, add refs before joiner
if term_part.refs ~= "" then
result = result .. term_part.refs
end
result = result .. term_part.joiner
end
end
end
-- For the last term, add punctuation then refs
local last_term = part.term_parts[#part.term_parts]
if last_term and last_term.joiner == "" then
if part.punctuation ~= "" then
-- If we have literal text, punctuation goes AFTER it
if part.lit then
-- Add refs first (attached to term)
if last_term.refs ~= "" then
result = result .. last_term.refs
end
-- Add keyword refs
if part.keyword_refs and part.keyword_refs ~= "" then
result = result .. part.keyword_refs
end
-- Add literal text
result = result .. ", literally “" .. part.lit .. "”"
-- Add punctuation
result = result .. part.punctuation
else
-- Normal behavior: punctuation then refs
result = result .. part.punctuation
if last_term.refs ~= "" then
result = result .. last_term.refs
end
-- Add keyword refs after term refs
if part.keyword_refs and part.keyword_refs ~= "" then
result = result .. part.keyword_refs
end
end
else
-- No punctuation
if last_term.refs ~= "" then
result = result .. last_term.refs
end
-- Add keyword refs
if part.keyword_refs and part.keyword_refs ~= "" then
result = result .. part.keyword_refs
end
-- Add literal text if present (even without punctuation)
if part.lit then
result = result .. ", literally “" .. part.lit .. "”"
end
end
end
else
-- No terms, just add punctuation and keyword refs
if part.punctuation ~= "" then
result = result .. part.punctuation
end
-- Add keyword refs even when there are no terms
if part.keyword_refs and part.keyword_refs ~= "" then
result = result .. part.keyword_refs
end
end
-- Add continuation
if part.continuation then
result = result .. " " .. assemble_text(part.continuation)
end
-- Add joiner to next container
if part.joiner ~= "" then
result = result .. part.joiner
end
end
end
return result
end
local text_tree = build_text_tree(data_tree, 1, true, false)
-- Supplements (doublets, etydate, …) are rendered outside the main derivation text tree.
local function assemble_supplements()
if not data_tree.supplements then
return ""
end
local chunks = {}
local pending_trees = {}
local function flush_pending_trees()
local num = #pending_trees
for i, supplement_tree in ipairs(pending_trees) do
analyze_punctuation(supplement_tree, true)
apply_closing_punctuation_override(supplement_tree, i == num)
local chunk = assemble_text(supplement_tree)
if chunk ~= "" then
table.insert(chunks, chunk)
end
end
pending_trees = {}
end
for _, supplement in ipairs(data_tree.supplements) do
local supplement_tree
if supplement.type == "doublet" and supplement.config
and supplement.terms and #supplement.terms > 0 then
local config = supplement.config
local term_parts = {}
for _, term in ipairs(supplement.terms) do
local term_part = build_term_part(term, entry_lang, 1)
if term_part.text ~= "" then
table.insert(term_parts, term_part)
end
end
if #term_parts == 0 then
supplement_tree = nil
else
supplement_tree = {
type = "tree",
container_parts = {
{
type = "doublet",
intro_text = config.text,
phrase = config.phrase,
term_parts = term_parts,
conj = config.default_conj or "and",
new_sentence = true,
},
},
depth = 1,
}
end
elseif supplement.type == "etydate" and supplement.etydate_text and supplement.etydate_text ~= "" then
supplement_tree = {
type = "tree",
container_parts = {
{
type = "etydate",
etydate_text = supplement.etydate_text,
etydate_refs = supplement.etydate_refs,
new_sentence = true,
},
},
depth = 1,
}
end
if supplement_tree then
table.insert(pending_trees, supplement_tree)
end
end
flush_pending_trees()
return table.concat(chunks, " ")
end
if not text_tree then
local supplement_text = assemble_supplements()
if supplement_text == "" then
if stop_at_lang then
return "", { stop_lang_reached = false }
end
return ""
end
if stop_at_lang then
return supplement_text, { stop_lang_reached = false }
end
return supplement_text
end
local rendered = ""
if text_tree then
check_complexity(text_tree)
analyze_punctuation(text_tree, true)
apply_closing_punctuation_override(text_tree, not has_supplements())
rendered = assemble_text(text_tree)
end
local supplement_text = assemble_supplements()
if supplement_text ~= "" then
if rendered ~= "" then
rendered = rendered .. " " .. supplement_text
else
rendered = supplement_text
end
end
if stop_at_lang then
return rendered, { stop_lang_reached = stop_lang_reached }
end
return rendered
end
return export
a3mx4ennfh4mr76u9n82k1zgi19ao11
Modul:etymon/data
828
118627
373457
342817
2026-09-10T08:19:40Z
SNN95
2113
letak dulu, terjemah kemudian
373457
Scribunto
text/plain
local export = {}
export.STATUS = {
OK = "ok",
INLINE = "inline",
MISSING = "missing",
REDLINK = "redlink",
AMBIGUOUS = "ambiguous",
}
export.TRANSITIVE = {
ALWAYS = "always", -- always recurse into children
NEVER = "never", -- never recurse into children
CROSS_LANG = "cross_lang", -- only recurse when source lang differs from target lang (but pos chain continues)
CROSS_LANG_NO_INTERNAL_SOURCE = "cross_lang_no_internal_source", -- like CROSS_LANG, but source breaks for internal derivations in the same language context
}
-- Deep merge tables (nested tables are merged recursively, later values override earlier)
local function deep_merge(...)
local result = {}
for _, t in ipairs({ ... }) do
for k, v in pairs(t) do
if type(v) == "table" and type(result[k]) == "table" then
result[k] = deep_merge(result[k], v)
else
result[k] = v
end
end
end
return result
end
local function make_glossary_link(term, display_text)
if not term then return display_text end
return "[[Appendix:Glossary#" .. term:gsub(" ", "_") .. "|" .. display_text .. "]]"
end
-- Extract base word and connector from text like "Borrowed from" or "calque of"
local function split_glossary_text(text)
for _, pattern in ipairs({ "^(.-)(%s+[Oo][Ff])$", "^(.-)(%s+[Ff][Rr][Oo][Mm])$" }) do
local base, rest = text:match(pattern)
if base then return base, rest end
end
return text, ""
end
local TRANSITIVE = export.TRANSITIVE
local function create_keyword(opts)
local entry = {
is_group = opts.is_group or false,
abbrev = opts.abbrev,
glossary = opts.glossary,
transitive = opts.transitive or TRANSITIVE.ALWAYS, -- default "always"
inherited_chain = opts.inherited_chain or false,
affix_categories = opts.affix_categories or false,
borrowing_type = opts.borrowing_type,
specialized_borrowing = opts.specialized_borrowing,
toplevel_category = opts.toplevel_category,
no_child_categories = opts.no_child_categories or false,
source_category_type = opts.source_category_type,
invisible = (opts.invisible == true and "all") or opts.invisible or false,
pos_override = opts.pos_override,
new_sentence = opts.new_sentence or false,
separate_clause = opts.separate_clause or false,
default_conj = opts.default_conj,
min_etymons = opts.min_etymons,
max_etymons = opts.max_etymons,
term_rules = opts.term_rules,
aliases = opts.aliases,
}
-- Only set text/phrase when visible in text (invisible ~= "all" and ~= "text")
local inv = entry.invisible
if inv ~= "all" and inv ~= "text" then
entry.phrase = opts.phrase
if opts.text then
if opts.glossary then
local base_word, rest = split_glossary_text(opts.text)
entry.text = make_glossary_link(opts.glossary, base_word) .. rest
else
entry.text = opts.text
end
end
end
return entry
end
-- Shared defaults for keyword groups
local DEFAULTS = {
-- Keywords that pass through inheritance chain
inheritance = {
transitive = TRANSITIVE.ALWAYS,
inherited_chain = true,
},
-- Standard transitive derivation
transitive = {
transitive = TRANSITIVE.ALWAYS,
},
-- Standard for internal derivations: transitive across languages, but not within them
internal_derivation = {
transitive = TRANSITIVE.CROSS_LANG,
},
-- Borrowing keywords
borrowing = {
transitive = TRANSITIVE.ALWAYS,
},
-- Affix group keywords (compound words, blends, etc.)
affix_group = {
is_group = true,
min_etymons = 2,
transitive = TRANSITIVE.CROSS_LANG,
affix_categories = true,
},
-- Calque-like keywords (calque, partial calque, semantic loan)
calque_like = {
transitive = TRANSITIVE.NEVER,
no_child_categories = true,
new_sentence = true,
},
-- Non-transitive influence
influence_like = {
transitive = TRANSITIVE.NEVER,
no_child_categories = true,
},
}
export.keywords = {
--
-- Inheritance keywords
--
["from"] = create_keyword(deep_merge(DEFAULTS.inheritance, {
text = "From", phrase = "from",
term_rules = { entry_lang = true },
})),
["inherited"] = create_keyword(deep_merge(DEFAULTS.inheritance, {
text = "Inherited from",
phrase = "from",
glossary = "inherited",
aliases = { "inh" },
term_rules = {
family = "disallowed",
family_suffix = "; use a specific language.",
ancestor_check = true,
},
})),
--
-- Basic derivation keywords
--
["uder"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "From",
phrase = "from",
toplevel_category = "undefined derivations",
})),
["derived"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Derived from",
phrase = "from",
abbrev = "der.",
glossary = "derived terms",
aliases = { "der" },
})),
--
-- Affix/compound group keywords
--
["affix"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "From",
phrase = "from",
min_etymons = 1,
aliases = { "af" },
})),
["blend"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "Blend of",
phrase = "a blend of",
abbrev = "blend",
glossary = "blend",
toplevel_category = "blends",
})),
["univerbation"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "Univerbation of",
phrase = "univerbation of",
abbrev = "univ.",
glossary = "univerbation",
toplevel_category = "univerbations",
min_etymons = 1,
aliases = { "univ" },
})),
["vrd-af"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "Vṛddhi derivative of",
phrase = "a vṛddhi derivative of",
abbrev = "vṛd.",
glossary = "vṛddhi derivative",
toplevel_category = "vrddhi derivatives",
})),
["sa-af"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "[[Sanskritic]] formation from",
phrase = "a [[Sanskritic]] formation of",
toplevel_category = "Sanskritic formations",
})),
--
-- Borrowing keywords
--
["bor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Borrowed from",
phrase = "borrowed from",
abbrev = "bor.",
glossary = "loanword",
borrowing_type = "borrowed",
aliases = { "borrowed" },
})),
["lbor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Learned borrowing from",
phrase = "a learned borrowing from",
abbrev = "lbor.",
glossary = "learned borrowing",
specialized_borrowing = "learned",
})),
["obor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Orthographic borrowing from",
phrase = "an orthographic borrowing from",
abbrev = "obor.",
glossary = "orthographic borrowing",
specialized_borrowing = "orthographic",
})),
["slbor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Semi-learned borrowing from",
phrase = "a semi-learned borrowing from",
abbrev = "slbor.",
glossary = "semi-learned borrowing",
specialized_borrowing = "semi-learned",
aliases = { "slb" },
})),
["ubor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Unadapted borrowing from",
phrase = "an unadapted borrowing from",
abbrev = "ubor.",
glossary = "unadapted borrowing",
specialized_borrowing = "unadapted",
})),
--
-- Calque-like keywords (non-transitive, start new sentence)
--
["calque"] = create_keyword(deep_merge(DEFAULTS.calque_like, {
text = "Calque of",
phrase = "a calque of",
abbrev = "calq.",
glossary = "calque",
specialized_borrowing = "calque",
aliases = { "cal", "clq" },
})),
["partial calque"] = create_keyword(deep_merge(DEFAULTS.calque_like, {
text = "Partial calque of",
phrase = "a partial calque of",
abbrev = "pcalq.",
glossary = "partial calque",
specialized_borrowing = "partial-calque",
aliases = { "pcal" },
})),
["semantic loan"] = create_keyword(deep_merge(DEFAULTS.calque_like, {
text = "Semantic loan from",
phrase = "a semantic loan from",
abbrev = "sl.",
glossary = "semantic loan",
specialized_borrowing = "semantic-loan",
aliases = { "sl" },
})),
["psm"] = create_keyword(deep_merge(DEFAULTS.calque_like, {
text = "Phono-semantic matching of",
phrase = "a phono-semantic matching of",
abbrev = "psm.",
glossary = "phono-semantic matching",
specialized_borrowing = "phono-semantic-matching",
aliases = { "phono-semantic matching" },
})),
--
-- Influence keywords (non-transitive, separate clause)
--
["influence"] = create_keyword(deep_merge(DEFAULTS.influence_like, {
text = "Influenced by",
phrase = "influenced by",
abbrev = "influ.",
glossary = "contamination",
separate_clause = true,
})),
--
-- Morphological derivation keywords
--
["clipping"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Clipping of",
phrase = "clipping of",
abbrev = "clip.",
glossary = "clipping",
toplevel_category = "clippings",
aliases = { "clip" },
})),
["ellipsis"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Ellipsis of",
phrase = "ellipsis of",
abbrev = "ellip.",
glossary = "ellipsis",
toplevel_category = "ellipses",
aliases = { "ellip" },
})),
["back-formation"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Back-formation from",
phrase = "a back-formation from",
abbrev = "bf.",
glossary = "back-formation",
toplevel_category = "back-formations",
aliases = { "bf" },
})),
["nominalization"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Nominalization of",
phrase = "a nominalization of",
abbrev = "nom.",
glossary = "nominalization",
toplevel_category = "nominalizations",
aliases = { "nom" },
})),
["transliteration"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Transliteration of",
phrase = "borrowed from",
abbrev = "translit.",
glossary = "transliteration",
aliases = { "translit" },
})),
["vrd"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Vṛddhi derivative of",
phrase = "a vṛddhi derivative of",
abbrev = "vṛd.",
glossary = "vṛddhi derivative",
toplevel_category = "vrddhi derivatives",
})),
["apheretic"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Apheretic form of",
phrase = "an apheretic form of",
abbrev = "aph.",
glossary = "apheresis",
aliases = { "apheresis", "aphetic" },
})),
["denominal"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Denominal verb from",
phrase = "denominal verb from",
abbrev = "denom.",
glossary = "denominal",
toplevel_category = "denominal verbs",
aliases = { "denom" },
})),
["deverbal"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Deverbal from",
phrase = "deverbal from",
abbrev = "deverb.",
glossary = "deverbal",
toplevel_category = "deverbals",
})),
["reduplication"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Reduplication of",
phrase = "reduplication of",
abbrev = "redup.",
glossary = "reduplication",
toplevel_category = "reduplications",
aliases = { "redup" },
})),
["abbreviation"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Abbreviation of",
phrase = "abbreviation of",
abbrev = "abbr.",
glossary = "abbreviation",
aliases = { "abbr", "abbrev" },
})),
["syllabic abbreviation"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Syllabic abbreviation of",
phrase = "syllabic abbreviation of",
abbrev = "syl. abbr.",
glossary = "syllabic abbreviation",
aliases = { "sylabbr", "sylabbrev" },
})),
["acronym"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Acronym of",
phrase = "acronym of",
abbrev = "acronym",
glossary = "acronym",
aliases = { "acro" },
})),
["initialism"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Initialism of",
phrase = "initialism of",
abbrev = "init.",
glossary = "initialism",
aliases = { "init" },
})),
["metathesis"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Metathesis of",
phrase = "metathesis of",
abbrev = "meta.",
glossary = "metathesis",
toplevel_category = "words derived through metathesis",
aliases = { "meta" },
})),
--
-- Invisible keywords (no text output)
--
["root"] = create_keyword {
transitive = TRANSITIVE.ALWAYS,
invisible = "all",
pos_override = "root",
},
["afeq"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "From",
phrase = "from",
transitive = TRANSITIVE.NEVER,
min_etymons = 1,
invisible = "all",
})),
}
-- Template-parameter supplements.
export.supplements = {
doublet = {
text = "Doublet of",
phrase = "doublet of",
glossary = "doublet",
toplevel_category = "doublets",
default_conj = "and",
term_rules = {
entry_lang = true,
require_term = true,
disallow = { "suppress", "unknown", "family" },
},
},
}
local aliases_to_register = {}
local canonical_aliases = {}
-- Map every keyword (canonical or alias) to its canonical form for consistent checks and tracking.
export.keyword_canonical = {}
for name, keyword_data in pairs(export.keywords) do
export.keyword_canonical[name] = name
if keyword_data.aliases then
canonical_aliases[name] = keyword_data.aliases
for _, alias in ipairs(keyword_data.aliases) do
if export.keywords[alias] then
error("Alias '" ..
alias .. "' defined in keyword '" .. name .. "' collides with existing keyword '" .. alias .. "'.")
end
if aliases_to_register[alias] then
error("Alias '" ..
alias .. "' defined in keyword '" .. name .. "' is already claimed by another keyword.")
end
aliases_to_register[alias] = keyword_data
export.keyword_canonical[alias] = name
end
keyword_data.aliases = nil
end
end
for alias, data in pairs(aliases_to_register) do
export.keywords[alias] = data
end
--
-- Language exception presets
--
local EXCEPTION_PRESETS = {
-- Fully disallowed: no tree, no text, no categories
disallowed = {
disallow = { tree = true, text = true },
suppress_categories = true,
},
-- Suppress transliteration only
no_translit = {
suppress_tr = true,
},
-- Suppress all categories only
no_categories = {
suppress_categories = true,
},
}
--[=[
Available exception options:
disallow = { Related options for disallowing output:
tree Disallow etymology trees for this language
text Disallow etymology text generation for this language
ref Reference link shown when tree/text is disallowed
}
suppress_tr Suppress transliteration in links
suppress_categories Suppress all category generation
normalize_to Normalize language code to a different code
normalize_from_families Apply normalization to languages in these families
normalize_exclude_families Exclude these families from normalization
keyword_overrides Per-keyword categorisation overrides (e.g. { ["af"] = { transitive = TRANSITIVE.NEVER } })
]=]
local function create_exception(preset, overrides)
local base = preset and EXCEPTION_PRESETS[preset] or {}
return deep_merge(base, overrides or {})
end
export.config = {
lang_exceptions = {
["zh"] = create_exception("disallowed", {
disallow = { ref = "[[Wiktionary:Beer parlour/2025/May#Template:etymon for Chinese]]" },
suppress_tr = true,
normalize_to = "zh",
normalize_from_families = { "zhx" },
normalize_exclude_families = { "qfa-cnt" },
}),
},
}
-- Supported codes for the nominalization <g:code> modifier (subset of common gender/number-style codes)
export.nominalization_g_codes = {
["m"] = "masculine",
["f"] = "feminine",
["n"] = "neuter",
["c"] = "common",
["gneut"] = "gender-neutral",
["s"] = "singular",
["p"] = "plural",
["d"] = "dual",
["pauc"] = "paucal",
["mf"] = "masculine or feminine",
["fm"] = "masculine or feminine",
["mfn"] = "masculine, feminine or neuter",
["mnf"] = "masculine, feminine or neuter",
["fmn"] = "masculine, feminine or neuter",
["fnm"] = "masculine, feminine or neuter",
["nmf"] = "masculine, feminine or neuter",
["nfm"] = "masculine, feminine or neuter",
}
--
-- Propagate keyword overrides to aliases
--
if export.config.lang_exceptions then
for lang_code, exception in pairs(export.config.lang_exceptions) do
if exception.keyword_overrides then
for canonical, aliases in pairs(canonical_aliases) do
if exception.keyword_overrides[canonical] then
local override_data = exception.keyword_overrides[canonical]
for _, alias in ipairs(aliases) do
if not exception.keyword_overrides[alias] then
exception.keyword_overrides[alias] = override_data
end
end
end
end
end
end
end
return export
lqhnxervmhv2peftp9vj5dwbzwj2t3h
373458
373457
2026-09-10T08:20:47Z
SNN95
2113
373458
Scribunto
text/plain
local export = {}
export.STATUS = {
OK = "ok",
INLINE = "inline",
MISSING = "missing",
REDLINK = "redlink",
AMBIGUOUS = "ambiguous",
}
export.TRANSITIVE = {
ALWAYS = "always", -- always recurse into children
NEVER = "never", -- never recurse into children
CROSS_LANG = "cross_lang", -- only recurse when source lang differs from target lang (but pos chain continues)
CROSS_LANG_NO_INTERNAL_SOURCE = "cross_lang_no_internal_source", -- like CROSS_LANG, but source breaks for internal derivations in the same language context
}
-- Deep merge tables (nested tables are merged recursively, later values override earlier)
local function deep_merge(...)
local result = {}
for _, t in ipairs({ ... }) do
for k, v in pairs(t) do
if type(v) == "table" and type(result[k]) == "table" then
result[k] = deep_merge(result[k], v)
else
result[k] = v
end
end
end
return result
end
local function make_glossary_link(term, display_text)
if not term then return display_text end
return "[[Lampiran:Glosari#" .. term:gsub(" ", "_") .. "|" .. display_text .. "]]"
end
-- Extract base word and connector from text like "Borrowed from" or "calque of"
local function split_glossary_text(text)
for _, pattern in ipairs({ "^(.-)(%s+[Oo][Ff])$", "^(.-)(%s+[Ff][Rr][Oo][Mm])$" }) do
local base, rest = text:match(pattern)
if base then return base, rest end
end
return text, ""
end
local TRANSITIVE = export.TRANSITIVE
local function create_keyword(opts)
local entry = {
is_group = opts.is_group or false,
abbrev = opts.abbrev,
glossary = opts.glossary,
transitive = opts.transitive or TRANSITIVE.ALWAYS, -- default "always"
inherited_chain = opts.inherited_chain or false,
affix_categories = opts.affix_categories or false,
borrowing_type = opts.borrowing_type,
specialized_borrowing = opts.specialized_borrowing,
toplevel_category = opts.toplevel_category,
no_child_categories = opts.no_child_categories or false,
source_category_type = opts.source_category_type,
invisible = (opts.invisible == true and "all") or opts.invisible or false,
pos_override = opts.pos_override,
new_sentence = opts.new_sentence or false,
separate_clause = opts.separate_clause or false,
default_conj = opts.default_conj,
min_etymons = opts.min_etymons,
max_etymons = opts.max_etymons,
term_rules = opts.term_rules,
aliases = opts.aliases,
}
-- Only set text/phrase when visible in text (invisible ~= "all" and ~= "text")
local inv = entry.invisible
if inv ~= "all" and inv ~= "text" then
entry.phrase = opts.phrase
if opts.text then
if opts.glossary then
local base_word, rest = split_glossary_text(opts.text)
entry.text = make_glossary_link(opts.glossary, base_word) .. rest
else
entry.text = opts.text
end
end
end
return entry
end
-- Shared defaults for keyword groups
local DEFAULTS = {
-- Keywords that pass through inheritance chain
inheritance = {
transitive = TRANSITIVE.ALWAYS,
inherited_chain = true,
},
-- Standard transitive derivation
transitive = {
transitive = TRANSITIVE.ALWAYS,
},
-- Standard for internal derivations: transitive across languages, but not within them
internal_derivation = {
transitive = TRANSITIVE.CROSS_LANG,
},
-- Borrowing keywords
borrowing = {
transitive = TRANSITIVE.ALWAYS,
},
-- Affix group keywords (compound words, blends, etc.)
affix_group = {
is_group = true,
min_etymons = 2,
transitive = TRANSITIVE.CROSS_LANG,
affix_categories = true,
},
-- Calque-like keywords (calque, partial calque, semantic loan)
calque_like = {
transitive = TRANSITIVE.NEVER,
no_child_categories = true,
new_sentence = true,
},
-- Non-transitive influence
influence_like = {
transitive = TRANSITIVE.NEVER,
no_child_categories = true,
},
}
export.keywords = {
--
-- Inheritance keywords
--
["from"] = create_keyword(deep_merge(DEFAULTS.inheritance, {
text = "From", phrase = "from",
term_rules = { entry_lang = true },
})),
["inherited"] = create_keyword(deep_merge(DEFAULTS.inheritance, {
text = "Inherited from",
phrase = "from",
glossary = "inherited",
aliases = { "inh" },
term_rules = {
family = "disallowed",
family_suffix = "; use a specific language.",
ancestor_check = true,
},
})),
--
-- Basic derivation keywords
--
["uder"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "From",
phrase = "from",
toplevel_category = "undefined derivations",
})),
["derived"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Derived from",
phrase = "from",
abbrev = "der.",
glossary = "derived terms",
aliases = { "der" },
})),
--
-- Affix/compound group keywords
--
["affix"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "From",
phrase = "from",
min_etymons = 1,
aliases = { "af" },
})),
["blend"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "Blend of",
phrase = "a blend of",
abbrev = "blend",
glossary = "blend",
toplevel_category = "blends",
})),
["univerbation"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "Univerbation of",
phrase = "univerbation of",
abbrev = "univ.",
glossary = "univerbation",
toplevel_category = "univerbations",
min_etymons = 1,
aliases = { "univ" },
})),
["vrd-af"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "Vṛddhi derivative of",
phrase = "a vṛddhi derivative of",
abbrev = "vṛd.",
glossary = "vṛddhi derivative",
toplevel_category = "vrddhi derivatives",
})),
["sa-af"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "[[Sanskritic]] formation from",
phrase = "a [[Sanskritic]] formation of",
toplevel_category = "Sanskritic formations",
})),
--
-- Borrowing keywords
--
["bor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Borrowed from",
phrase = "borrowed from",
abbrev = "bor.",
glossary = "loanword",
borrowing_type = "borrowed",
aliases = { "borrowed" },
})),
["lbor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Learned borrowing from",
phrase = "a learned borrowing from",
abbrev = "lbor.",
glossary = "learned borrowing",
specialized_borrowing = "learned",
})),
["obor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Orthographic borrowing from",
phrase = "an orthographic borrowing from",
abbrev = "obor.",
glossary = "orthographic borrowing",
specialized_borrowing = "orthographic",
})),
["slbor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Semi-learned borrowing from",
phrase = "a semi-learned borrowing from",
abbrev = "slbor.",
glossary = "semi-learned borrowing",
specialized_borrowing = "semi-learned",
aliases = { "slb" },
})),
["ubor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Unadapted borrowing from",
phrase = "an unadapted borrowing from",
abbrev = "ubor.",
glossary = "unadapted borrowing",
specialized_borrowing = "unadapted",
})),
--
-- Calque-like keywords (non-transitive, start new sentence)
--
["calque"] = create_keyword(deep_merge(DEFAULTS.calque_like, {
text = "Calque of",
phrase = "a calque of",
abbrev = "calq.",
glossary = "calque",
specialized_borrowing = "calque",
aliases = { "cal", "clq" },
})),
["partial calque"] = create_keyword(deep_merge(DEFAULTS.calque_like, {
text = "Partial calque of",
phrase = "a partial calque of",
abbrev = "pcalq.",
glossary = "partial calque",
specialized_borrowing = "partial-calque",
aliases = { "pcal" },
})),
["semantic loan"] = create_keyword(deep_merge(DEFAULTS.calque_like, {
text = "Semantic loan from",
phrase = "a semantic loan from",
abbrev = "sl.",
glossary = "semantic loan",
specialized_borrowing = "semantic-loan",
aliases = { "sl" },
})),
["psm"] = create_keyword(deep_merge(DEFAULTS.calque_like, {
text = "Phono-semantic matching of",
phrase = "a phono-semantic matching of",
abbrev = "psm.",
glossary = "phono-semantic matching",
specialized_borrowing = "phono-semantic-matching",
aliases = { "phono-semantic matching" },
})),
--
-- Influence keywords (non-transitive, separate clause)
--
["influence"] = create_keyword(deep_merge(DEFAULTS.influence_like, {
text = "Influenced by",
phrase = "influenced by",
abbrev = "influ.",
glossary = "contamination",
separate_clause = true,
})),
--
-- Morphological derivation keywords
--
["clipping"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Clipping of",
phrase = "clipping of",
abbrev = "clip.",
glossary = "clipping",
toplevel_category = "clippings",
aliases = { "clip" },
})),
["ellipsis"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Ellipsis of",
phrase = "ellipsis of",
abbrev = "ellip.",
glossary = "ellipsis",
toplevel_category = "ellipses",
aliases = { "ellip" },
})),
["back-formation"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Back-formation from",
phrase = "a back-formation from",
abbrev = "bf.",
glossary = "back-formation",
toplevel_category = "back-formations",
aliases = { "bf" },
})),
["nominalization"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Nominalization of",
phrase = "a nominalization of",
abbrev = "nom.",
glossary = "nominalization",
toplevel_category = "nominalizations",
aliases = { "nom" },
})),
["transliteration"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Transliteration of",
phrase = "borrowed from",
abbrev = "translit.",
glossary = "transliteration",
aliases = { "translit" },
})),
["vrd"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Vṛddhi derivative of",
phrase = "a vṛddhi derivative of",
abbrev = "vṛd.",
glossary = "vṛddhi derivative",
toplevel_category = "vrddhi derivatives",
})),
["apheretic"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Apheretic form of",
phrase = "an apheretic form of",
abbrev = "aph.",
glossary = "apheresis",
aliases = { "apheresis", "aphetic" },
})),
["denominal"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Denominal verb from",
phrase = "denominal verb from",
abbrev = "denom.",
glossary = "denominal",
toplevel_category = "denominal verbs",
aliases = { "denom" },
})),
["deverbal"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Deverbal from",
phrase = "deverbal from",
abbrev = "deverb.",
glossary = "deverbal",
toplevel_category = "deverbals",
})),
["reduplication"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Reduplication of",
phrase = "reduplication of",
abbrev = "redup.",
glossary = "reduplication",
toplevel_category = "reduplications",
aliases = { "redup" },
})),
["abbreviation"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Abbreviation of",
phrase = "abbreviation of",
abbrev = "abbr.",
glossary = "abbreviation",
aliases = { "abbr", "abbrev" },
})),
["syllabic abbreviation"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Syllabic abbreviation of",
phrase = "syllabic abbreviation of",
abbrev = "syl. abbr.",
glossary = "syllabic abbreviation",
aliases = { "sylabbr", "sylabbrev" },
})),
["acronym"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Acronym of",
phrase = "acronym of",
abbrev = "acronym",
glossary = "acronym",
aliases = { "acro" },
})),
["initialism"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Initialism of",
phrase = "initialism of",
abbrev = "init.",
glossary = "initialism",
aliases = { "init" },
})),
["metathesis"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Metathesis of",
phrase = "metathesis of",
abbrev = "meta.",
glossary = "metathesis",
toplevel_category = "words derived through metathesis",
aliases = { "meta" },
})),
--
-- Invisible keywords (no text output)
--
["root"] = create_keyword {
transitive = TRANSITIVE.ALWAYS,
invisible = "all",
pos_override = "root",
},
["afeq"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "From",
phrase = "from",
transitive = TRANSITIVE.NEVER,
min_etymons = 1,
invisible = "all",
})),
}
-- Template-parameter supplements.
export.supplements = {
doublet = {
text = "Doublet of",
phrase = "doublet of",
glossary = "doublet",
toplevel_category = "doublets",
default_conj = "and",
term_rules = {
entry_lang = true,
require_term = true,
disallow = { "suppress", "unknown", "family" },
},
},
}
local aliases_to_register = {}
local canonical_aliases = {}
-- Map every keyword (canonical or alias) to its canonical form for consistent checks and tracking.
export.keyword_canonical = {}
for name, keyword_data in pairs(export.keywords) do
export.keyword_canonical[name] = name
if keyword_data.aliases then
canonical_aliases[name] = keyword_data.aliases
for _, alias in ipairs(keyword_data.aliases) do
if export.keywords[alias] then
error("Alias '" ..
alias .. "' defined in keyword '" .. name .. "' collides with existing keyword '" .. alias .. "'.")
end
if aliases_to_register[alias] then
error("Alias '" ..
alias .. "' defined in keyword '" .. name .. "' is already claimed by another keyword.")
end
aliases_to_register[alias] = keyword_data
export.keyword_canonical[alias] = name
end
keyword_data.aliases = nil
end
end
for alias, data in pairs(aliases_to_register) do
export.keywords[alias] = data
end
--
-- Language exception presets
--
local EXCEPTION_PRESETS = {
-- Fully disallowed: no tree, no text, no categories
disallowed = {
disallow = { tree = true, text = true },
suppress_categories = true,
},
-- Suppress transliteration only
no_translit = {
suppress_tr = true,
},
-- Suppress all categories only
no_categories = {
suppress_categories = true,
},
}
--[=[
Available exception options:
disallow = { Related options for disallowing output:
tree Disallow etymology trees for this language
text Disallow etymology text generation for this language
ref Reference link shown when tree/text is disallowed
}
suppress_tr Suppress transliteration in links
suppress_categories Suppress all category generation
normalize_to Normalize language code to a different code
normalize_from_families Apply normalization to languages in these families
normalize_exclude_families Exclude these families from normalization
keyword_overrides Per-keyword categorisation overrides (e.g. { ["af"] = { transitive = TRANSITIVE.NEVER } })
]=]
local function create_exception(preset, overrides)
local base = preset and EXCEPTION_PRESETS[preset] or {}
return deep_merge(base, overrides or {})
end
export.config = {
lang_exceptions = {
["zh"] = create_exception("disallowed", {
disallow = { ref = "[[Wiktionary:Beer parlour/2025/May#Template:etymon for Chinese]]" },
suppress_tr = true,
normalize_to = "zh",
normalize_from_families = { "zhx" },
normalize_exclude_families = { "qfa-cnt" },
}),
},
}
-- Supported codes for the nominalization <g:code> modifier (subset of common gender/number-style codes)
export.nominalization_g_codes = {
["m"] = "masculine",
["f"] = "feminine",
["n"] = "neuter",
["c"] = "common",
["gneut"] = "gender-neutral",
["s"] = "singular",
["p"] = "plural",
["d"] = "dual",
["pauc"] = "paucal",
["mf"] = "masculine or feminine",
["fm"] = "masculine or feminine",
["mfn"] = "masculine, feminine or neuter",
["mnf"] = "masculine, feminine or neuter",
["fmn"] = "masculine, feminine or neuter",
["fnm"] = "masculine, feminine or neuter",
["nmf"] = "masculine, feminine or neuter",
["nfm"] = "masculine, feminine or neuter",
}
--
-- Propagate keyword overrides to aliases
--
if export.config.lang_exceptions then
for lang_code, exception in pairs(export.config.lang_exceptions) do
if exception.keyword_overrides then
for canonical, aliases in pairs(canonical_aliases) do
if exception.keyword_overrides[canonical] then
local override_data = exception.keyword_overrides[canonical]
for _, alias in ipairs(aliases) do
if not exception.keyword_overrides[alias] then
exception.keyword_overrides[alias] = override_data
end
end
end
end
end
end
end
return export
0u20fhazph78gckjc4tw9vwhajr3x5b
Modul:etymon/data/text allowed
828
118630
373465
342827
2026-09-10T08:42:45Z
SNN95
2113
kemaskini
373465
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,
-- 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,
},
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
["zlw-lch"] = true, -- Lechitic
},
}
kgow4gb0ziadv2y7k1g7wp62mgl1vf0
Pengguna:Mirlim/atma bahasa
2
136565
373446
368816
2026-09-10T07:14:49Z
Mirlim
8057
373446
wikitext
text/x-wiki
: '''Khamis, pukul 3 petang di Perak FM'''<br/>Segmen 1: Uniknya Dialek Perak<br/>Segmen 2: Kesalahan Lazim<br/>Segmen 3: Biasakan yang Betul, Betulkan yang Biasa<br/>Segmen 4: Perbendaharaan kata<br/>Segmen 5: Peribahasa<br/>Segmen 6: Imbas Loghat Perak (minggu lepas) & Kuiz (05-545 8559)
----
; Intro (nanti baiki)
; UDP
: bejijio - meleleh
: bejerobon - bertindan/berlonggok
: berpiang-piang - pening lalat
: nyambé - sambil?
: robek - kupas
: tereban - balig batu
: nyadin - buat tak peduli
; Peribahasa
: meniup api dalam air?
: seperti air dengan asap - tak dapat dpshkn
: api padam puntung hanyut, kami tak di situ lagi - slesai?
==Ogos 2026==
===06-08-26===
; UDP
: [[sepasei]] - satu hal
: [[sepicin]] - sekejap
: [[serogoh]] - marah dengan menengking
: [[serokop]] - menutup
: [[setumbar]] - suatu masa, suatu ketika
; Perbendaharaan kata
: [[lejas]] - telus
: [[sebam]] - lusuh, luntur warnanya, pucat
: [[ruai]] - lobi hotel
: [[rumpang]] - sela waktu
: [[leja]]? - marah
; Peribahasa
: [[bagai lalang ditiup angin]] - tidak tetap pendirian
: [[bagai galah di tengah arus]] - selalu keluh-kesah
: [[bagai berumah di tepi tebing]] - selalu dalam ketakutan
: [[bagai bulan dengan matahari]] - sama-sama indah sama-sama cantik, [[bagai pinang dibelah dua]]
: [[bagai lebah menghimpun madu]] - orang yang sangat rajin
; Imbas Loghat Perak
: [[gincah]] - menggunakan air berlebih-lebihan
: [[gincang]] - pantas dan cekap melakukan pekerjaan, lincah
: [[goyo]] - keadaan berdiri atau berjalan secara terhuyung-hayang
===13-08-26===
; UDP
: (nanti bukak rakaman)
; Kesalahan Lazim
: perkarangan > pekarangan
: persaraan > persaraan
: penglibatan > pelibatan
: perlaksanaan > pelaksanaan
: kepimpinan > kepemimpinan
: pesiaran (jalan) vs persiaran
: penghawa dingin > pendingin hawa
; BYBBYB
: bilik persalinan (salin baju) > bilik acu (cuba baju)
: temu janji > janji temu (janji dulu baru temu, hukum DM)
: sampin > samping
: ves > rompi?
; Perbendaharaan kata
: [[suria kanta]]: kanta pembesar
: [[tetunggul]]
:: 1. panji-panji, bendera
:: 2. warna-warna di kaki langit, aurora borealis
: [[gencana]]: bencana, godaan, gangguan yang bawa bahaya
: [[beterangan]]: kediaman, tempat tinggal, tempat bermalam, tempat berteduh, tempat perlindungan
; Puisi tradisional
: Gurindam
; Imbas Loghat Perak
: sepasei: sepakat sepadan
: sepicin: seminit, sekejap
: serogoh:
:: 1. menceroboh tanpa izin
:: 2. marah
: serokop: tekup dari atas
: setumbar:
:: 1. seketika
:: 2. air yang penuh
===27-08-2026===
; UDP
: menyongèh - banyak cakap, merungut
: berambu - berselerak, tak teratur
: cempere (Kuala), cemperè (P. Tengah), cempèra (baku) - nakal, suka buat kacau
: membongai - terpinga-pinga, tercengang-cengang
: kécah - pecah
: cerèpèk - cakap tak henti
; Kesalahan Lazim
: ianya > ia
:: ia dan -nya dua-dua kata ganti
: mereka-mereka > mereka
:: Dialek Perak: mereka - dème; teman, awok, aye - saya
: terpaling > ter- atau paling
:: Gen Z guna [[terpaling]] untuk gurauan, selain itu [[sumpah]]
; BYBBYB
: submit > serahkan
: meeting > mesyuarat
: MC > cuti sakit
: update > kemaskini
: follow up > susulan
: dateline > tarikh akhir
: briefing > taklimat
; Perbendaharaan Kata
: (ambil dari lirik Di Ambang Wati - Wings)
: gita - lagu, nyanyian, syair atau puisi dilagukan, pujian, sanjungan
: kama - cinta, asmara, rindu, keinginan, hasrat
: citra - keperibadian, imej, gambaran
: wati - wanita, angkasa, langit
; Peribahasa
: jangan bermain di air keruh - jangan tiru buatan yang buruk
: jangan fikir air pasang sahaja - jangan fikir nasib baik sahaja
: jangan dengar siul ular - jangan terpedaya dengan musuh
: jangan bangkit harimau yang tidur
: jangan ditentang matahari condong
: jangan ditegakkan benang yang basah
: jangan difikir yang dicubit segantang ???
===10-09-26===
* dia duk sebut nama-nama tempat di Perak dalam Dialek Perak
: cangkat - tempat tinggi
:
rc0yhq9g0w5ytfs0ta77nnsje1b8soj
373447
373446
2026-09-10T07:26:59Z
Mirlim
8057
/* 10-09-26 */
373447
wikitext
text/x-wiki
: '''Khamis, pukul 3 petang di Perak FM'''<br/>Segmen 1: Uniknya Dialek Perak<br/>Segmen 2: Kesalahan Lazim<br/>Segmen 3: Biasakan yang Betul, Betulkan yang Biasa<br/>Segmen 4: Perbendaharaan kata<br/>Segmen 5: Peribahasa<br/>Segmen 6: Imbas Loghat Perak (minggu lepas) & Kuiz (05-545 8559)
----
; Intro (nanti baiki)
; UDP
: bejijio - meleleh
: bejerobon - bertindan/berlonggok
: berpiang-piang - pening lalat
: nyambé - sambil?
: robek - kupas
: tereban - balig batu
: nyadin - buat tak peduli
; Peribahasa
: meniup api dalam air?
: seperti air dengan asap - tak dapat dpshkn
: api padam puntung hanyut, kami tak di situ lagi - slesai?
==Ogos 2026==
===06-08-26===
; UDP
: [[sepasei]] - satu hal
: [[sepicin]] - sekejap
: [[serogoh]] - marah dengan menengking
: [[serokop]] - menutup
: [[setumbar]] - suatu masa, suatu ketika
; Perbendaharaan kata
: [[lejas]] - telus
: [[sebam]] - lusuh, luntur warnanya, pucat
: [[ruai]] - lobi hotel
: [[rumpang]] - sela waktu
: [[leja]]? - marah
; Peribahasa
: [[bagai lalang ditiup angin]] - tidak tetap pendirian
: [[bagai galah di tengah arus]] - selalu keluh-kesah
: [[bagai berumah di tepi tebing]] - selalu dalam ketakutan
: [[bagai bulan dengan matahari]] - sama-sama indah sama-sama cantik, [[bagai pinang dibelah dua]]
: [[bagai lebah menghimpun madu]] - orang yang sangat rajin
; Imbas Loghat Perak
: [[gincah]] - menggunakan air berlebih-lebihan
: [[gincang]] - pantas dan cekap melakukan pekerjaan, lincah
: [[goyo]] - keadaan berdiri atau berjalan secara terhuyung-hayang
===13-08-26===
; UDP
: (nanti bukak rakaman)
; Kesalahan Lazim
: perkarangan > pekarangan
: persaraan > persaraan
: penglibatan > pelibatan
: perlaksanaan > pelaksanaan
: kepimpinan > kepemimpinan
: pesiaran (jalan) vs persiaran
: penghawa dingin > pendingin hawa
; BYBBYB
: bilik persalinan (salin baju) > bilik acu (cuba baju)
: temu janji > janji temu (janji dulu baru temu, hukum DM)
: sampin > samping
: ves > rompi?
; Perbendaharaan kata
: [[suria kanta]]: kanta pembesar
: [[tetunggul]]
:: 1. panji-panji, bendera
:: 2. warna-warna di kaki langit, aurora borealis
: [[gencana]]: bencana, godaan, gangguan yang bawa bahaya
: [[beterangan]]: kediaman, tempat tinggal, tempat bermalam, tempat berteduh, tempat perlindungan
; Puisi tradisional
: Gurindam
; Imbas Loghat Perak
: sepasei: sepakat sepadan
: sepicin: seminit, sekejap
: serogoh:
:: 1. menceroboh tanpa izin
:: 2. marah
: serokop: tekup dari atas
: setumbar:
:: 1. seketika
:: 2. air yang penuh
===27-08-2026===
; UDP
: menyongèh - banyak cakap, merungut
: berambu - berselerak, tak teratur
: cempere (Kuala), cemperè (P. Tengah), cempèra (baku) - nakal, suka buat kacau
: membongai - terpinga-pinga, tercengang-cengang
: kécah - pecah
: cerèpèk - cakap tak henti
; Kesalahan Lazim
: ianya > ia
:: ia dan -nya dua-dua kata ganti
: mereka-mereka > mereka
:: Dialek Perak: mereka - dème; teman, awok, aye - saya
: terpaling > ter- atau paling
:: Gen Z guna [[terpaling]] untuk gurauan, selain itu [[sumpah]]
; BYBBYB
: submit > serahkan
: meeting > mesyuarat
: MC > cuti sakit
: update > kemaskini
: follow up > susulan
: dateline > tarikh akhir
: briefing > taklimat
; Perbendaharaan Kata
: (ambil dari lirik Di Ambang Wati - Wings)
: gita - lagu, nyanyian, syair atau puisi dilagukan, pujian, sanjungan
: kama - cinta, asmara, rindu, keinginan, hasrat
: citra - keperibadian, imej, gambaran
: wati - wanita, angkasa, langit
; Peribahasa
: jangan bermain di air keruh - jangan tiru buatan yang buruk
: jangan fikir air pasang sahaja - jangan fikir nasib baik sahaja
: jangan dengar siul ular - jangan terpedaya dengan musuh
: jangan bangkit harimau yang tidur
: jangan ditentang matahari condong
: jangan ditegakkan benang yang basah
: jangan difikir yang dicubit segantang ???
===10-09-26===
; UDP
* dia duk sebut nama-nama tempat di Perak dalam Dialek Perak
: cangkat - tempat tinggi
; Segmen Tatabahasa (?)
* kata baynak makna
: [[asal]] - mula, sebaik sahaja, pangkal
: [[mereka]] - KG3, merancang, mencipta
: [[kesan]] - tanda, sesuatu yg timbul, pengaruh yg timbul dari menyaksikan atau mendengar sesatu
; BYBBYB
* jenama yang sinonim sehingga terbawa-bawa
: [[Colgate]] > [[ubat gigi]]
: [[Maggi]] > [[mi]] [[segera]]
: [[Kodak]] > [[filem]] [[kamera]]
: [[Pampers]] > [[lampin]] [[pakai buang]]
: [[Tupperware]] > [[bekas]] [[makanan]] (Perak ''siã'')
kw80397r4copkbais78ld90ba500h52
373448
373447
2026-09-10T07:32:04Z
Mirlim
8057
373448
wikitext
text/x-wiki
: '''Khamis, pukul 3 petang di Perak FM'''<br/>Segmen 1: Uniknya Dialek Perak<br/>Segmen 2: Tatabahasa/Kesalahan Lazim<br/>Segmen 3: Biasakan yang Betul, Betulkan yang Biasa<br/>Segmen 4: Perbendaharaan kata<br/>Segmen 5: Peribahasa<br/>Segmen 6: Imbas Loghat Perak (minggu lepas) & Kuiz (05-545 8559)
----
; Intro (nanti baiki)
; UDP
: bejijio - meleleh
: bejerobon - bertindan/berlonggok
: berpiang-piang - pening lalat
: nyambé - sambil?
: robek - kupas
: tereban - balig batu
: nyadin - buat tak peduli
; Peribahasa
: meniup api dalam air?
: seperti air dengan asap - tak dapat dpshkn
: api padam puntung hanyut, kami tak di situ lagi - slesai?
==Ogos 2026==
===06-08-26===
; UDP
: [[sepasei]] - satu hal
: [[sepicin]] - sekejap
: [[serogoh]] - marah dengan menengking
: [[serokop]] - menutup
: [[setumbar]] - suatu masa, suatu ketika
; Perbendaharaan kata
: [[lejas]] - telus
: [[sebam]] - lusuh, luntur warnanya, pucat
: [[ruai]] - lobi hotel
: [[rumpang]] - sela waktu
: [[leja]]? - marah
; Peribahasa
: [[bagai lalang ditiup angin]] - tidak tetap pendirian
: [[bagai galah di tengah arus]] - selalu keluh-kesah
: [[bagai berumah di tepi tebing]] - selalu dalam ketakutan
: [[bagai bulan dengan matahari]] - sama-sama indah sama-sama cantik, [[bagai pinang dibelah dua]]
: [[bagai lebah menghimpun madu]] - orang yang sangat rajin
; Imbas Loghat Perak
: [[gincah]] - menggunakan air berlebih-lebihan
: [[gincang]] - pantas dan cekap melakukan pekerjaan, lincah
: [[goyo]] - keadaan berdiri atau berjalan secara terhuyung-hayang
===13-08-26===
; UDP
: (nanti bukak rakaman)
; Kesalahan Lazim
: perkarangan > pekarangan
: persaraan > persaraan
: penglibatan > pelibatan
: perlaksanaan > pelaksanaan
: kepimpinan > kepemimpinan
: pesiaran (jalan) vs persiaran
: penghawa dingin > pendingin hawa
; BYBBYB
: bilik persalinan (salin baju) > bilik acu (cuba baju)
: temu janji > janji temu (janji dulu baru temu, hukum DM)
: sampin > samping
: ves > rompi?
; Perbendaharaan kata
: [[suria kanta]]: kanta pembesar
: [[tetunggul]]
:: 1. panji-panji, bendera
:: 2. warna-warna di kaki langit, aurora borealis
: [[gencana]]: bencana, godaan, gangguan yang bawa bahaya
: [[beterangan]]: kediaman, tempat tinggal, tempat bermalam, tempat berteduh, tempat perlindungan
; Puisi tradisional
: Gurindam
; Imbas Loghat Perak
: sepasei: sepakat sepadan
: sepicin: seminit, sekejap
: serogoh:
:: 1. menceroboh tanpa izin
:: 2. marah
: serokop: tekup dari atas
: setumbar:
:: 1. seketika
:: 2. air yang penuh
===27-08-2026===
; UDP
: menyongèh - banyak cakap, merungut
: berambu - berselerak, tak teratur
: cempere (Kuala), cemperè (P. Tengah), cempèra (baku) - nakal, suka buat kacau
: membongai - terpinga-pinga, tercengang-cengang
: kécah - pecah
: cerèpèk - cakap tak henti
; Kesalahan Lazim
: ianya > ia
:: ia dan -nya dua-dua kata ganti
: mereka-mereka > mereka
:: Dialek Perak: mereka - dème; teman, awok, aye - saya
: terpaling > ter- atau paling
:: Gen Z guna [[terpaling]] untuk gurauan, selain itu [[sumpah]]
; BYBBYB
: submit > serahkan
: meeting > mesyuarat
: MC > cuti sakit
: update > kemaskini
: follow up > susulan
: dateline > tarikh akhir
: briefing > taklimat
; Perbendaharaan Kata
: (ambil dari lirik Di Ambang Wati - Wings)
: gita - lagu, nyanyian, syair atau puisi dilagukan, pujian, sanjungan
: kama - cinta, asmara, rindu, keinginan, hasrat
: citra - keperibadian, imej, gambaran
: wati - wanita, angkasa, langit
; Peribahasa
: jangan bermain di air keruh - jangan tiru buatan yang buruk
: jangan fikir air pasang sahaja - jangan fikir nasib baik sahaja
: jangan dengar siul ular - jangan terpedaya dengan musuh
: jangan bangkit harimau yang tidur
: jangan ditentang matahari condong
: jangan ditegakkan benang yang basah
: jangan difikir yang dicubit segantang ???
===10-09-26===
; UDP
* dia duk sebut nama-nama tempat di Perak dalam Dialek Perak
: cangkat - tempat tinggi
; Segmen Tatabahasa (?)
* kata baynak makna
: [[asal]] - mula, sebaik sahaja, pangkal
: [[mereka]] - KG3, merancang, mencipta
: [[kesan]] - tanda, sesuatu yg timbul, pengaruh yg timbul dari menyaksikan atau mendengar sesatu
; BYBBYB
* jenama yang sinonim sehingga terbawa-bawa
: [[Colgate]] > [[ubat gigi]]
: [[Maggi]] > [[mi]] [[segera]]
: [[Kodak]] > [[filem]] [[kamera]]
: [[Pampers]] > [[lampin]] [[pakai buang]]
: [[Tupperware]] > [[bekas]] [[makanan]] (Perak ''siã'')
9bwvnt0dklwfea35sq735y16fpxdioj
373453
373448
2026-09-10T07:53:38Z
Mirlim
8057
373453
wikitext
text/x-wiki
===Atma Bahasa===
: '''Khamis, pukul 3 petang di Perak FM'''<br/>Segmen 1: Uniknya Dialek Perak<br/>Segmen 2: Tatabahasa/Kesalahan Lazim<br/>Segmen 3: Biasakan yang Betul, Betulkan yang Biasa<br/>Segmen 4: Perbendaharaan kata<br/>Segmen 5: Peribahasa<br/>Segmen 6: Imbas Loghat Perak (minggu lepas) & Kuiz (05-545 8559)
----
<div class="toccolours mw-collapsible mw-collapsed" style="width:400px; overflow:auto;"><div style="font-weight:bold;">
Intro (nanti baiki)
</div><div class="mw-collapsible-content">
; UDP
: bejijio - meleleh
: bejerobon - bertindan/berlonggok
: berpiang-piang - pening lalat
: nyambé - sambil?
: robek - kupas
: tereban - balig batu
: nyadin - buat tak peduli
; Peribahasa
: meniup api dalam air?
: seperti air dengan asap - tak dapat dpshkn
: api padam puntung hanyut, kami tak di situ lagi - slesai?
: jangan bijak terpijak, biarlah bodoh bersuluh -
: laksana bunga dedap, sungguh merah berbau tidak -
: belum lepas asap di dapur, sudah mahu menjadi langit -
</div></div>
==Ogos 2026==
===06-08-26===
; UDP
: [[sepasei]] - satu hal
: [[sepicin]] - sekejap
: [[serogoh]] - marah dengan menengking
: [[serokop]] - menutup
: [[setumbar]] - suatu masa, suatu ketika
; Perbendaharaan kata
: [[lejas]] - telus
: [[sebam]] - lusuh, luntur warnanya, pucat
: [[ruai]] - lobi hotel
: [[rumpang]] - sela waktu
: [[leja]]? - marah
; Peribahasa
: [[bagai lalang ditiup angin]] - tidak tetap pendirian
: [[bagai galah di tengah arus]] - selalu keluh-kesah
: [[bagai berumah di tepi tebing]] - selalu dalam ketakutan
: [[bagai bulan dengan matahari]] - sama-sama indah sama-sama cantik, [[bagai pinang dibelah dua]]
: [[bagai lebah menghimpun madu]] - orang yang sangat rajin
; Imbas Loghat Perak
: [[gincah]] - menggunakan air berlebih-lebihan
: [[gincang]] - pantas dan cekap melakukan pekerjaan, lincah
: [[goyo]] - keadaan berdiri atau berjalan secara terhuyung-hayang
===13-08-26===
; UDP
: (nanti bukak rakaman)
; Kesalahan Lazim
: perkarangan > pekarangan
: persaraan > persaraan
: penglibatan > pelibatan
: perlaksanaan > pelaksanaan
: kepimpinan > kepemimpinan
: pesiaran (jalan) vs persiaran
: penghawa dingin > pendingin hawa
; BYBBYB
: bilik persalinan (salin baju) > bilik acu (cuba baju)
: temu janji > janji temu (janji dulu baru temu, hukum DM)
: sampin > samping
: ves > rompi?
; Perbendaharaan kata
: [[suria kanta]]: kanta pembesar
: [[tetunggul]]
:: 1. panji-panji, bendera
:: 2. warna-warna di kaki langit, aurora borealis
: [[gencana]]: bencana, godaan, gangguan yang bawa bahaya
: [[beterangan]]: kediaman, tempat tinggal, tempat bermalam, tempat berteduh, tempat perlindungan
; Puisi tradisional
: Gurindam
; Imbas Loghat Perak
: sepasei: sepakat sepadan
: sepicin: seminit, sekejap
: serogoh:
:: 1. menceroboh tanpa izin
:: 2. marah
: serokop: tekup dari atas
: setumbar:
:: 1. seketika
:: 2. air yang penuh
===27-08-2026===
; UDP
: menyongèh - banyak cakap, merungut
: berambu - berselerak, tak teratur
: cempere (Kuala), cemperè (P. Tengah), cempèra (baku) - nakal, suka buat kacau
: membongai - terpinga-pinga, tercengang-cengang
: kécah - pecah
: cerèpèk - cakap tak henti
; Kesalahan Lazim
: ianya > ia
:: ia dan -nya dua-dua kata ganti
: mereka-mereka > mereka
:: Dialek Perak: mereka - dème; teman, awok, aye - saya
: terpaling > ter- atau paling
:: Gen Z guna [[terpaling]] untuk gurauan, selain itu [[sumpah]]
; BYBBYB
: submit > serahkan
: meeting > mesyuarat
: MC > cuti sakit
: update > kemaskini
: follow up > susulan
: dateline > tarikh akhir
: briefing > taklimat
; Perbendaharaan Kata
* bahas lirik Di Ambang Wati - Wings
: gita - lagu, nyanyian, syair atau puisi dilagukan, pujian, sanjungan
: kama - cinta, asmara, rindu, keinginan, hasrat
: citra - keperibadian, imej, gambaran
: wati - wanita, angkasa, langit
; Peribahasa
: jangan bermain di air keruh - jangan tiru buatan yang buruk
: jangan fikir air pasang sahaja - jangan fikir nasib baik sahaja
: jangan dengar siul ular - jangan terpedaya dengan musuh
: jangan bangkit harimau yang tidur
: jangan ditentang matahari condong
: jangan ditegakkan benang yang basah
: jangan difikir yang dicubit segantang ???
===10-09-26===
; UDP
* dia duk sebut nama-nama tempat di Perak dalam Dialek Perak
: cangkat - tempat tinggi
; Segmen Tatabahasa
* kata baynak makna
: [[asal]] - mula, sebaik sahaja, pangkal
: [[mereka]] - KG3, merancang, mencipta
: [[kesan]] - tanda, sesuatu yg timbul, pengaruh yg timbul dari menyaksikan atau mendengar sesatu
; BYBBYB
* jenama yang sinonim sehingga terbawa-bawa
: [[Colgate]] > [[ubat gigi]]
: [[Maggi]] > [[mi]] [[segera]]
: [[Kodak]] > [[filem]] [[kamera]]
: [[Pampers]] > [[lampin]] [[pakai buang]]
: [[Tupperware]] > [[bekas]] [[makanan]] (Perak ''siã'')
; Perbendaharaan Kata
* bahas lirik Cindai - Siti Nurhaliza
: dil mas cdrny intan bbtl lgn - kehidupan yang mewah vs sederhana
: biduk lyrny krts prhu sbrg lwt brpi - prlmbgn mnusia yg srb lemah, yg prlu mnmpuh ujian yg bsr
; Peribahasa
* mengenai orang tua
*: (<code>mata sudah mula kabur, uban sudah mula berterabur</code>)
: tua2 tupai tak tidur ats tnh - org tua stiasa gembira hidupnya
: tua2 terung masam - org tua brprngai muda
: tua2 tlur ayam - tua sedikit shj, jurang usia yg kcil
: pinang tua merah ekor - prmpuan brumur brkelakuan gadis muda
: tua2 kelapa - smakin tua smakin baik prgai/ byk ilmu
: tua2 keladi - smakin tua smakin miang
: gayung tua - kata/keputusan dr org tua biasanya lebih tepat/brnas
gl19zdm8lrj83g8wvuligryjka6ar47
Wikikamus:dtp/monontian
4
142734
373426
371160
2026-09-09T15:45:07Z
Lynumiss
5957
Tambah kata
373426
wikitext
text/x-wiki
==Bahasa {{bahasa|dtp}}==
===Kata kerja===
{{inti|dtp|kata kerja}}
# bunting {{cp|dtp|'''Monontian''' ilo tingau.|Kucing itu '''[[bunting]]'''.}}
# mengandung {{cp|dtp|'''Monontian''' i taka ku.|Kakak saya '''[[mengandung]]'''.}}
gaq6mt89g1bykvirlrvpq084wezb67k
Wikikamus:dtp/monungkamang
4
144603
373425
2026-09-09T15:38:25Z
Lynumiss
5957
Tambah kata
373425
wikitext
text/x-wiki
==Bahasa {{bahasa|dtp}}==
===Kata kerja===
{{inti|dtp|kata kerja}}
# {{label|1=dtp|2=Bundu Liwan|3=Sabah}} merangkak {{cp|dtp|Koilo no '''monungkamang''' i tadi ku.|Adik saya sudah tahu '''[[merangkak]]'''.}}
gq7vrz22knfp5qwlzskqchhib297l8c
Wikikamus:dtp/mogolimumu
4
144604
373427
2026-09-09T15:49:29Z
Lynumiss
5957
Tambah kata
373427
wikitext
text/x-wiki
==Bahasa {{bahasa|dtp}}==
===Kata kerja===
{{inti|dtp|kata kerja}}
# {{label|1=dtp|2=Bundu Liwan|3=Sabah}} berdalih {{cp|dtp|'''Mogolimumu''' tomod i Julis soira nuhot di mongingia' Jinol.|Julis '''[[berdalih]]''' ketika disoal oleh cikgu Jinol.}}
isx7qpidjpeuqi5h90uoj8pr6iis8sv
Acara:WikiKata Bulan Bahasa 2026
1728
144605
373428
2026-09-09T16:49:35Z
Ultron90
4762
Mencipta laman baru dengan kandungan 'WikiKata Bulan Bahasa 2026 is a workshop conducted in conjunction of Bulan Bahasa 2026 in Singapore. The workshop aims to teach participants on how to edit on Wikimedia projects in the Malay language such as Malay Wikipedia, Malay Wiktionary, Malay Wikibooks, and Wikidata.'
373428
wikitext
text/x-wiki
WikiKata Bulan Bahasa 2026 is a workshop conducted in conjunction of Bulan Bahasa 2026 in Singapore. The workshop aims to teach participants on how to edit on Wikimedia projects in the Malay language such as Malay Wikipedia, Malay Wiktionary, Malay Wikibooks, and Wikidata.
7d48z51ib90if4ru5hcocvu7ji95zvn
373437
373428
2026-09-09T17:33:54Z
Ultron90
4762
373437
wikitext
text/x-wiki
{{multiprojectbar
| title = WikiKata Bulan Bahasa 2026
| wikipedia_lang1 = ms | wikipedia_title1 = Event:WikiKata Bulan Bahasa 2026 | wikipedia_label1 = Wikipedia (ms)
| wikibooks_lang1 = en | wikibooks_title1 = Event:WikiKata Bulan Bahasa 2026 | wikibooks_label1 = Wikibuku (ms)
| wiktionary_lang1 = en | wiktionary_title1 = Event:WikiKata Bulan Bahasa 2026 | wiktionary_label1 = Wikikamus (ms)
| wikidata = Event:WikiKata Bulan Bahasa 2026
}}
WikiKata Bulan Bahasa 2026 is a workshop conducted in conjunction of Bulan Bahasa 2026 in Singapore. The workshop aims to teach participants on how to edit on Wikimedia projects in the Malay language such as Malay Wikipedia, Malay Wiktionary, Malay Wikibooks, and Wikidata.
n6xq7tckxgmptckg7ht238hb30gk64m
373438
373437
2026-09-09T17:46:41Z
Ultron90
4762
373438
wikitext
text/x-wiki
{{multiprojectbar
| title = WikiKata Bulan Bahasa 2026
| meta = Event:WikiKata Bulan Bahasa 2026
| wikipedia_lang1 = ms | wikipedia_title1 = Event:WikiKata Bulan Bahasa 2026 | wikipedia_label1 = Wikipedia (ms)
| wikibooks_lang1 = en | wikibooks_title1 = Event:WikiKata Bulan Bahasa 2026 | wikibooks_label1 = Wikibuku (ms)
| wiktionary_lang1 = en | wiktionary_title1 = Event:WikiKata Bulan Bahasa 2026 | wiktionary_label1 = Wikikamus (ms)
| wikidata = Event:WikiKata Bulan Bahasa 2026
}}
WikiKata Bulan Bahasa 2026 is a workshop conducted in conjunction of Bulan Bahasa 2026 in Singapore. The workshop aims to teach participants on how to edit on Wikimedia projects in the Malay language such as Malay Wikipedia, Malay Wiktionary, Malay Wikibooks, and Wikidata.
cpgr0ppxpfzhpl2aywxib9ink6awrel
373439
373438
2026-09-09T17:47:10Z
Ultron90
4762
373439
wikitext
text/x-wiki
{{multiprojectbar
| title = WikiKata Bulan Bahasa 2026
| meta = Event:WikiKata Bulan Bahasa 2026
| wikipedia_lang1 = ms | wikipedia_title1 = Event:WikiKata Bulan Bahasa 2026 | wikipedia_label1 = Wikipedia (ms)
| wikibooks_lang1 = ms | wikibooks_title1 = Event:WikiKata Bulan Bahasa 2026 | wikibooks_label1 = Wikibuku (ms)
| wiktionary_lang1 = ms | wiktionary_title1 = Event:WikiKata Bulan Bahasa 2026 | wiktionary_label1 = Wikikamus (ms)
| wikidata = Event:WikiKata Bulan Bahasa 2026
}}
WikiKata Bulan Bahasa 2026 is a workshop conducted in conjunction of Bulan Bahasa 2026 in Singapore. The workshop aims to teach participants on how to edit on Wikimedia projects in the Malay language such as Malay Wikipedia, Malay Wiktionary, Malay Wikibooks, and Wikidata.
5508gfqkhbf58dk0kb4autnh8qbbjwd
Modul:multiprojectbar
828
144606
373429
2026-09-09T17:16:58Z
Ultron90
4762
Mencipta laman baru dengan kandungan 'local p = {} function p.main(frame) local args = frame:getParent().args local title = args.title or "Wikimedia Projects" -- Load TemplateStyles local styles = frame:extensionTag('templatestyles', '', { src = 'Template:multiprojectbar/styles.css' }) local html = mw.html.create('div'):addClass('wm-navbar') html:tag('div'):addClass('wm-navbar-title'):wikitext(title) local container = html:tag('div'):addClass('wm...'
373429
Scribunto
text/plain
local p = {}
function p.main(frame)
local args = frame:getParent().args
local title = args.title or "Wikimedia Projects"
-- Load TemplateStyles
local styles = frame:extensionTag('templatestyles', '', { src = 'Template:multiprojectbar/styles.css' })
local html = mw.html.create('div'):addClass('wm-navbar')
html:tag('div'):addClass('wm-navbar-title'):wikitext(title)
local container = html:tag('div'):addClass('wm-navbar-container')
-- Map interwiki prefixes to CSS classes for icons
local prefixClassMap = {
w = 'wm-wikipedia',
commons = 'wm-commons',
s = 'wm-wikisource',
d = 'wm-wikidata',
voy = 'wm-wikivoyage',
n = 'wm-wikinews',
wikt = 'wm-wiktionary',
b = 'wm-wikibooks',
q = 'wm-wikiquote',
v = 'wm-wikiversity',
species = 'wm-wikispecies',
m = 'wm-meta'
}
-- MODE 1: SINGLE-PROJECT MULTI-LANG (e.g., project = w)
if args.project and args.project ~= '' then
local project = args.project
local targetDefault = args.target or ''
local iconClass = prefixClassMap[project] or 'wm-lang'
local i = 1
while args['lang' .. i] or args['code' .. i] do
local code = args['code' .. i] or args['lang' .. i]
local langTitle = args['title' .. i] or args['page' .. i] or targetDefault
local label = args['label' .. i] or code:upper()
if code ~= '' then
local btn = container:tag('div'):addClass('wm-btn ' .. iconClass)
btn:wikitext(string.format('[[%s:%s:%s|%s]]', project, code, langTitle, label))
end
i = i + 1
end
-- MODE 2: MULTI-PROJECT (Supports single links and multi-language variants)
else
local projects = {
{ key = 'wikipedia', prefix = 'w', defaultLabel = 'Wikipedia', class = 'wm-wikipedia' },
{ key = 'commons', prefix = 'commons', defaultLabel = 'Commons', class = 'wm-commons' },
{ key = 'wikisource', prefix = 's', defaultLabel = 'Wikisource', class = 'wm-wikisource' },
{ key = 'wikidata', prefix = 'd', defaultLabel = 'Wikidata', class = 'wm-wikidata' },
{ key = 'wikivoyage', prefix = 'voy', defaultLabel = 'Wikivoyage', class = 'wm-wikivoyage' },
{ key = 'wikinews', prefix = 'n', defaultLabel = 'Wikinews', class = 'wm-wikinews' },
{ key = 'wiktionary', prefix = 'wikt', defaultLabel = 'Wiktionary', class = 'wm-wiktionary' },
{ key = 'wikibooks', prefix = 'b', defaultLabel = 'Wikibooks', class = 'wm-wikibooks' },
{ key = 'wikiquote', prefix = 'q', defaultLabel = 'Wikiquote', class = 'wm-wikiquote' },
{ key = 'wikiversity',prefix = 'v', defaultLabel = 'Wikiversity',class = 'wm-wikiversity' },
{ key = 'wikispecies',prefix = 'species', defaultLabel = 'Wikispecies',class = 'wm-wikispecies' },
{ key = 'meta', prefix = 'm', defaultLabel = 'Meta-Wiki', class = 'wm-meta' }
}
for _, pData in ipairs(projects) do
local baseVal = args[pData.key]
local baseLang = args[pData.key .. '_lang']
if baseVal and baseVal ~= '' then
local label = args[pData.key .. '_label'] or pData.defaultLabel
local langPrefix = (baseLang and baseLang ~= '') and (baseLang .. ':') or ''
local btn = container:tag('div'):addClass('wm-btn ' .. pData.class)
btn:wikitext(string.format('[[%s:%s%s|%s]]', pData.prefix, langPrefix, baseVal, label))
end
local j = 1
while args[pData.key .. '_lang' .. j] do
local langCode = args[pData.key .. '_lang' .. j]
local pageTitle = args[pData.key .. '_title' .. j] or args[pData.key] or ''
local customLabel = args[pData.key .. '_label' .. j] or (pData.defaultLabel .. ' (' .. langCode:upper() .. ')')
if langCode ~= '' and pageTitle ~= '' then
local btn = container:tag('div'):addClass('wm-btn ' .. pData.class)
btn:wikitext(string.format('[[%s:%s:%s|%s]]', pData.prefix, langCode, pageTitle, customLabel))
end
j = j + 1
end
end
end
return styles .. tostring(html)
end
return p
dxt018gb4zfc22khwe2eoylh07jduwg
373440
373429
2026-09-09T18:19:39Z
Ultron90
4762
373440
Scribunto
text/plain
local p = {}
function p.main(frame)
local args = frame:getParent().args
local title = args.title or "Wikimedia Projects"
-- Load TemplateStyles
local styles = frame:extensionTag('templatestyles', '', { src = 'Template:multiprojectbar/styles.css' })
local html = mw.html.create('div'):addClass('wm-navbar')
html:tag('div'):addClass('wm-navbar-title'):wikitext(title)
local container = html:tag('div'):addClass('wm-navbar-container')
-- Map interwiki prefixes to CSS classes for icons
local prefixClassMap = {
w = 'wm-wikipedia',
commons = 'wm-commons',
s = 'wm-wikisource',
d = 'wm-wikidata',
voy = 'wm-wikivoyage',
n = 'wm-wikinews',
wikt = 'wm-wiktionary',
b = 'wm-wikibooks',
q = 'wm-wikiquote',
v = 'wm-wikiversity',
species = 'wm-wikispecies',
m = 'wm-meta'
}
-- MODE 1: SINGLE-PROJECT MULTI-LANG (e.g., project = w)
if args.project and args.project ~= '' then
local project = args.project
local targetDefault = args.target or ''
local iconClass = prefixClassMap[project] or 'wm-lang'
local i = 1
while args['lang' .. i] or args['code' .. i] do
local code = args['code' .. i] or args['lang' .. i]
local langTitle = args['title' .. i] or args['page' .. i] or targetDefault
local label = args['label' .. i] or code:upper()
if code ~= '' then
local btn = container:tag('div'):addClass('wm-btn ' .. iconClass)
btn:wikitext(string.format('[[%s:%s:%s|%s]]', project, code, langTitle, label))
end
i = i + 1
end
-- MODE 2: MULTI-PROJECT (Supports single links and multi-language variants)
else
local projects = {
{ key = 'meta', prefix = 'm', defaultLabel = 'Meta-Wiki', class = 'wm-meta' },
{ key = 'wikipedia', prefix = 'w', defaultLabel = 'Wikipedia', class = 'wm-wikipedia' },
{ key = 'commons', prefix = 'commons', defaultLabel = 'Commons', class = 'wm-commons' },
{ key = 'wikisource', prefix = 's', defaultLabel = 'Wikisource', class = 'wm-wikisource' },
{ key = 'wikidata', prefix = 'd', defaultLabel = 'Wikidata', class = 'wm-wikidata' },
{ key = 'wikivoyage', prefix = 'voy', defaultLabel = 'Wikivoyage', class = 'wm-wikivoyage' },
{ key = 'wikinews', prefix = 'n', defaultLabel = 'Wikinews', class = 'wm-wikinews' },
{ key = 'wiktionary', prefix = 'wikt', defaultLabel = 'Wiktionary', class = 'wm-wiktionary' },
{ key = 'wikibooks', prefix = 'b', defaultLabel = 'Wikibooks', class = 'wm-wikibooks' },
{ key = 'wikiquote', prefix = 'q', defaultLabel = 'Wikiquote', class = 'wm-wikiquote' },
{ key = 'wikiversity',prefix = 'v', defaultLabel = 'Wikiversity',class = 'wm-wikiversity' },
{ key = 'wikispecies',prefix = 'species', defaultLabel = 'Wikispecies',class = 'wm-wikispecies' }
}
for _, pData in ipairs(projects) do
local baseVal = args[pData.key]
local baseLang = args[pData.key .. '_lang']
if baseVal and baseVal ~= '' then
local label = args[pData.key .. '_label'] or pData.defaultLabel
local langPrefix = (baseLang and baseLang ~= '') and (baseLang .. ':') or ''
local btn = container:tag('div'):addClass('wm-btn ' .. pData.class)
btn:wikitext(string.format('[[%s:%s%s|%s]]', pData.prefix, langPrefix, baseVal, label))
end
local j = 1
while args[pData.key .. '_lang' .. j] do
local langCode = args[pData.key .. '_lang' .. j]
local pageTitle = args[pData.key .. '_title' .. j] or args[pData.key] or ''
local customLabel = args[pData.key .. '_label' .. j] or (pData.defaultLabel .. ' (' .. langCode:upper() .. ')')
if langCode ~= '' and pageTitle ~= '' then
local btn = container:tag('div'):addClass('wm-btn ' .. pData.class)
btn:wikitext(string.format('[[%s:%s:%s|%s]]', pData.prefix, langCode, pageTitle, customLabel))
end
j = j + 1
end
end
end
return styles .. tostring(html)
end
return p
cnsaguk1obku8rw5y5mfy5e1184cwct
Templat:multiprojectbar
10
144607
373430
2026-09-09T17:17:27Z
Ultron90
4762
Mencipta laman baru dengan kandungan '<noinclude> This template creates a horizontal navigation bar linking to various Wikimedia projects and/or language editions. == Usage == === Multi-Project Mode === <pre> {{multiprojectbar | wikipedia = Main Page | commons = Category:Featured pictures | wikidata = Q1 }} </pre> === Multi-Language Mode (Unlimited Languages) === <pre> {{multiprojectbar | title = Wikipedia Languages | project = w | target = Solar System | lang1 = en | label1 = Engl...'
373430
wikitext
text/x-wiki
<noinclude>
This template creates a horizontal navigation bar linking to various Wikimedia projects and/or language editions.
== Usage ==
=== Multi-Project Mode ===
<pre>
{{multiprojectbar
| wikipedia = Main Page
| commons = Category:Featured pictures
| wikidata = Q1
}}
</pre>
=== Multi-Language Mode (Unlimited Languages) ===
<pre>
{{multiprojectbar
| title = Wikipedia Languages
| project = w
| target = Solar System
| lang1 = en | label1 = English
| lang2 = fr | label2 = Français | title2 = Système solaire
| lang3 = ja | label3 = 日本語 | title3 = 太陽系
}}
</pre>
=== Combined Multi-Project + Multi-Language Mode ===
<pre>
{{multiprojectbar
| title = Combined Projects
| wikipedia_lang1 = en | wikipedia_title1 = Physics | wikipedia_label1 = Wikipedia (EN)
| wikipedia_lang2 = fr | wikipedia_title2 = Physique | wikipedia_label2 = Wikipedia (FR)
| wikibooks_lang1 = en | wikibooks_title1 = Physics | wikibooks_label1 = Wikibooks (EN)
| wikibooks_lang2 = es | wikibooks_title2 = Física | wikibooks_label2 = Wikibooks (ES)
| commons = Category:Physics
| wikidata = Q413
}}
</pre>
[[Category:Navigation templates]]
</noinclude><includeonly>{{#invoke:Multiprojectbar|main}}</includeonly>
e7hehtdd8cap7l7ef8hsgt9t1xrc2nd
373432
373430
2026-09-09T17:18:54Z
Ultron90
4762
373432
wikitext
text/x-wiki
<noinclude>
This template creates a horizontal navigation bar linking to various Wikimedia projects and/or language editions.
== Usage ==
=== Multi-Project Mode ===
{{multiprojectbar
| wikipedia = Main Page
| commons = Category:Featured pictures
| wikidata = Q1
}}
<pre>
{{multiprojectbar
| wikipedia = Main Page
| commons = Category:Featured pictures
| wikidata = Q1
}}
</pre>
=== Multi-Language Mode (Unlimited Languages) ===
<pre>
{{multiprojectbar
| title = Wikipedia Languages
| project = w
| target = Solar System
| lang1 = en | label1 = English
| lang2 = fr | label2 = Français | title2 = Système solaire
| lang3 = ja | label3 = 日本語 | title3 = 太陽系
}}
</pre>
=== Combined Multi-Project + Multi-Language Mode ===
<pre>
{{multiprojectbar
| title = Combined Projects
| wikipedia_lang1 = en | wikipedia_title1 = Physics | wikipedia_label1 = Wikipedia (EN)
| wikipedia_lang2 = fr | wikipedia_title2 = Physique | wikipedia_label2 = Wikipedia (FR)
| wikibooks_lang1 = en | wikibooks_title1 = Physics | wikibooks_label1 = Wikibooks (EN)
| wikibooks_lang2 = es | wikibooks_title2 = Física | wikibooks_label2 = Wikibooks (ES)
| commons = Category:Physics
| wikidata = Q413
}}
</pre>
[[Category:Navigation templates]]
</noinclude><includeonly>{{#invoke:Multiprojectbar|main}}</includeonly>
60d79h5j02qnyp174bspqx7hmo73wdf
373433
373432
2026-09-09T17:19:30Z
Ultron90
4762
373433
wikitext
text/x-wiki
<noinclude>
This template creates a horizontal navigation bar linking to various Wikimedia projects and/or language editions.
== Usage ==
=== Multi-Project Mode ===
{{multiprojectbar
| wikipedia = Main Page
| commons = Category:Featured pictures
| wikidata = Q1
}}
<pre>
{{multiprojectbar
| wikipedia = Main Page
| commons = Category:Featured pictures
| wikidata = Q1
}}
</pre>
=== Multi-Language Mode (Unlimited Languages) ===
<pre>
{{multiprojectbar
| title = Wikipedia Languages
| project = w
| target = Solar System
| lang1 = en | label1 = English
| lang2 = fr | label2 = Français | title2 = Système solaire
| lang3 = ja | label3 = 日本語 | title3 = 太陽系
}}
</pre>
=== Combined Multi-Project + Multi-Language Mode ===
<pre>
{{multiprojectbar
| title = Combined Projects
| wikipedia_lang1 = en | wikipedia_title1 = Physics | wikipedia_label1 = Wikipedia (EN)
| wikipedia_lang2 = fr | wikipedia_title2 = Physique | wikipedia_label2 = Wikipedia (FR)
| wikibooks_lang1 = en | wikibooks_title1 = Physics | wikibooks_label1 = Wikibooks (EN)
| wikibooks_lang2 = es | wikibooks_title2 = Física | wikibooks_label2 = Wikibooks (ES)
| commons = Category:Physics
| wikidata = Q413
}}
</pre>
[[Category:Navigation templates]]
</noinclude><includeonly>{{#invoke:multiprojectbar|main}}</includeonly>
3fnhz3tmxgs0gee2f63tl9ep61d3utx
373434
373433
2026-09-09T17:19:58Z
Ultron90
4762
/* Multi-Language Mode (Unlimited Languages) */
373434
wikitext
text/x-wiki
<noinclude>
This template creates a horizontal navigation bar linking to various Wikimedia projects and/or language editions.
== Usage ==
=== Multi-Project Mode ===
{{multiprojectbar
| wikipedia = Main Page
| commons = Category:Featured pictures
| wikidata = Q1
}}
<pre>
{{multiprojectbar
| wikipedia = Main Page
| commons = Category:Featured pictures
| wikidata = Q1
}}
</pre>
=== Multi-Language Mode (Unlimited Languages) ===
{{multiprojectbar
| title = Wikipedia Languages
| project = w
| target = Solar System
| lang1 = en | label1 = English
| lang2 = fr | label2 = Français | title2 = Système solaire
| lang3 = ja | label3 = 日本語 | title3 = 太陽系
}}
<pre>
{{multiprojectbar
| title = Wikipedia Languages
| project = w
| target = Solar System
| lang1 = en | label1 = English
| lang2 = fr | label2 = Français | title2 = Système solaire
| lang3 = ja | label3 = 日本語 | title3 = 太陽系
}}
</pre>
=== Combined Multi-Project + Multi-Language Mode ===
<pre>
{{multiprojectbar
| title = Combined Projects
| wikipedia_lang1 = en | wikipedia_title1 = Physics | wikipedia_label1 = Wikipedia (EN)
| wikipedia_lang2 = fr | wikipedia_title2 = Physique | wikipedia_label2 = Wikipedia (FR)
| wikibooks_lang1 = en | wikibooks_title1 = Physics | wikibooks_label1 = Wikibooks (EN)
| wikibooks_lang2 = es | wikibooks_title2 = Física | wikibooks_label2 = Wikibooks (ES)
| commons = Category:Physics
| wikidata = Q413
}}
</pre>
[[Category:Navigation templates]]
</noinclude><includeonly>{{#invoke:multiprojectbar|main}}</includeonly>
hgmqipkuej12fqx4kfp8caiyfk89ge6
373435
373434
2026-09-09T17:20:29Z
Ultron90
4762
/* Combined Multi-Project + Multi-Language Mode */
373435
wikitext
text/x-wiki
<noinclude>
This template creates a horizontal navigation bar linking to various Wikimedia projects and/or language editions.
== Usage ==
=== Multi-Project Mode ===
{{multiprojectbar
| wikipedia = Main Page
| commons = Category:Featured pictures
| wikidata = Q1
}}
<pre>
{{multiprojectbar
| wikipedia = Main Page
| commons = Category:Featured pictures
| wikidata = Q1
}}
</pre>
=== Multi-Language Mode (Unlimited Languages) ===
{{multiprojectbar
| title = Wikipedia Languages
| project = w
| target = Solar System
| lang1 = en | label1 = English
| lang2 = fr | label2 = Français | title2 = Système solaire
| lang3 = ja | label3 = 日本語 | title3 = 太陽系
}}
<pre>
{{multiprojectbar
| title = Wikipedia Languages
| project = w
| target = Solar System
| lang1 = en | label1 = English
| lang2 = fr | label2 = Français | title2 = Système solaire
| lang3 = ja | label3 = 日本語 | title3 = 太陽系
}}
</pre>
=== Combined Multi-Project + Multi-Language Mode ===
{{multiprojectbar
| title = Combined Projects
| wikipedia_lang1 = en | wikipedia_title1 = Physics | wikipedia_label1 = Wikipedia (EN)
| wikipedia_lang2 = fr | wikipedia_title2 = Physique | wikipedia_label2 = Wikipedia (FR)
| wikibooks_lang1 = en | wikibooks_title1 = Physics | wikibooks_label1 = Wikibooks (EN)
| wikibooks_lang2 = es | wikibooks_title2 = Física | wikibooks_label2 = Wikibooks (ES)
| commons = Category:Physics
| wikidata = Q413
}}
<pre>
{{multiprojectbar
| title = Combined Projects
| wikipedia_lang1 = en | wikipedia_title1 = Physics | wikipedia_label1 = Wikipedia (EN)
| wikipedia_lang2 = fr | wikipedia_title2 = Physique | wikipedia_label2 = Wikipedia (FR)
| wikibooks_lang1 = en | wikibooks_title1 = Physics | wikibooks_label1 = Wikibooks (EN)
| wikibooks_lang2 = es | wikibooks_title2 = Física | wikibooks_label2 = Wikibooks (ES)
| commons = Category:Physics
| wikidata = Q413
}}
</pre>
[[Category:Navigation templates]]
</noinclude><includeonly>{{#invoke:multiprojectbar|main}}</includeonly>
7csyuuulflnx6jffuv38vlzy8f1cnek
Templat:multiprojectbar/styles.css
10
144608
373431
2026-09-09T17:18:14Z
Ultron90
4762
Mencipta laman baru dengan kandungan '/* Base Navbar Container */ .wm-navbar { display: flex; align-items: center; background-color: #f8f9fa; border: 1px solid #c8ccd1; border-radius: 6px; padding: 8px 12px; margin: 10px 0; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Lato, Helvetica, Arial, sans-serif; } /* Header/Title on the Left */ .wm-navbar-title { font-weight: 600; font-size: 0.85em; color: #5...'
373431
sanitized-css
text/css
/* Base Navbar Container */
.wm-navbar {
display: flex;
align-items: center;
background-color: #f8f9fa;
border: 1px solid #c8ccd1;
border-radius: 6px;
padding: 8px 12px;
margin: 10px 0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Lato, Helvetica, Arial, sans-serif;
}
/* Header/Title on the Left */
.wm-navbar-title {
font-weight: 600;
font-size: 0.85em;
color: #54595d;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-right: 12px;
padding-right: 12px;
border-right: 2px solid #eaecf0;
white-space: nowrap;
}
/* Button Flex Row */
.wm-navbar-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
/* Wrapper Div */
.wm-btn {
display: inline-flex;
}
/* The <a> link styled as a physical button */
.wm-btn a {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 12px;
font-size: 0.85em;
font-weight: 600;
color: #202122 !important;
text-decoration: none !important;
background-color: #ffffff;
border: 1px solid #c8ccd1;
border-radius: 4px;
cursor: pointer;
user-select: none;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
transition: background-color 0.1s ease, border-color 0.1s ease, box-shadow 0.1s ease, transform 0.05s ease;
}
/* Icon Pseudo-element Base */
.wm-btn a::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
flex-shrink: 0;
}
/* Hover State */
.wm-btn a:hover {
background-color: #f8f9fa !important;
border-color: #a2a9b1;
color: #000000 !important;
text-decoration: none !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Active Press State */
.wm-btn a:active {
background-color: #eaf3ff !important;
border-color: #36c;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
transform: translateY(1px);
}
/* Project Accent Colors & SVG Icons */
.wm-wikipedia a { border-left: 3px solid #000000; }
.wm-wikipedia a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/6/63/Wikipedia-logo-v2-single.svg'); }
.wm-commons a { border-left: 3px solid #006699; }
.wm-commons a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/4/4a/Commons-logo.svg'); }
.wm-wikisource a { border-left: 3px solid #1b73e8; }
.wm-wikisource a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/4/4c/Wikisource-logo.svg'); }
.wm-wikidata a { border-left: 3px solid #990000; }
.wm-wikidata a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/f/ff/Wikidata-logo.svg'); }
.wm-wikivoyage a { border-left: 3px solid #00ab84; }
.wm-wikivoyage a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/8/8a/Wikivoyage-logo.svg'); }
.wm-wikinews a { border-left: 3px solid #c00000; }
.wm-wikinews a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/2/24/Wikinews-logo.svg'); }
.wm-wiktionary a { border-left: 3px solid #0066cc; }
.wm-wiktionary a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/e/ec/Wiktionary-logo.svg'); }
.wm-wikibooks a { border-left: 3px solid #008080; }
.wm-wikibooks a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/f/fa/Wikibooks-logo.svg'); }
.wm-wikiquote a { border-left: 3px solid #555555; }
.wm-wikiquote a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/f/fa/Wikiquote-logo.svg'); }
.wm-wikiversity a{ border-left: 3px solid #0022ff; }
.wm-wikiversity a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/9/91/Wikiversity-logo.svg'); }
.wm-wikispecies a{ border-left: 3px solid #008800; }
.wm-wikispecies a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/d/df/Wikispecies-logo.svg'); }
.wm-meta a { border-left: 3px solid #006699; }
.wm-meta a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/7/75/Wikimedia_Community_Logo.svg'); }
.wm-lang a { border-left: 3px solid #36c; }
.wm-lang a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/e/ec/Language_icon.svg'); }
azapduxu7lk7p6stzbqz28t34zgs8z3
373436
373431
2026-09-09T17:24:13Z
Ultron90
4762
373436
sanitized-css
text/css
/* Base Navbar Container */
.wm-navbar {
display: flex;
align-items: center;
background-color: #f8f9fa;
border: 1px solid #c8ccd1;
border-radius: 6px;
padding: 8px 12px;
margin: 10px 0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Lato, Helvetica, Arial, sans-serif;
}
/* Header/Title on the Left */
.wm-navbar-title {
font-weight: 600;
font-size: 0.85em;
color: #54595d;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-right: 12px;
padding-right: 12px;
border-right: 2px solid #eaecf0;
white-space: nowrap;
}
/* Button Flex Row */
.wm-navbar-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
/* Wrapper Div */
.wm-btn {
display: inline-flex;
}
/* The <a> link styled as a physical button */
.wm-btn a {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 12px;
font-size: 0.85em;
font-weight: 600;
color: #202122 !important;
text-decoration: none !important;
background-color: #ffffff;
border: 1px solid #c8ccd1;
border-radius: 4px;
cursor: pointer;
user-select: none;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
transition: background-color 0.1s ease, border-color 0.1s ease, box-shadow 0.1s ease, transform 0.05s ease;
}
/* Icon Pseudo-element Base */
.wm-btn a::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
flex-shrink: 0;
}
/* Hover State */
.wm-btn a:hover {
background-color: #f8f9fa !important;
border-color: #a2a9b1;
color: #000000 !important;
text-decoration: none !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Active Press State */
.wm-btn a:active {
background-color: #eaf3ff !important;
border-color: #36c;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
transform: translateY(1px);
}
/* Project Accent Colors & SVG Icons */
.wm-wikipedia a { border-left: 3px solid #000000; }
.wm-wikipedia a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/8/80/Wikipedia-logo-v2.svg'); }
.wm-commons a { border-left: 3px solid #006699; }
.wm-commons a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/4/4a/Commons-logo.svg'); }
.wm-wikisource a { border-left: 3px solid #1b73e8; }
.wm-wikisource a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/4/4c/Wikisource-logo.svg'); }
.wm-wikidata a { border-left: 3px solid #990000; }
.wm-wikidata a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/f/ff/Wikidata-logo.svg'); }
.wm-wikivoyage a { border-left: 3px solid #00ab84; }
.wm-wikivoyage a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/8/8a/Wikivoyage-logo.svg'); }
.wm-wikinews a { border-left: 3px solid #c00000; }
.wm-wikinews a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/2/24/Wikinews-logo.svg'); }
.wm-wiktionary a { border-left: 3px solid #0066cc; }
.wm-wiktionary a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/e/ec/Wiktionary-logo.svg'); }
.wm-wikibooks a { border-left: 3px solid #008080; }
.wm-wikibooks a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/f/fa/Wikibooks-logo.svg'); }
.wm-wikiquote a { border-left: 3px solid #555555; }
.wm-wikiquote a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/f/fa/Wikiquote-logo.svg'); }
.wm-wikiversity a{ border-left: 3px solid #0022ff; }
.wm-wikiversity a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/9/91/Wikiversity-logo.svg'); }
.wm-wikispecies a{ border-left: 3px solid #008800; }
.wm-wikispecies a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/d/df/Wikispecies-logo.svg'); }
.wm-meta a { border-left: 3px solid #006699; }
.wm-meta a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/7/75/Wikimedia_Community_Logo.svg'); }
.wm-lang a { border-left: 3px solid #36c; }
.wm-lang a::before { background-image: url('https://upload.wikimedia.org/wikipedia/commons/e/ec/Language_icon.svg'); }
18zawz0hqzxcltfkxg1kfaboihxgf16
Wikikamus:dtp/opusakan
4
144609
373441
2026-09-10T00:21:04Z
~2026-48956-46
11436
Mencipta laman baru dengan kandungan '==Bahasa {{bahasa|{{safesubst:ROOTPAGENAME}}}}== ===Kata nama=== {{inti|{{safesubst:ROOTPAGENAME}}|kata nama}} # sesak'
373441
wikitext
text/x-wiki
==Bahasa {{bahasa|dtp}}==
===Kata nama===
{{inti|dtp|kata nama}}
# sesak
a1e9c5h11cwcnr43qzxt57aza43adm9
Wikikamus:dtp/Tolu hopod om iso
4
144610
373442
2026-09-10T02:17:21Z
Kimora Xav
11323
Bahasa Kadazan Dusun
373442
wikitext
text/x-wiki
==Bahasa {{bahasa|dtp}}==
===Kata nama===
{{inti|dtp|kata nama}}
# tiga puluh satu
6hftapo75d0cidnklp3giufuve5soay
Wikikamus:bdr/lambuh
4
144611
373443
2026-09-10T04:54:32Z
Jainnie
10839
Membuat terjemahan baru
373443
wikitext
text/x-wiki
==Bahasa {{bahasa|bdr}}==
===Kata sifat===
{{inti|bdr|kata sifat}}
# {{label|1=bdr|2=|3=Sabah}} lebar {{cp|bdr|Tipo geta' a '''lambuh''' bana.|Tikar getah itu sangat '''[[lebar]]'''.}}
oouva60mmjye5hyy77rp2m24md44fcu
Wikikamus:bdr/kapal
4
144612
373444
2026-09-10T05:01:29Z
Jainnie
10839
Membuat terjemahan baru
373444
wikitext
text/x-wiki
==Bahasa {{bahasa|bdr}}==
===Kata sifat===
{{inti|bdr|kata sifat}}
# {{label|1=bdr|2=|3=Sabah}} tebal {{cp|bdr|Buk a '''kapal''' bana.|Buku itu sangat '''[[tebal]]'''.}}
aj5t8x4o56pxd6few3c4x5m9r3wc9tb
Wikikamus:dtp/nokosunsuya
4
144613
373445
2026-09-10T06:45:29Z
~2026-48937-76
11438
nokosunsuya
373445
wikitext
text/x-wiki
==Bahasa {{bahasa|dtp}}==
===Kata nama===
{{inti|dtp|kata nama}}
# {{label|1=dtp|2=dialek|3=Johor}} tegelincir
1akufgt0ji6pvgxh13fkjb8abj2uaan
kirkification
0
144614
373450
2026-09-10T07:47:41Z
EmpAhmadK
4110
Mencipta laman baru dengan kandungan '== Bahasa Inggeris == ===Etymologi=== {{ety|en|:af|Kirk|-ification|tree=1}} Gabungan {{suffix|en|Kirk|ification}}. ===Pronunciation=== * {{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}} # Proses menyunting or atau mengganti muka pada gambar seseorang dengan gambar aktivis politik Amerika Syarikat, w:Charlie Kir...'
373450
wikitext
text/x-wiki
== Bahasa Inggeris ==
===Etymologi===
{{ety|en|:af|Kirk|-ification|tree=1}}
Gabungan {{suffix|en|Kirk|ification}}.
===Pronunciation===
* {{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}}
# Proses 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.}}
#* {{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.}}
#* {{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.}}
# {{lb|en|linguistik}} Proses mengubah sesebuah perkataan untuk menyertakan ''Kirk'' dalam ejaan atau sebulannya, merujuk kepada aktivis politik Amerika Syarikat, Charlie Kirk.
gyjca8tge59tkti9jagyv1nig3s4mjb
373451
373450
2026-09-10T07:52:35Z
EmpAhmadK
4110
373451
wikitext
text/x-wiki
== Bahasa Inggeris ==
[[File:Mona Lisa Kirkification.jpg|thumb|alt=The Mona Lisa with the face replaced by the face of Charlie Kirk|Kirkification of the ''[[Mona Lisa]]'']]
===Etymologi===
{{#invoke:etymon|main}}
Gabungan {{suffix|en|Kirk|ification}}.
===Pronunciation===
* {{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}}
# Proses 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.}}
#* {{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.}}
#* {{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.}}
# {{lb|en|linguistik}} Proses mengubah sesebuah perkataan untuk menyertakan ''Kirk'' dalam ejaan atau sebulannya, merujuk kepada aktivis politik Amerika Syarikat, Charlie Kirk.
clw5epfjblymlhtxrmgqtsxzue62nbq
373452
373451
2026-09-10T07:53:17Z
EmpAhmadK
4110
373452
wikitext
text/x-wiki
== Bahasa Inggeris ==
[[File:Mona Lisa Kirkification.jpg|thumb|alt=The Mona Lisa with the face replaced by the face of Charlie Kirk|Kirkification of the ''[[Mona Lisa]]'']]
===Etymologi===
{{ety|en|:af|Kirk|-ification|tree=1}}
Gabungan {{suffix|en|Kirk|ification}}.
===Pronunciation===
* {{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}}
# Proses 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.}}
#* {{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.}}
#* {{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.}}
# {{lb|en|linguistik}} Proses mengubah sesebuah perkataan untuk menyertakan ''Kirk'' dalam ejaan atau sebulannya, merujuk kepada aktivis politik Amerika Syarikat, Charlie Kirk.
byg3pkvadfj53lnvunmpnl2lfgjg11h
373464
373452
2026-09-10T08:39:22Z
SNN95
2113
kemaskini
373464
wikitext
text/x-wiki
== Bahasa Inggeris ==
[[File:Mona Lisa Kirkification.jpg|thumb|alt=The Mona Lisa with the face replaced by the face of Charlie Kirk|Kirkification of the ''[[Mona Lisa]]'']]
===Etimologi===
{{ety|en|:af|Kirk<alt:(Charlie) Kirk><id:original proper noun>|-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}}
# Proses 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.}}
#* {{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.}}
#* {{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.}}
# {{lb|en|linguistik}} Proses mengubah sesebuah perkataan untuk menyertakan ''Kirk'' dalam ejaan atau sebulannya, merujuk kepada aktivis politik Amerika Syarikat, Charlie Kirk.
7cf64uzotflj9eltjl34zz4locxwuhv
Kirk
0
144615
373454
2026-09-10T07:56:53Z
EmpAhmadK
4110
Mencipta laman baru dengan kandungan '==Bahasa Inggeris== ===Etymologi === {{etymon|en|id=original proper noun|kirk|tree=1|text=++}} {{dbt|en|Church}}. # {{surname|daripada bahasa Inggeris}}'
373454
wikitext
text/x-wiki
==Bahasa Inggeris==
===Etymologi ===
{{etymon|en|id=original proper noun|kirk|tree=1|text=++}} {{dbt|en|Church}}.
# {{surname|daripada bahasa Inggeris}}
ra2qjbhive3i5q3wgogs5f0i4xkz7a5
kirk
0
144616
373455
2026-09-10T07:58:46Z
EmpAhmadK
4110
Mencipta laman baru dengan kandungan '==Bahasa Inggeris== ===Etymologi=== {{etymon|en|:inh|enm-nor:kirke|text=++|tree=1}} {{doublet|en|church}}. ===Kata nama=== {{en-noun}} # {{lb|en|England Utara|and|Scotland}} [[gereja]].'
373455
wikitext
text/x-wiki
==Bahasa Inggeris==
===Etymologi===
{{etymon|en|:inh|enm-nor:kirke|text=++|tree=1}} {{doublet|en|church}}.
===Kata nama===
{{en-noun}}
# {{lb|en|England Utara|and|Scotland}} [[gereja]].
9imdva8w67blms2vqzynvfnozz93fcx
Modul:etymon/tracking
828
144617
373462
2026-09-10T08:26:56Z
SNN95
2113
letak dulu, terjemah kemudian
373462
Scribunto
text/plain
--[=[
Documentation: [[WT:Tracking#Etymon]].
]=]
local export = {}
local M = require("Module:module loader").init({
require = {
track = "Module:debug/track",
},
})
local DEPTH_RANGES = {
{ min = 50, label = "extremely-deep" },
{ min = 20, label = "20+" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ max = 2, label = "1-2" },
}
local NODE_RANGES = {
{ min = 100, label = "extremely-large" },
{ min = 50, label = "50+" },
{ min = 20, max = 49, label = "20-49" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ max = 4, label = "1-4" },
}
local LANGUAGE_RANGES = {
{ min = 10, label = "10+" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ exact = 2, label = "2" },
{ exact = 1, label = "1" },
}
local TERM_PAGE_NORMALIZERS = {
{
pattern = "^Reconstruction:[^/]+/(.+)$",
normalize = function(term)
if term:sub(1, 1) ~= "*" then
return "*" .. term
end
return term
end,
},
{
pattern = "^Appendix:[^/]+/(.+)$",
normalize = function(term)
return term
end,
},
}
local function normalize_term_page(term_page)
local page = tostring(term_page)
for _, rule in ipairs(TERM_PAGE_NORMALIZERS) do
local term = page:match(rule.pattern)
if term then
return rule.normalize(term)
end
end
return page
end
local function sanitize_term_page(term_page)
return normalize_term_page(term_page):gsub(":", "-"):gsub("/", "-"):gsub(" ", "_")
end
local function sanitize_track_segment(value)
return tostring(value):gsub(":", "-"):gsub("/", "-"):gsub(" ", "_")
end
local function track_path(page_lang_code, path)
M.track(path)
if page_lang_code then
M.track("etymon/lang/" .. page_lang_code .. "/" .. path:match("^etymon/(.+)$"))
end
end
local function term_path(term_lang_code, term_page, ...)
local safe_term = sanitize_term_page(term_page)
local parts = { "etymon", "term", term_lang_code, safe_term }
for i = 1, select("#", ...) do
local segment = select(i, ...)
if segment then
table.insert(parts, segment)
end
end
return table.concat(parts, "/")
end
local function term_id_path(term_lang_code, term_page, id_value, suffix)
return term_path(term_lang_code, term_page, "id", sanitize_track_segment(id_value), suffix)
end
local function idless_term_path(term_lang_code, term_page, outcome)
return term_path(term_lang_code, term_page, outcome)
end
local function mismatched_term_path(term_lang_code, term_page, id_value)
return term_id_path(term_lang_code, term_page, id_value, "mismatched")
end
local function record_term_id(id_stats, term_lang_code, term_page, id_value, is_override)
if not term_page or term_page == "" or not id_value or id_value == "" then
return
end
id_stats.term_ids[term_lang_code] = id_stats.term_ids[term_lang_code] or {}
id_stats.term_ids[term_lang_code][term_page] = id_stats.term_ids[term_lang_code][term_page] or {}
local entry = id_stats.term_ids[term_lang_code][term_page][id_value]
if not entry then
entry = { count = 0, override = false }
id_stats.term_ids[term_lang_code][term_page][id_value] = entry
end
entry.count = entry.count + 1
if is_override then
entry.override = true
end
end
local function record_idless_term(id_stats, term_lang_code, term_page)
if not term_page or term_page == "" then
return
end
id_stats.idless_terms[term_lang_code] = id_stats.idless_terms[term_lang_code] or {}
local entry = id_stats.idless_terms[term_lang_code][term_page]
if not entry then
entry = { count = 0, outcomes = {} }
id_stats.idless_terms[term_lang_code][term_page] = entry
end
entry.count = entry.count + 1
end
local function track_ranges(base_key, value, ranges, lang_code)
M.track("etymon/" .. base_key .. "/" .. value)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. value)
end
for _, range in ipairs(ranges) do
local matches = false
if range.min and range.max then
matches = value >= range.min and value <= range.max
elseif range.min then
matches = value >= range.min
elseif range.max then
matches = value <= range.max
elseif range.exact then
matches = value == range.exact
end
if matches then
M.track("etymon/" .. base_key .. "/" .. range.label)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. range.label)
end
break
end
end
end
function export.track_term(rest)
if rest == "" then
M.track("etymon/term/empty")
elseif rest == "?" then
M.track("etymon/term/question-mark")
elseif rest == "-" then
M.track("etymon/term/hyphen")
end
end
function export.track_title_pagename_mismatch(lang)
local lang_code = lang:getCode()
M.track("etymon/title/pagename-mismatch-after-strip-diacritics")
M.track("etymon/lang/" .. lang_code .. "/title/pagename-mismatch-after-strip-diacritics")
end
function export.record_keyword_usage(keyword_stats, keyword, target_lang, source_lang, is_toplevel)
if not is_toplevel then
return
end
if not keyword_stats[keyword] then
keyword_stats[keyword] = {
count = 0,
target_langs = {},
source_langs = {},
}
end
local keyword_data = keyword_stats[keyword]
keyword_data.count = keyword_data.count + 1
local target_code = target_lang:getCode()
keyword_data.target_langs[target_code] = (keyword_data.target_langs[target_code] or 0) + 1
if source_lang then
local source_code = source_lang:getCode()
keyword_data.source_langs[source_code] = (keyword_data.source_langs[source_code] or 0) + 1
end
end
function export.track_tree_metrics(opts)
local max_depth = opts.max_depth_reached
if not max_depth or max_depth <= 0 then
return
end
local lang_code = opts.lang:getCode()
local total_nodes = opts.total_nodes
local language_count = opts.language_count
track_ranges("depth", max_depth, DEPTH_RANGES, lang_code)
track_ranges("nodes", total_nodes, NODE_RANGES, lang_code)
local unique_languages = 0
for _ in pairs(language_count) do
unique_languages = unique_languages + 1
end
track_ranges("unique-languages", unique_languages, LANGUAGE_RANGES, lang_code)
if total_nodes == max_depth + 1 then
track_ranges("linear-depth", max_depth, DEPTH_RANGES, lang_code)
end
end
function export.track_keywords(keyword_stats, target_lang)
local target_lang_code = target_lang:getCode()
for keyword, keyword_data in pairs(keyword_stats) do
M.track("etymon/keyword/" .. keyword)
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code)
for source_code in pairs(keyword_data.source_langs) do
M.track("etymon/keyword/" .. keyword .. "/source/" .. source_code)
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code .. "/source/" .. source_code)
end
end
end
function export.record_term_id_usage(id_stats, etymon_data, term_page)
local term_lang_code = etymon_data.lang:getCode()
if etymon_data.id and etymon_data.id ~= "" then
record_term_id(id_stats, term_lang_code, term_page, etymon_data.id, etymon_data.override)
else
record_idless_term(id_stats, term_lang_code, term_page)
end
end
function export.record_mismatched_id_usage(id_stats, term_lang, term_page, id_value)
if not term_page or term_page == "" or not id_value or id_value == "" then
return
end
local term_lang_code = term_lang:getCode()
id_stats.mismatched_ids[term_lang_code] = id_stats.mismatched_ids[term_lang_code] or {}
id_stats.mismatched_ids[term_lang_code][term_page] = id_stats.mismatched_ids[term_lang_code][term_page] or {}
id_stats.mismatched_ids[term_lang_code][term_page][id_value] =
(id_stats.mismatched_ids[term_lang_code][term_page][id_value] or 0) + 1
end
function export.record_idless_resolution(id_stats, term_lang, term_page, outcome)
if not term_page or term_page == "" then
return
end
local term_lang_code = term_lang:getCode()
id_stats.idless_terms[term_lang_code] = id_stats.idless_terms[term_lang_code] or {}
local entry = id_stats.idless_terms[term_lang_code][term_page]
if not entry then
entry = { count = 0, outcomes = {} }
id_stats.idless_terms[term_lang_code][term_page] = entry
end
entry.outcomes[outcome] = (entry.outcomes[outcome] or 0) + 1
end
function export.track_text_stop_lang_missing(page_lang, stop_code)
if not stop_code or stop_code == "" then
return
end
local page_lang_code = page_lang:getCode()
M.track("etymon/text/stop-lang/missing/" .. stop_code)
M.track("etymon/lang/" .. page_lang_code .. "/text/stop-lang/missing/" .. stop_code)
end
function export.track_page_id(page_lang, id)
local lang_code = page_lang:getCode()
if id and id ~= "" then
M.track("etymon/page-id/set")
M.track("etymon/lang/" .. lang_code .. "/page-id/set")
else
M.track("etymon/page-id/unset")
M.track("etymon/lang/" .. lang_code .. "/page-id/unset")
end
end
function export.track_ids(id_stats, page_lang)
local page_lang_code = page_lang:getCode()
for term_lang_code, terms in pairs(id_stats.term_ids or {}) do
for term_page, ids in pairs(terms) do
for id_value, entry in pairs(ids) do
if entry.count > 0 then
track_path(page_lang_code, term_id_path(term_lang_code, term_page, id_value))
if entry.override then
track_path(page_lang_code, term_id_path(term_lang_code, term_page, id_value, "override"))
end
end
end
end
end
for term_lang_code, terms in pairs(id_stats.idless_terms or {}) do
for term_page, entry in pairs(terms) do
if entry.count > 0 then
track_path(page_lang_code, idless_term_path(term_lang_code, term_page))
end
for outcome, count in pairs(entry.outcomes or {}) do
if count > 0 then
track_path(page_lang_code, idless_term_path(term_lang_code, term_page, outcome))
end
end
end
end
for term_lang_code, terms in pairs(id_stats.mismatched_ids or {}) do
for term_page, ids in pairs(terms) do
for id_value, count in pairs(ids) do
if count > 0 then
track_path(page_lang_code, mismatched_term_path(term_lang_code, term_page, id_value))
end
end
end
end
end
function export.new_id_stats()
return {
term_ids = {},
idless_terms = {},
mismatched_ids = {},
}
end
return export
1a6uqo8jj8m8p5gt11ki7rx17qdrf11
-ification
0
144618
373463
2026-09-10T08:38:29Z
SNN95
2113
Mencipta laman baru dengan kandungan '{{also|-ificâtion}} ==Bhasa Inggeris== ===Bentuk lain=== * {{l|en|-fication}} ===Etimologi=== {{etymon|en|:inh|enm:-ificacioun<ety:bor<fro:-ification<ety:uder<la:-ficātiō<ety:af<la:-ficō><la:-tiō>>>>>>}} Dari {{inh|en|enm|-ificacioun}} (akhiran pada perkataan yang biasanya dipinjam keseluruhannya daripada bahasa Perancis Lama), dari {{der|en|fro|-ification}}, kemudiannya dari {{der|en|la||-ficātiō}},kata nama yang berakhiran yang muncul pada k...'
373463
wikitext
text/x-wiki
{{also|-ificâtion}}
==Bhasa Inggeris==
===Bentuk lain===
* {{l|en|-fication}}
===Etimologi===
{{etymon|en|:inh|enm:-ificacioun<ety:bor<fro:-ification<ety:uder<la:-ficātiō<ety:af<la:-ficō><la:-tiō>>>>>>}}
Dari {{inh|en|enm|-ificacioun}} (akhiran pada perkataan yang biasanya dipinjam keseluruhannya daripada bahasa Perancis Lama), dari {{der|en|fro|-ification}}, kemudiannya dari {{der|en|la||-ficātiō}},kata nama yang berakhiran yang muncul pada kata nama tindakan yang dibentuk menggunakan akhiran {{m|la|-tiō}} (bahasa Inggeris{{m|en|-tion}}) dari kata kerja berakhir dalam {{m|la|-ficō}} (bahasa Inggeris {{m|en|-ify}}). Berbanding {{m|en|-faction}}.
===Sebutan===
* {{IPA|en|/ɪ.fɪˈkeɪ.ʃən/}}
* {{audio|en|LL-Q1860 (eng)-Vealhurl--ification.wav|a=England Selatan}}
* {{rhymes|en|eɪʃən|s=4}}
* {{hyph|en|i|fi|ca|tion}}
===Akhiran===
{{en-noun|~}}
# {{n-g|Membentuk kata nama yang menunjukkan perbuatan atau proses yang mana subjek [[menjadi]] sesuatu yang lain.}}
====Nota penggunaan====
* Akhiran ini terdapat dalam perkataan yang berasal dari bahasa Perancis atau Latin, tetapi juga produktif dalam bahasa Inggeris. Apabila menggunakan akhiran {{m|en|-ation}} pada kata kerja yang berakhir dengan {{m|en|-ify}}, ''-ification'' digunakan dan bukannya *''-ifiation'' yang dijangkakan. Bandingkan {{m|en|-ability}}.
====Istilah terbitan====
{{suffixsee|en}}
====Istilah berkaitan====
* {{l|en|-ific}}
* {{l|en|-ificate}}
* {{l|en|-ifier}}
* {{l|en|-ify}}
* {{l|en|-ication}}
{{col|en|title=istilah lain berkahir dalam ''-ification''
|acetification
|acidification
|amplification
|beatification
|beautification
|BibTeXification
|bourgeoisification
|calcification
|certification
|clarification
|classification
|codification
|complexification
|debathification
|decalcification
|declassification
|deification
|demystification
|denazification
|denitrification
|desertification
|despecification
|detoxification
|disqualification
|diversification
|dowdification
|edification
|electrification
|emulsification
|exemplification
|extensification
|falsification
|floccinaucinihilipilification
|fortification
|Frenchification
|fructification
|gentrification
|glorification
|gratification
|humidification
|identification
|indemnification
|intensification
|jollification
|justification
|magnification
|modification
|mortification
|mummification
|mystification
|nitrification
|notification
|nullification
|obscurification
|ossification
|pacification
|personification
|petrification
|purification
|qualification
|quantification
|ramification
|rancidification
|ratification
|rectification
|reunification
|rigidification
|sanctification
|saponification
|scarification
|scorification
|signification
|silicification
|simplification
|solidification
|specification
|stratification
|studentification
|syllabification
|transmogrification
|typification
|unification
|verbification
|verification
|versification
|vilification
|vinification
|vitrification
|vivification
}}
==Bahasa Perancis==
===Etimologi===
{{root|fr|ine-pro|*dʰeh₁-}}
{{inh+|fr|fro|-ification}}, seterusnya dipinjam daripada {{der|fr|la|-ficātiō|-ficātiōnem}}, kata nama yang berakhiran berkaitan dengan akhiran terlentang {{m|la|-ficātum}} bagi kata kerja konjugasi pertama yang berakhiran dengan {{m|la|-ficō}}.
===Sebutan===
* {{fr-IPA}}
===Akhiran===
{{fr-noun|f}}
# {{l|en|-ification}}
====Istilah terbitan====
{{col3|fr|
|authentification
|béatification
|bonification
|certification
|clarification
|classification
|codification
|décalcification
|déification
|démystification
|démythification
|désertification
|disqualification
|diversification
|édification
|électrification
|falsification
|fortification
|fructification
|gazéification
|gentrification
|glorification
|gratification
|humidification
|identification
|intensification
|justification
|lubrification
|mèmification
|modification
|mollification
|mortification
|mystification
|notification
|opacification
|ossification
|pacification
|panification
|personnification
|pétrification
|planification
|purification
|qualification
|quantification
|ramification
|ratification
|rectification
|réunification
|russification
|sanctification
|scarification
|signification
|simplification
|solidification
|spécification
|stratification
|tarification
|unification
|vérification
|versification
|vinification
|vitrification
}}
====Istilah berkaitan====
* {{l|fr|-ifier}}
1o4qs9i07r345ncxe6htmfqua7zitnr
Modul:Grek-common
828
144619
373468
2026-09-10T08:49:36Z
SNN95
2113
letak dulu, terjemah kemudian
373468
Scribunto
text/plain
local export = {}
local gsub = string.gsub
local toNFC = mw.ustring.toNFC
local toNFD = mw.ustring.toNFD
local u = require("Module:string/char")
local ugsub = mw.ustring.gsub
local CARON = u(0x030C)
local DIAERBELOW = u(0x0324)
local BREVEBELOW = u(0x032E)
local RSQUO = u(0x2019)
local displaytext_substitutes = {
["'"] = RSQUO,
[u(0x02B9)] = RSQUO, -- modifier letter prime
[u(0x02BC)] = RSQUO, -- modifier letter apostrophe
[u(0x0374)] = RSQUO, -- Greek numeral sign
-- Not tonos (0x0384): used as the numeral sign in entries.
[u(0x1FBD)] = RSQUO, -- koronis
[u(0x1FBF)] = RSQUO, -- psili
[u(0x0303)] = u(0x0342), -- tilde to perispomeni
[u(0x0312)] = u(0x0314), -- turned comma above to reversed comma above (rough breathing)
["ɑ"] = "α",
["ꞵ"] = "β",
["ɣ"] = "γ",
["ẟ"] = "δ",
["ɛ"] = "ε",
["Ⱶ"] = "Ͱ", ["ⱶ"] = "ͱ",
["Ɩ"] = "Ι", ["ɩ"] = "ι",
["ĸ"] = "κ",
[""] = "λ",
["µ"] = "μ",
["Ʃ"] = "Σ",
["ʋ"] = "υ", ["ʊ"] = "υ",
["ɸ"] = "φ",
["Ꭓ"] = "Χ", ["ꭓ"] = "χ",
["ꞷ"] = "ω",
["Þ"] = "Ϸ", ["þ"] = "ϸ",
}
function export.makeDisplayText(text, lang, sc)
return toNFC(gsub(toNFD(text), "[\1-\127\194-\244][\128-\191]*", displaytext_substitutes))
end
local stripdiacritics_substitutes = {}
for k, v in next, displaytext_substitutes do
stripdiacritics_substitutes[k == "'" and RSQUO or k] = v == RSQUO and "'" or v
end
function export.stripDiacritics(text, lang, sc)
text = gsub(toNFD(text), "[\1-\127\194-\244][\128-\191]*", stripdiacritics_substitutes)
if sc == "Grek" and lang ~= "sq" then
text = ugsub(toNFD(text), "[" .. CARON .. DIAERBELOW .. BREVEBELOW .. "]+", "")
end
return toNFC(text)
end
return export
271byl2mogwwon79okiwb41o5qk4fjp
Modul:Polyt-stripdiacritics
828
144620
373469
2026-09-10T08:51:41Z
SNN95
2113
letak dulu, terjemah kemudian
373469
Scribunto
text/plain
local export = {}
local toNFC = mw.ustring.toNFC
local toNFD = mw.ustring.toNFD
local u = require("Module:string/char")
local ugsub = mw.ustring.gsub
local umatch = mw.ustring.match
local grave = u(0x300)
local acute = u(0x301)
local smooth = u(0x313)
local rough = u(0x314)
local word_ch = "[%w" .. grave .. acute .. smooth .. rough .. u(0x308, 0x342, 0x345) .. "]"
local following_word_pattern = "^" .. word_ch .. "*%s+" .. word_ch -- not punctuation
local breathing_ch = "[" .. smooth .. rough .. "]"
local rho_cap_smooth_sub = u(0x1FDC) -- temporary (unused) codepoint for Ρ̓, which has no atomic codepoint
local rho = "[ρῤῥΡ" .. rho_cap_smooth_sub .. "Ῥ]"
local two_or_more_rhos = rho .. rho .. "+"
local expected_rho_breathings = "^[ρῤΡ" .. rho_cap_smooth_sub .. "]+[ρῥΡῬ]$"
local Grek_stripDiacritics = require("Module:Grek-common").stripDiacritics
function export.stripDiacritics(text, lang, sc)
-- Do some substitutions done for all Greek text.
text = Grek_stripDiacritics(text, lang, sc)
-- Remove length marks and double undertie.
text = toNFD(text):gsub("\204[\132\134]", ""):gsub("\205\156", "")
-- Convert grave to acute unless followed by another word.
text = ugsub(text, grave .. "()", function(pos)
if not umatch(text, following_word_pattern, pos) then
return acute
end
end)
-- Convert "ῤῥ" to "ρρ".
text = ugsub(toNFC(text):gsub("Ρ̓", rho_cap_smooth_sub), two_or_more_rhos, function(rhos)
if umatch(rhos, expected_rho_breathings) then
return (toNFD(rhos:gsub(rho_cap_smooth_sub, "Ρ̓")):gsub(breathing_ch, ""))
end
end):gsub(rho_cap_smooth_sub, "Ρ̓")
return toNFC(text)
end
return export
eqzr38qq3hkar1z8ab23f1nx777pqka