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 " +&lrm; " 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-&#45;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