Wiksiyonaryo tlwiktionary https://tl.wiktionary.org/wiki/Wiksiyonaryo:Unang_Pahina MediaWiki 1.47.0-wmf.21 case-sensitive Midya Natatangi Usapan Tagagamit Usapang tagagamit Wiksiyonaryo Usapang Wiksiyonaryo Talaksan Usapang talaksan MediaWiki Usapang MediaWiki Padron Usapang padron Tulong Usapang tulong Kategorya Usapang kategorya TimedText TimedText talk Module Module talk Event Event talk pamanahon 0 2935 178048 175158 2026-09-24T14:47:57Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 178048 wikitext text/x-wiki [[Category:Mga katutubong salitang Tagalog]] =={{=tl=}}== ===Etimolohiya=== Mula sa {{panlapi|tl|pam-|panahon}}. ===Pagbigkas=== * {{tl-pr|pamanahón}} ===Pangngalan=== {{tl-noun|b=+}} # {{lb|tl|balarila}} [[pang-abay|Pang-abay]] na nagsasaad ng panahong pinagyarihan. #: {{ux|tl|Inaaral niya ang '''pamanahong''' kanya namang kinaiinisan.}} ===Pang-uri=== {{tl-adj|b=+}} # Naaayon sa panahon. #:''Ang '''pamanahong''' pagdating ng ulan ay kanyang ihinihintay.'' ===Basahin din=== * {{R:KWF Diksiyonaryo}} * {{R:Pambansang Diksiyonaryo}} npas46nsj7vrdysm8siz13upagjmhyn pinagbuhatan 0 3261 178051 178031 2026-09-24T16:14:25Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 178051 wikitext text/x-wiki =={{=tl=}}== ===Etimolohiya=== Mula sa {{panlapi|tl|buhat|pinag- -an}}. ===Pagbigkas=== {{tl-pr}} ===Pangngalan=== {{tl-noun|b=+}} # Kung saan nagmula ang isang bagay. #: {{ux|tl|Ang mga salitang Tagalog ay may '''pinagbuhatan''' sa salitang Dumagat.}} #: {{syn|tl|pinagmula|pinanggalingan}} # Sinaktan sa pamamagitan ng [[suntok]] o [[palo]]. #: {{ux|tl|'''Pinagbuhatan''' niya ang kabit niya dahil masyadong nagpapahalata kapag nasa harap ng tunay na asawa niya.}} ===Basahin din=== * {{R:KWF Diksiyonaryo}} * {{R:Pambansang Diksiyonaryo}} * {{R:Vicassan's Pilipino-English Dictionary 1978|page=}} e8gpmu6c3ngm70qi9qasj9kzpvbzn8f 178052 178051 2026-09-24T16:19:16Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 178052 wikitext text/x-wiki =={{=tl=}}== ===Etimolohiya=== Mula sa {{panlapi|tl|buhat|pinag- -an}}. ===Pagbigkas=== {{tl-pr}} ===Pangngalan=== {{tl-noun|b=+}} # Kung saan nagmula ang isang bagay. #: {{ux|tl|Ang mga salitang Tagalog ay may '''pinagbuhatan''' sa salitang Dumagat.}} #: {{syn|tl|pinagmula|pinanggalingan}} # Sinaktan sa pamamagitan ng [[suntok]] o [[palo]]. #: {{ux|tl|'''Pinagbuhatan''' niya ang kabit niya dahil masyadong nagpapahalata kapag nasa harap ng tunay na asawa niya.}} ====Katawagang kaugnay==== {{col|tl|pinagbubuhatan}} ===Basahin din=== * {{R:KWF Diksiyonaryo}} * {{R:Pambansang Diksiyonaryo}} * {{R:Vicassan's Pilipino-English Dictionary 1978|page=}} 3xa41q7vqpqzxnphnqain5hfapn16zw 178053 178052 2026-09-24T16:24:33Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 178053 wikitext text/x-wiki =={{=tl=}}== ===Etimolohiya=== Mula sa {{panlapi|tl|buhat|pinag- -an}}. ===Pagbigkas=== {{tl-pr}} ===Pangngalan=== {{tl-noun|b=+}} # Kung saan nagmula ang isang bagay. #: {{ux|tl|Ang mga salitang Tagalog ay may '''pinagbuhatan''' sa salitang Dumagat.}} #: {{syn|tl|pinagmula|pinanggalingan}} # Sinaktan sa pamamagitan ng [[suntok]] o [[palo]]. #: {{ux|tl|'''Pinagbuhatan''' niya ang kabit niya dahil masyadong nagpapahalata kapag nasa harap ng tunay na asawa niya.}} ===Pandiwa=== {{tl-head|verb form|b=+}} # {{verb form of|tl|pagbuhatan||compl|asp}} ===Basahin din=== * {{R:KWF Diksiyonaryo}} * {{R:Pambansang Diksiyonaryo}} 0y3x7kp3w6eieossw3lc43kxpnqhdhl 178073 178053 2026-09-25T02:36:24Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 etymon 178073 wikitext text/x-wiki =={{=tl=}}== ===Etimolohiya=== {{ety|tl|id=source|:af|buhat|pinag- -an|text=+|tree=1}} ===Pagbigkas=== {{tl-pr}} ===Pangngalan=== {{tl-noun|b=+}} # Kung saan nagmula ang isang bagay. #: {{ux|tl|Ang mga salitang Tagalog ay may '''pinagbuhatan''' sa salitang Dumagat.}} #: {{syn|tl|pinagmula|pinanggalingan}} # Sinaktan sa pamamagitan ng [[suntok]] o [[palo]]. #: {{ux|tl|'''Pinagbuhatan''' niya ang kabit niya dahil masyadong nagpapahalata kapag nasa harap ng tunay na asawa niya.}} ===Pandiwa=== {{tl-head|verb form|b=+}} # {{verb form of|tl|pagbuhatan||compl|asp}} ===Basahin din=== * {{R:KWF Diksiyonaryo}} * {{R:Pambansang Diksiyonaryo}} iigh09jv9oxmpik7m3glo08bvxkxexk Module:parameters/data 828 33794 178075 169699 2026-09-25T02:46:43Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 merge changes 178075 Scribunto text/plain local list_to_set = require("Module:table").listToSet local alias_of_2 = {alias_of = 2} local boolean = {type = "boolean"} local empty_list = {} local list = {list = true} local list_allow_holes_separate_no_index = {list = true, allow_holes = true, separate_no_index = true} local required = {required = true} local required_default_ = {required = true, default = ""} local required_lang_default_und = {required = true, type = "language", default = "und"} local type_labels = {type = "labels"} local type_qualifier = {type = "qualifier"} local type_references = {type = "references"} local m = {} -- [[Module:anchors]] m["anchor"] = { [1] = {required = true, list = true, disallow_holes = true}, } m["senseid"] = { [1] = required_lang_default_und, [2] = required_default_, id = alias_of_2, tag = {set = list_to_set{"li", "p"}, default = "li"}, } m["etymid"] = { [1] = required_lang_default_und, [2] = required_default_, id = alias_of_2, } -- [[Module:etymon]] m["etymon"] = { [1] = required_lang_default_und, [2] = {list = true, disallow_holes = true}, id = true, title = true, tree = boolean, text = true, exnihilo = boolean, etydate = true, doublet = {sublist = "comma without whitespace"}, rfe = true, etystub = true, nl = boolean, pos = true, notree = {default = false, type = "boolean"}, nocat = {default = false, type = "boolean"}, nodot = {default = false, type = "boolean"}, dot = true, json = {default = false, type = "boolean"}, } -- [[Module:transclude]] m["transclude"] = { [1] = {required = true, type = "language"}, [2] = {list = true, required = true}, id = true, sort = true, nogloss = {default = false, type = "boolean"}, no_truncate_gloss = boolean, include_place_extra_info = boolean, place_translation_follows = boolean, place_addl = true, lb = true, nolb = true, nocat = boolean, to = boolean, t = list, indent = true, dot = boolean, pagename = true, } -- [[Module:translations]] m["translation"] = { [1] = required_lang_default_und, [2] = true, [3] = list, alt = true, id = true, sc = {type = "script"}, tr = true, ts = true, lit = true, l = type_labels, ll = type_labels, q = type_qualifier, qq = type_qualifier, ref = type_references, } m["t-needed"] = { [1] = required_lang_default_und, [2] = {set = list_to_set{"usex", "quote"}}, nocat = boolean, sort = true, } m["trans-top"] = { [1] = true, id = true, ["column-width"] = true, } m["trans-top-also"] = { [1] = required, [2] = list, id = list_allow_holes_separate_no_index, ["column-width"] = true, } m["checktrans-top"] = { [1] = true, ["column-width"] = true, } m["trans-bottom"] = empty_list m["trans-see"] = { [1] = required, [2] = list, id = list_allow_holes_separate_no_index, } m["translation subpage"] = empty_list m["no equivalent translation"] = { [1] = required_lang_default_und, noend = boolean, } m["no attested translation"] = { [1] = required_lang_default_und, noend = boolean, sort = true, } m["not used"] = { [1] = required_lang_default_und, [2] = true, } return m 51jr4evgp9f3cxhobug46hkqur9gpta Module:affix 828 33858 178049 175906 2026-09-24T15:48:54Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 Pikitan nang masalin ang paggamit ng panlapi (hal. na maging "hinulapian ng " ang "suffixed with ") 178049 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 rsub = m_str_utils.gsub local usub = m_str_utils.sub 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 --TLCHANGE add logic to add pang-angkop to it, depending on the other labels function add_pangangkop(pos) if pos == nil then error("There should be an inputted pos") end local pos_with_pangangkop = pos local pos_lastletter = pos:sub(-1) if pos_lastletter == "n" then pos_with_pangangkop = pos_with_pangangkop .. "g" elseif pos_lastletter == "a" or pos_lastletter == "e" or pos_lastletter == "i" or pos_lastletter == "o" or pos_lastletter == "u" then pos_with_pangangkop = pos_with_pangangkop .. "ng" else pos_with_pangangkop = pos_with_pangangkop .. " na" end return pos_with_pangangkop end --TLCHANGE hardcode translations of '''<affix_type>-ed with ''' local into_nilapian = { circumfix = "kinabilaang-lapian ng ", infix = "ginitlapian ng ", interfix = "ginitnaang-lapian ng ", prefix = "inunlapian ng ", suffix = "hinulapian ng ", ["non-affix"] = "di-nilapian ng ", } -- 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 = "salita" --TLCHANGE "term" --[==[ 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 'fa-Arab' and 'ur-Arab' will match 'Arab'. 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 ["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 'fa-Arab' and 'ur-Arab' -- will match 'Arab'. The value should be a single character. local lookup_hyphens = { ["Hebr"] = "־", -- This covers all Arabic scripts. See above. ["Arab"] = "ـ", } -- 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 'fa-Arab', 'ur-Arab' etc. will match 'Arab'. local display_hyphens = { -- This covers all Arabic scripts. See above. ["Arab"] = 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 "[[Appendix:Glossary#" .. 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) .. " compound", cat = typ .. " compounds", } 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 .. " compound", cat = typ .. " compounds", } 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() while i < max_index do 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(lang:getFullName() .. " " .. cat.cat, lang, cat.sort_key, cat.sort_base, force_cat) else data.categories[i] = require(utilities_module).format_categories(lang:getFullName() .. " " .. cat, 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 ", literally " .. 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 local function pluralize(pos) if pos ~= "nouns" and usub(pos, -5) ~= "verbs" and usub(pos, -4) ~= "ives" then if pos:find("[sx]$") then pos = pos .. "es" else pos = pos .. "s" end end return pos 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, "term", 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 fa-Arab, ur-Arab etc. to Arab. 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 == "circumfix" 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 == "infix" or affix_type == "interfix" 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 == "prefix" 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 == "suffix" 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(affix) -- 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(affix, 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 ({"prefix"}, {"interfix"}, {"suffix"}, {"circumfix"} 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.) ]==] local function 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 = "circumfix" 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 = "interfix" elseif has_ending_hyphen then affix_type = "prefix" elseif has_beginning_hyphen then affix_type = "suffix" 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` == {"prefix"}, 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 == "prefix" or affix_type == "suffix" or affix_type == "circumfix" or affix_type == "infix" or affix_type == "interfix" or affix_type == "non-affix") then error("Internal error: Invalid affix type " .. (affix_type or "(nil)")) end local _, link_term, display_term, lookup_term = 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) --TLCHANGE commented this out 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 = 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 = "patronymics", 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 = "diminutive " .. data.pos, 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 --TLCHANGE add_pangangkop into_nilapian table.insert(categories, {cat = "na " .. add_pangangkop(data.pos) .. " " .. into_nilapian[affix_type] .. 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 --TLCHANGE ADD THIS CHECER: if "salita" then it should be "na salitang tambalan" if data.pos == "salita" then table.insert(categories, "na salitang tambalan") else table.insert(categories, "na tambalang " .. data.pos) --TLCHANGE "compound " .. data.pos end 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, borrowing_type = 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 = "by " .. 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 text_sections, categories, borrowing_type = 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) --TLCHANGE commented this out 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 = {} --TLCHANGE "compound " .. data.pos table.insert(categories, "na " .. add_pangangkop(data.pos) .. " tambalan" ) -- 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 = 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 == "interfix" 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 --TLCHANGE add_pangangkop into_nilapian table.insert(categories, {cat = "na " .. add_pangangkop(data.pos) .. " " .. into_nilapian[affix_type] .. 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, borrowing_type = 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 "suffix", 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 = 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 --TLCHANGE add_pangangkop into_nilapian local cat = "na " .. add_pangangkop(data.pos) .. " " .. into_nilapian[affix_type] .. 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) --TLCHANGE commented this out 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, "prefix") make_part_into_affix(data.suffix, data.lang, data.sc, "suffix") track_wrong_affix_type("circumfix", data.prefix, "prefix") track_wrong_affix_type("circumfix", data.base, nil) track_wrong_affix_type("circumfix", data.suffix, "suffix") -- 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 --TLCHANGE add_pangangkop and translate " circumixed with " table.insert(categories, {cat = "na" .. add_pangangkop(data.pos) .. " kinabilaang-lapian ng " .. 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) --TLCHANGE commented this out 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, "prefix") make_part_into_affix(data.suffix, data.lang, data.sc, "suffix") track_wrong_affix_type("confix", data.prefix, "prefix") track_wrong_affix_type("confix", data.base, nil) track_wrong_affix_type("confix", data.suffix, "suffix") -- 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, "prefix", 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, "suffix", 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) --TLCHANGE commented this out 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, "infix") track_wrong_affix_type("infix", data.base, nil) track_wrong_affix_type("infix", data.infix, "infix") -- 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, "infix", 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) --TLCHANGE commented this out 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, "prefix") end for i, prefix in ipairs(data.prefixes) do track_wrong_affix_type("prefix", prefix, "prefix") end track_wrong_affix_type("prefix", 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, "prefix", 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) --TLCHANGE commented this out 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, "suffix") end track_wrong_affix_type("suffix", data.base, nil) for i, suffix in ipairs(data.suffixes) do track_wrong_affix_type("suffix", suffix, "suffix") 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, "suffix", suffix) if suffix.pos and rfind(suffix.pos, "patronym") then table.insert(categories, "patronymics") end end return export.join_formatted_parts { data = data, parts_formatted = parts_formatted, categories = categories } end return export qafgaeqgiaf2xi7iapb6rzorp53yhwj 178050 178049 2026-09-24T15:51:39Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 178050 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 rsub = m_str_utils.gsub local usub = m_str_utils.sub 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 --TLCHANGE add logic to add pang-angkop to it, depending on the other labels function add_pangangkop(pos) if pos == nil then error("There should be an inputted pos") end local pos_with_pangangkop = pos local pos_lastletter = pos:sub(-1) if pos_lastletter == "n" then pos_with_pangangkop = pos_with_pangangkop .. "g" elseif pos_lastletter == "a" or pos_lastletter == "e" or pos_lastletter == "i" or pos_lastletter == "o" or pos_lastletter == "u" then pos_with_pangangkop = pos_with_pangangkop .. "ng" else pos_with_pangangkop = pos_with_pangangkop .. " na" end return pos_with_pangangkop end --TLCHANGE hardcode translations of '''<affix_type>-ed with ''' local into_nilapian = { circumfix = "kinabilaang-lapian ng ", infix = "ginitlapian ng ", interfix = "ginitnaang-lapian ng ", prefix = "inunlapian ng ", suffix = "hinulapian ng ", ["non-affix"] = "di-nilapian ng ", } -- 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 = "salita" --TLCHANGE "term" --[==[ 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 'fa-Arab' and 'ur-Arab' will match 'Arab'. 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 ["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 'fa-Arab' and 'ur-Arab' -- will match 'Arab'. The value should be a single character. local lookup_hyphens = { ["Hebr"] = "־", -- This covers all Arabic scripts. See above. ["Arab"] = "ـ", } -- 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 'fa-Arab', 'ur-Arab' etc. will match 'Arab'. local display_hyphens = { -- This covers all Arabic scripts. See above. ["Arab"] = 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 "[[Appendix:Glossary#" .. 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) .. " compound", cat = typ .. " compounds", } 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 .. " compound", cat = typ .. " compounds", } 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() while i < max_index do 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(lang:getFullName() .. " " .. cat.cat, lang, cat.sort_key, cat.sort_base, force_cat) else data.categories[i] = require(utilities_module).format_categories(lang:getFullName() .. " " .. cat, 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 ", literally " .. 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 local function pluralize(pos) if pos ~= "nouns" and usub(pos, -5) ~= "verbs" and usub(pos, -4) ~= "ives" then if pos:find("[sx]$") then pos = pos .. "es" else pos = pos .. "s" end end return pos 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, "term", 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 fa-Arab, ur-Arab etc. to Arab. 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 == "circumfix" 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 == "infix" or affix_type == "interfix" 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 == "prefix" 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 == "suffix" 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(affix) -- 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(affix, 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 ({"prefix"}, {"interfix"}, {"suffix"}, {"circumfix"} 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.) ]==] local function 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 = "circumfix" 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 = "interfix" elseif has_ending_hyphen then affix_type = "prefix" elseif has_beginning_hyphen then affix_type = "suffix" 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` == {"prefix"}, 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 == "prefix" or affix_type == "suffix" or affix_type == "circumfix" or affix_type == "infix" or affix_type == "interfix" or affix_type == "non-affix") then error("Internal error: Invalid affix type " .. (affix_type or "(nil)")) end local _, link_term, display_term, lookup_term = 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) --TLCHANGE commented this out 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 = 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 = "patronymics", 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 = "diminutive " .. data.pos, 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 --TLCHANGE add_pangangkop into_nilapian table.insert(categories, {cat = "na " .. add_pangangkop(data.pos) .. " " .. into_nilapian[affix_type] .. 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 --TLCHANGE ADD THIS CHECER: if "salita" then it should be "na salitang tambalan" if data.pos == "salita" then table.insert(categories, "na salitang tambalan") else table.insert(categories, "na tambalang " .. data.pos) --TLCHANGE "compound " .. data.pos end 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, borrowing_type = 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 = "by " .. 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 text_sections, categories, borrowing_type = 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) --TLCHANGE commented this out 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 = {} --TLCHANGE "compound " .. data.pos table.insert(categories, "na " .. add_pangangkop(data.pos) .. " tambalan" ) -- 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 = 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 == "interfix" 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 --TLCHANGE add_pangangkop into_nilapian table.insert(categories, {cat = "na " .. add_pangangkop(data.pos) .. " " .. into_nilapian[affix_type] .. 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, borrowing_type = 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 "suffix", 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 = 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 --TLCHANGE add_pangangkop into_nilapian local cat = "na " .. add_pangangkop(pos) .. " " .. into_nilapian[affix_type] .. 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) --TLCHANGE commented this out 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, "prefix") make_part_into_affix(data.suffix, data.lang, data.sc, "suffix") track_wrong_affix_type("circumfix", data.prefix, "prefix") track_wrong_affix_type("circumfix", data.base, nil) track_wrong_affix_type("circumfix", data.suffix, "suffix") -- 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 --TLCHANGE add_pangangkop and translate " circumixed with " table.insert(categories, {cat = "na" .. add_pangangkop(data.pos) .. " kinabilaang-lapian ng " .. 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) --TLCHANGE commented this out 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, "prefix") make_part_into_affix(data.suffix, data.lang, data.sc, "suffix") track_wrong_affix_type("confix", data.prefix, "prefix") track_wrong_affix_type("confix", data.base, nil) track_wrong_affix_type("confix", data.suffix, "suffix") -- 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, "prefix", 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, "suffix", 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) --TLCHANGE commented this out 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, "infix") track_wrong_affix_type("infix", data.base, nil) track_wrong_affix_type("infix", data.infix, "infix") -- 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, "infix", 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) --TLCHANGE commented this out 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, "prefix") end for i, prefix in ipairs(data.prefixes) do track_wrong_affix_type("prefix", prefix, "prefix") end track_wrong_affix_type("prefix", 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, "prefix", 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) --TLCHANGE commented this out 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, "suffix") end track_wrong_affix_type("suffix", data.base, nil) for i, suffix in ipairs(data.suffixes) do track_wrong_affix_type("suffix", suffix, "suffix") 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, "suffix", suffix) if suffix.pos and rfind(suffix.pos, "patronym") then table.insert(categories, "patronymics") end end return export.join_formatted_parts { data = data, parts_formatted = parts_formatted, categories = categories } end return export 2nzu7yqq5ycyl8hn9k2jqfjmpp0d42x Module:anchors 828 33951 178061 165465 2026-09-25T00:07:24Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 enwikt parity, update oto (oldid 90373122) 178061 Scribunto text/plain local export = {} local string_utilities_module = "Module:string utilities" local anchor_encode = mw.uri.anchorEncode local concat = table.concat local insert = table.insert local language_anchor -- Defined below. local function decode_entities(...) decode_entities = require(string_utilities_module).decode_entities return decode_entities(...) end local function encode_entities(...) encode_entities = require(string_utilities_module).encode_entities return encode_entities(...) end -- Returns the anchor text to be used as the fragment of a link to a language section. function export.language_anchor(lang, id) return anchor_encode(lang:getFullName() .. ": " .. id) end language_anchor = export.language_anchor -- Normalizes input text (removes formatting etc.), which can then be used as an anchor in an `id=` field. function export.normalize_anchor(str) return decode_entities(anchor_encode(str)) end function export.make_anchors(ids) local anchors = {} for i = 1, #ids do local id = ids[i] local el = mw.html.create("span") :addClass("template-anchor") :attr("id", anchor_encode(id)) :attr("data-id", id) insert(anchors, tostring(el)) end return concat(anchors) end function export.senseid(lang, id, tag_name) -- The following tag is opened but never closed, where is it supposed to be closed? -- with <li> it doesn't matter, as it is closed automatically. -- with <p> it is a problem -- Cannot use mw.html here as it always closes tags return "<" .. tag_name .. " class=\"senseid\" id=\"" .. language_anchor(lang, id) .. "\" data-lang=\"" .. lang:getCode() .. "\" data-id=\"" .. encode_entities(id) .. "\">" end function export.etymid(lang, id) -- Use a <ul> tag to ensure spacing doesn't get messed up. local el = mw.html.create("ul") :addClass("etymid") :attr("id", language_anchor(lang, id)) :attr("data-lang", lang:getCode()) :attr("data-id", id) return tostring(el) end function export.etymonid(lang, id, opts) opts = opts or {} -- Use a <ul> tag to ensure spacing doesn't get messed up. local el = mw.html.create("ul") :addClass("etymonid") :attr("data-lang", lang:getCode()) if id then el:attr("id", language_anchor(lang, id)) el:attr("data-id", id) end if opts.no_tree then el:attr("data-no-tree", "1") end if opts.title then el:attr("data-title", opts.title) end if opts.empty_tree then el:attr("data-empty-tree", "1") end if opts.ety_tree_json then el:attr("data-ety-tree-json", opts.ety_tree_json) end return tostring(el) end return export 2o0jqln8y0bxvpfhe66eogs2k88qkap Module:parameters/track 828 34536 178074 167334 2026-09-25T02:43:17Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 merge changes 178074 Scribunto text/plain local debug_track_module = "Module:debug/track" local string_gline_module = "Module:string/gline" local match = string.match local new_title = mw.title.new local require = require local traceback = debug.traceback local function debug_track(...) debug_track = require(debug_track_module) return debug_track(...) end local function gline(...) gline = require(string_gline_module) return gline(...) end local params_title local function get_params_title() params_title, get_params_title = new_title("parameters", 828), nil return params_title end return function(page, param_name) debug_track("parameters/" .. page) if param_name ~= nil then debug_track("parameters/" .. page .. "/" .. param_name .. "=") end -- Check through the traceback to get the calling module and function. local mod_title for line in gline((traceback())) do local mod, func = match(line, "^\t*(.-):%d+: in function (.*)$") -- Must match a conventional module, not a tail call, C function, in- -- built Scribunto file etc. If `mod` matches `mod_title`, it means the -- module has been seen before, so there is nothing else to check. if mod and not (mod_title and mod == mod_title.prefixedText) then mod_title = new_title(mod) if mod_title and mod_title.namespace == 828 and not ( mod_title == (params_title or get_params_title()) or mod_title:isSubpageOf(params_title) ) then mod = mod_title.text debug_track("parameters/" .. page .. "/" .. mod) -- traceback() encloses function names in single quotes. local funcname = match(func, "^'(.*)'$") if funcname then debug_track("parameters/" .. page .. "/" .. mod .. "/" .. funcname) return end -- WHen a function is unnamed, line numbers are given after the -- module name in angle brackets, directly after a separating -- colon. local funcline = match(func, "^<.-:(%d+)>$") if funcline then debug_track("parameters/" .. page .. "/" .. mod .. ":" .. funcline) end return end end end end ep6rfkuslbwvncu36j3fa17iqe4seb6 Module:root 828 35405 178064 169264 2026-09-25T00:24:42Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 enwikt parity, updated to (oldid 91767397), implementation had moved to Module:roots 178064 Scribunto text/plain return require("Module:roots") aw7nuem1odrzl9a0ukjfaj2q24kiw7m Padron:catlink 10 38859 178054 2026-09-24T23:05:44Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 enwikt parity, copied over 178054 wikitext text/x-wiki [[:Category:{{{1|}}}|{{{2|{{#invoke:string/templates|replace|{{{1|}}}|_|&#32;}}}}}]]<noinclude> {{documentation}} </noinclude> 6t77z8fqnrbh7qmstz22wk15met98y0 178056 178054 2026-09-24T23:10:22Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 gamitin ang salin ng Category 178056 wikitext text/x-wiki [[:Kategorya:{{{1|}}}|{{{2|{{#invoke:string/templates|replace|{{{1|}}}|_|&#32;}}}}}]]<noinclude> {{documentation}} </noinclude> fxi0h3juk56tl8k7tgn9cd827i9zhrl Module:phi-headword/documentation 828 38860 178055 2026-09-24T23:08:17Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 enwikt parity, just copied over 178055 wikitext text/x-wiki This module is used for Philippine-language headword-line templates. Currently it supports: {| class=wikitable ! Language !! Noun !! Proper noun !! Verb !! Adjective !! Adverb !! Other parts of speech |- | {{catlink|Aklanon headword-line templates|Aklanon}} | {{tl|akl-noun}} | {{tl|akl-proper noun}} | {{tl|akl-verb}} | {{tl|akl-adj}} | {{tl|akl-adv}} | {{tl|akl-head}} |- | {{catlink|Central Bikol headword-line templates|Central Bikol}} | {{tl|bcl-noun}} | {{tl|bcl-proper noun}} | {{tl|bcl-verb}} | {{tl|bcl-adj}} | {{tl|bcl-adv}} | {{tl|bcl-head}} |- | {{catlink|Buhid headword-line templates|Buhid}} | | | | | | {{tl|bku-head}} |- | {{catlink|Cebuano headword-line templates|Cebuano}} | {{tl|ceb-noun}} | {{tl|ceb-proper noun}} | {{tl|ceb-verb}} | {{tl|ceb-adj}} | {{tl|ceb-adv}} | {{tl|ceb-num}} (for numerals) <br/> {{tl|ceb-head}} |- | {{catlink|Chavacano headword-line templates|Chavacano}} | {{tl|cbk-noun}} | {{tl|cbk-proper noun}} | {{tl|cbk-verb}} | {{tl|cbk-adj}} | {{tl|cbk-adv}} | {{tl|cbk-head}} |- | {{catlink|Hanunoo headword-line templates|Hanunoo}} | {{tl|hnn-noun}} | {{tl|hnn-proper noun}} | {{tl|hnn-verb}} | {{tl|hnn-adj}} | {{tl|hnn-adv}} | {{tl|hnn-letter}} (for letters) <br/> {{tl|hnn-head}} |- | {{catlink|Hiligaynon headword-line templates|Hiligaynon}} | {{tl|hil-noun}} | {{tl|hil-proper noun}} | {{tl|hil-verb}} | {{tl|hil-adj}} | {{tl|hil-adv}} | {{tl|hil-head}} |- | {{catlink|Ilocano headword-line templates|Ilocano}} | {{tl|ilo-noun}} | {{tl|ilo-proper noun}} | {{tl|ilo-verb}} | {{tl|ilo-adj}} | {{tl|ilo-adv}} | {{tl|ilo-head}} |- | {{catlink|Kankanaey headword-line templates|Kankanaey}} | {{tl|kne-noun}} | {{tl|kne-proper noun}} | {{tl|kne-verb}} | {{tl|kne-adj}} | {{tl|kne-adv}} | {{tl|kne-root}} (for roots with unknown POS) <br/> {{tl|kne-head}} |- | {{catlink|Kapampangan headword-line templates|Kapampangan}} | {{tl|pam-noun}} | {{tl|pam-proper noun}} | {{tl|pam-verb}} | {{tl|pam-adj}} | {{tl|pam-adv}} | {{tl|pam-head}} |- | {{catlink|Kinaray-a headword-line templates|Kinaray-a}} | {{tl|krj-noun}} | {{tl|krj-proper noun}} | {{tl|krj-verb}} | {{tl|krj-adj}} | {{tl|krj-adv}} | {{tl|krj-head}} |- | {{catlink|Maguindanao headword-line templates|Maguindanao}} | {{tl|mdh-noun}} | {{tl|mdh-proper noun}} | {{tl|mdh-verb}} | {{tl|mdh-adj}} | {{tl|mdh-adv}} | {{tl|mdh-head}} |- | {{catlink|Maranao headword-line templates|Maranao}} | {{tl|mrw-noun}} | {{tl|mrw-proper noun}} | {{tl|mrw-verb}} | {{tl|mrw-adj}} | {{tl|mrw-adv}} | {{tl|mrw-head}} |- | {{catlink|Pangasinan headword-line templates|Pangasinan}} | {{tl|pag-noun}} | {{tl|pag-proper noun}} | {{tl|pag-verb}} | {{tl|pag-adj}} | {{tl|pag-adv}} | {{tl|pag-head}} |- | {{catlink|Tagalog headword-line templates|Tagalog}} | {{tl|tl-noun}} | {{tl|tl-proper noun}} | {{tl|tl-verb}} | {{tl|tl-adj}} | {{tl|tl-adv}} | {{tl|tl-num}} (for numerals) <br/> {{tl|tl-prep}} (for prepositions) <br/> {{tl|tl-pron}} (for pronouns) <br/> {{tl|tl-letter}} (for letters) <br/> {{tl|tl-head}} |- | {{catlink|Tausug headword-line templates|Tausug}} | {{tl|tsg-noun}} | {{tl|tsg-proper noun}} | {{tl|tsg-verb}} | {{tl|tsg-adj}} | {{tl|tsg-adv}} | {{tl|tsg-head}} |- | {{catlink|Waray-Waray headword-line templates|Waray-Waray}} | {{tl|war-noun}} | {{tl|war-proper noun}} | {{tl|war-verb}} | {{tl|war-adj}} | {{tl|war-adv}} | {{tl|war-head}} |} '''The rest of this documentation describes parameters from the point of view of Tagalog, but other languages work very similarly.''' The module is always invoked the same way, by passing a single parameter to the "show" function. This parameter is the name of the part of speech, but in plural (examples given are for nouns, and for adjective forms respectively): <pre> {{#invoke:tl-headword|show|nouns}} {{#invoke:tl-headword|show|adjective forms}} </pre> If the part of speech is omitted, the template being implemented must have the part of speech (in its singular form, e.g. <code>interjection</code>) specified in {{para|1}}. This is used to support {{tl|tl-head}}. The template will, by default, accept the following parameters (specific parts of speech may accept or require others): ; {{para|1}} : Override the headword display; used to add accents to the headword and/or links to individual words in a multiword term. Use the value <code>+</code> to explicitly request the default (which is the same as the page name). Note that it no longer works to specify a blank parameter to request the default; you must use <code>+</code>. Note also that if the invoking template is {{tl|tl-head}}, parameter {{para|1}} is instead the part of speech (in its singular form, e.g. <code>interjection</code>), and {{para|2}} is the first explicit headword. Multiple comma-separated values can be given (with no space after the comma), along with per-value inline modifiers to specify qualifiers, labels, references and manual transliterations. ; {{para|b}} : Specify the corresponding Baybayin-script or other native-script spelling of a Latin-script term, for languages that support native scripts: Basahan for Central Bikol, Badlit for Cebuano, Kur-itan for Ilocano and Baybayin for Tagalog. The value can be either the actual Baybayin/etc. spelling or the Latin-script equivalent. The value <code>+</code> is equivalent to specifying the page name. Multiple comma-separated values can be given (with no space after the comma), along with per-value inline modifiers to specify qualifiers, labels and references. ; {{para|j}} : Specify the corresponding Arabic-script spelling of a Latin-script term, for languages that support Arabic scripts (Maguindanao, where it goes by the name of ''Jawi'', and Maranao, where it goes by the name of ''batang Arab''). Currently the value must be the Arabic script spelling; unlike for native scripts, the Latin-script equivalent cannot be given. Multiple comma-separated values can be given (with no space after the comma), along with per-value inline modifiers to specify qualifiers, labels and references. ; {{para|id}} : Sense ID for linking to this headword. See {{tl|senseid}} for more information. ; {{para|nolink|1}} ''or'' {{para|nolinkhead|1}} : Don't link individual words in the headword of a multiword term. Useful for foreign or otherwise unanalyzable terms, where the expression functions as a whole in Tagalog but the individual parts are not Tagalog words. ; {{para|suffix|1}} : Indicate that a term is a suffix even if it doesn't begin with a hyphen. Useful for Baybayin-script suffixes. See [[#Suffix handling]] below. ; {{para|nosuffix|1}} : Prevent terms beginning with a hyphen from being interpreted as suffixes. See [[#Suffix handling]] below. ; {{para|addlpos}} : Comma-separated list of additional parts of speech that suffix can form. See [[#Suffix handling]] below. ; {{para|pagename}} : Override the page name used to compute default values of various sorts. Useful when testing, for documentation pages, etc. ==Suffix handling== If the term begins with a hyphen (<code>-</code>), it is assumed to be a suffix rather than a base form, and is categorized into [[:Category:Tagalog suffixes]] and [[:Category:Tagalog POS-forming suffixes]] rather than [[:Category:Tagalog POSs]] (e.g. [[:Category:Tagalog noun-forming suffixes]] rather than [[:Category:Tagalog nouns]]). This behavior can also be obtained for terms not beginning with a hyphen using {{para|suffix|1}}, and can be prevented using {{para|nosuffix|1}}. If the suffix can form multiple parts of speech and it is not desirable to use separate headers for these (e.g. for alternative forms), use {{para|addlpos}} to specify the additional parts of speech (comma-separated). b8m9lop96ac6gm63aflv2e6xlg2c6j0 178081 178055 2026-09-25T07:23:36Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 sinalin ang pang-ulo ng talahanayan 178081 wikitext text/x-wiki This module is used for Philippine-language headword-line templates. Currently it supports: {| class=wikitable ! Wika !! Pangngalan !! Pangngalang pantangi !! Pandiwa !! Pang-uri !! Pang-abay !! Ibang bahagi ng pananalita |- | {{catlink|Aklanon headword-line templates|Aklanon}} | {{tl|akl-noun}} | {{tl|akl-proper noun}} | {{tl|akl-verb}} | {{tl|akl-adj}} | {{tl|akl-adv}} | {{tl|akl-head}} |- | {{catlink|Central Bikol headword-line templates|Central Bikol}} | {{tl|bcl-noun}} | {{tl|bcl-proper noun}} | {{tl|bcl-verb}} | {{tl|bcl-adj}} | {{tl|bcl-adv}} | {{tl|bcl-head}} |- | {{catlink|Buhid headword-line templates|Buhid}} | | | | | | {{tl|bku-head}} |- | {{catlink|Cebuano headword-line templates|Cebuano}} | {{tl|ceb-noun}} | {{tl|ceb-proper noun}} | {{tl|ceb-verb}} | {{tl|ceb-adj}} | {{tl|ceb-adv}} | {{tl|ceb-num}} (for numerals) <br/> {{tl|ceb-head}} |- | {{catlink|Chavacano headword-line templates|Chavacano}} | {{tl|cbk-noun}} | {{tl|cbk-proper noun}} | {{tl|cbk-verb}} | {{tl|cbk-adj}} | {{tl|cbk-adv}} | {{tl|cbk-head}} |- | {{catlink|Hanunoo headword-line templates|Hanunoo}} | {{tl|hnn-noun}} | {{tl|hnn-proper noun}} | {{tl|hnn-verb}} | {{tl|hnn-adj}} | {{tl|hnn-adv}} | {{tl|hnn-letter}} (for letters) <br/> {{tl|hnn-head}} |- | {{catlink|Hiligaynon headword-line templates|Hiligaynon}} | {{tl|hil-noun}} | {{tl|hil-proper noun}} | {{tl|hil-verb}} | {{tl|hil-adj}} | {{tl|hil-adv}} | {{tl|hil-head}} |- | {{catlink|Ilocano headword-line templates|Ilocano}} | {{tl|ilo-noun}} | {{tl|ilo-proper noun}} | {{tl|ilo-verb}} | {{tl|ilo-adj}} | {{tl|ilo-adv}} | {{tl|ilo-head}} |- | {{catlink|Kankanaey headword-line templates|Kankanaey}} | {{tl|kne-noun}} | {{tl|kne-proper noun}} | {{tl|kne-verb}} | {{tl|kne-adj}} | {{tl|kne-adv}} | {{tl|kne-root}} (for roots with unknown POS) <br/> {{tl|kne-head}} |- | {{catlink|Kapampangan headword-line templates|Kapampangan}} | {{tl|pam-noun}} | {{tl|pam-proper noun}} | {{tl|pam-verb}} | {{tl|pam-adj}} | {{tl|pam-adv}} | {{tl|pam-head}} |- | {{catlink|Kinaray-a headword-line templates|Kinaray-a}} | {{tl|krj-noun}} | {{tl|krj-proper noun}} | {{tl|krj-verb}} | {{tl|krj-adj}} | {{tl|krj-adv}} | {{tl|krj-head}} |- | {{catlink|Maguindanao headword-line templates|Maguindanao}} | {{tl|mdh-noun}} | {{tl|mdh-proper noun}} | {{tl|mdh-verb}} | {{tl|mdh-adj}} | {{tl|mdh-adv}} | {{tl|mdh-head}} |- | {{catlink|Maranao headword-line templates|Maranao}} | {{tl|mrw-noun}} | {{tl|mrw-proper noun}} | {{tl|mrw-verb}} | {{tl|mrw-adj}} | {{tl|mrw-adv}} | {{tl|mrw-head}} |- | {{catlink|Pangasinan headword-line templates|Pangasinan}} | {{tl|pag-noun}} | {{tl|pag-proper noun}} | {{tl|pag-verb}} | {{tl|pag-adj}} | {{tl|pag-adv}} | {{tl|pag-head}} |- | {{catlink|Tagalog headword-line templates|Tagalog}} | {{tl|tl-noun}} | {{tl|tl-proper noun}} | {{tl|tl-verb}} | {{tl|tl-adj}} | {{tl|tl-adv}} | {{tl|tl-num}} (for numerals) <br/> {{tl|tl-prep}} (for prepositions) <br/> {{tl|tl-pron}} (for pronouns) <br/> {{tl|tl-letter}} (for letters) <br/> {{tl|tl-head}} |- | {{catlink|Tausug headword-line templates|Tausug}} | {{tl|tsg-noun}} | {{tl|tsg-proper noun}} | {{tl|tsg-verb}} | {{tl|tsg-adj}} | {{tl|tsg-adv}} | {{tl|tsg-head}} |- | {{catlink|Waray-Waray headword-line templates|Waray-Waray}} | {{tl|war-noun}} | {{tl|war-proper noun}} | {{tl|war-verb}} | {{tl|war-adj}} | {{tl|war-adv}} | {{tl|war-head}} |} '''The rest of this documentation describes parameters from the point of view of Tagalog, but other languages work very similarly.''' The module is always invoked the same way, by passing a single parameter to the "show" function. This parameter is the name of the part of speech, but in plural (examples given are for nouns, and for adjective forms respectively): <pre> {{#invoke:tl-headword|show|nouns}} {{#invoke:tl-headword|show|adjective forms}} </pre> If the part of speech is omitted, the template being implemented must have the part of speech (in its singular form, e.g. <code>interjection</code>) specified in {{para|1}}. This is used to support {{tl|tl-head}}. The template will, by default, accept the following parameters (specific parts of speech may accept or require others): ; {{para|1}} : Override the headword display; used to add accents to the headword and/or links to individual words in a multiword term. Use the value <code>+</code> to explicitly request the default (which is the same as the page name). Note that it no longer works to specify a blank parameter to request the default; you must use <code>+</code>. Note also that if the invoking template is {{tl|tl-head}}, parameter {{para|1}} is instead the part of speech (in its singular form, e.g. <code>interjection</code>), and {{para|2}} is the first explicit headword. Multiple comma-separated values can be given (with no space after the comma), along with per-value inline modifiers to specify qualifiers, labels, references and manual transliterations. ; {{para|b}} : Specify the corresponding Baybayin-script or other native-script spelling of a Latin-script term, for languages that support native scripts: Basahan for Central Bikol, Badlit for Cebuano, Kur-itan for Ilocano and Baybayin for Tagalog. The value can be either the actual Baybayin/etc. spelling or the Latin-script equivalent. The value <code>+</code> is equivalent to specifying the page name. Multiple comma-separated values can be given (with no space after the comma), along with per-value inline modifiers to specify qualifiers, labels and references. ; {{para|j}} : Specify the corresponding Arabic-script spelling of a Latin-script term, for languages that support Arabic scripts (Maguindanao, where it goes by the name of ''Jawi'', and Maranao, where it goes by the name of ''batang Arab''). Currently the value must be the Arabic script spelling; unlike for native scripts, the Latin-script equivalent cannot be given. Multiple comma-separated values can be given (with no space after the comma), along with per-value inline modifiers to specify qualifiers, labels and references. ; {{para|id}} : Sense ID for linking to this headword. See {{tl|senseid}} for more information. ; {{para|nolink|1}} ''or'' {{para|nolinkhead|1}} : Don't link individual words in the headword of a multiword term. Useful for foreign or otherwise unanalyzable terms, where the expression functions as a whole in Tagalog but the individual parts are not Tagalog words. ; {{para|suffix|1}} : Indicate that a term is a suffix even if it doesn't begin with a hyphen. Useful for Baybayin-script suffixes. See [[#Suffix handling]] below. ; {{para|nosuffix|1}} : Prevent terms beginning with a hyphen from being interpreted as suffixes. See [[#Suffix handling]] below. ; {{para|addlpos}} : Comma-separated list of additional parts of speech that suffix can form. See [[#Suffix handling]] below. ; {{para|pagename}} : Override the page name used to compute default values of various sorts. Useful when testing, for documentation pages, etc. ==Suffix handling== If the term begins with a hyphen (<code>-</code>), it is assumed to be a suffix rather than a base form, and is categorized into [[:Category:Tagalog suffixes]] and [[:Category:Tagalog POS-forming suffixes]] rather than [[:Category:Tagalog POSs]] (e.g. [[:Category:Tagalog noun-forming suffixes]] rather than [[:Category:Tagalog nouns]]). This behavior can also be obtained for terms not beginning with a hyphen using {{para|suffix|1}}, and can be prevented using {{para|nosuffix|1}}. If the suffix can form multiple parts of speech and it is not desirable to use separate headers for these (e.g. for alternative forms), use {{para|addlpos}} to specify the additional parts of speech (comma-separated). sqrio7eixih8bozr3qcm0ka0yomxj3q Module:etymon/tracking 828 38861 178057 2026-09-24T23:54:37Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 copied over (oldid 91079109) 178057 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 Module:etymon/data 828 38862 178058 2026-09-24T23:59:37Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 initial copy over, isasalin pa 178058 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", "pclq" }, })), ["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 = { "rdp", "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 fq1aqk6hdtj8bm1m2r7eza1upak8cdt 178070 178058 2026-09-25T02:13:45Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 simula ng iilang pagsasalin, hango sa umiiral na, katulad ng mga padron nito na kinuhaan ng gagamitin salin 178070 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 = "Mula sa", phrase = "mula sa", --TLCHANGE term_rules = { entry_lang = true }, })), ["inherited"] = create_keyword(deep_merge(DEFAULTS.inheritance, { text = "Minana mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE glossary = "minana", --TLCHANGE 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 = "Mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE toplevel_category = "undefined derivations", })), ["derived"] = create_keyword(deep_merge(DEFAULTS.transitive, { text = "Hinango mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE abbrev = "der.", glossary = "derived terms", aliases = { "der" }, })), -- -- Affix/compound group keywords -- ["affix"] = create_keyword(deep_merge(DEFAULTS.affix_group, { text = "Mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE min_etymons = 1, aliases = { "af" }, })), ["blend"] = create_keyword(deep_merge(DEFAULTS.affix_group, { text = "Pagpapasama ng", --TLCHANGE phrase = "isang pagpapasama ng", --TLCHANGE abbrev = "blend", glossary = "pagpapasama", --TLCHANGE toplevel_category = "pagpapasama", --TLCHANGE })), ["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 = "Hiniram mula sa", --TLCHANGE phrase = "hiniram mula sa", --TLCHANGE abbrev = "bor.", glossary = "loanword", borrowing_type = "borrowed", aliases = { "borrowed" }, })), ["lbor"] = create_keyword(deep_merge(DEFAULTS.borrowing, { text = "Natutuhang paghihiram mula sa", --TLCHANGE phrase = "isang natutuhang paghihiram mula sa", --TLCHANGE 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 = "Bahagyang natutuhang paghihiram mula sa", --TLCHANGE phrase = "isang bahagyang natutuhang paghihiram mula sa", --TLCHANGE 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", "pclq" }, })), ["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 = "Paggupit ng", --TLCHANGE phrase = "paggupit ng", --TLCHANGE abbrev = "clip.", glossary = "paggupit", --TLCHANGE toplevel_category = "paggupit", --TLCHANGE 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 ng", --TLCHANGE phrase = "reduplication ng", --TLCHANGE abbrev = "redup.", glossary = "reduplication", toplevel_category = "reduplications", aliases = { "rdp", "redup" }, })), ["abbreviation"] = create_keyword(deep_merge(DEFAULTS.transitive, { text = "Daglat ng", --TLCHANGE phrase = "daglat ng", --TLCHANGE abbrev = "abbr.", glossary = "daglat", --TLCHANGE 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 = "Akronim ng", --TLCHANGE phrase = "akronim ng", --TLCHANGE abbrev = "acronym", glossary = "akronim", --TLCHANGE aliases = { "acro" }, })), ["initialism"] = create_keyword(deep_merge(DEFAULTS.transitive, { text = "Inisyalismo ng", --TLCHANGE phrase = "inisyalismo ng", --TLCHANGE abbrev = "init.", glossary = "inisyalismo", --TLCHANGE 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 = "Mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE transitive = TRANSITIVE.NEVER, min_etymons = 1, invisible = "all", })), } -- Template-parameter supplements. export.supplements = { doublet = { text = "Doblete ng", --TLCHANGE phrase = "doblete ng", --TLCHANGE glossary = "doblete", --TLCHANGE toplevel_category = "doblete", --TLCHANGE default_conj = "at", --TLCHANGE 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 nblv5eaq41jtgcp8euw44bqpsl2xmh6 178078 178070 2026-09-25T03:22:37Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 178078 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" -- TLCHANGE: inangkop para sa mga tekstong katulad ng "Hiniram mula sa" at ng "reduplication ng" local function split_glossary_text(text) for _, pattern in ipairs({ "^(.-)(%s+[Nn][Gg])$", "^(.-)(%s+[Mm][Uu][Ll][Aa]%s+[Ss][Aa])$" }) 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 = "Mula sa", phrase = "mula sa", --TLCHANGE term_rules = { entry_lang = true }, })), ["inherited"] = create_keyword(deep_merge(DEFAULTS.inheritance, { text = "Minana mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE glossary = "minana", --TLCHANGE 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 = "Mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE toplevel_category = "undefined derivations", })), ["derived"] = create_keyword(deep_merge(DEFAULTS.transitive, { text = "Hinango mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE abbrev = "der.", glossary = "derived terms", aliases = { "der" }, })), -- -- Affix/compound group keywords -- ["affix"] = create_keyword(deep_merge(DEFAULTS.affix_group, { text = "Mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE min_etymons = 1, aliases = { "af" }, })), ["blend"] = create_keyword(deep_merge(DEFAULTS.affix_group, { text = "Pagpapasama ng", --TLCHANGE phrase = "isang pagpapasama ng", --TLCHANGE abbrev = "blend", glossary = "pagpapasama", --TLCHANGE toplevel_category = "pagpapasama", --TLCHANGE })), ["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 = "Hiniram mula sa", --TLCHANGE phrase = "hiniram mula sa", --TLCHANGE abbrev = "bor.", glossary = "loanword", borrowing_type = "borrowed", aliases = { "borrowed" }, })), ["lbor"] = create_keyword(deep_merge(DEFAULTS.borrowing, { text = "Natutuhang paghihiram mula sa", --TLCHANGE phrase = "isang natutuhang paghihiram mula sa", --TLCHANGE 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 = "Bahagyang natutuhang paghihiram mula sa", --TLCHANGE phrase = "isang bahagyang natutuhang paghihiram mula sa", --TLCHANGE 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", "pclq" }, })), ["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 = "Paggupit ng", --TLCHANGE phrase = "paggupit ng", --TLCHANGE abbrev = "clip.", glossary = "paggupit", --TLCHANGE toplevel_category = "paggupit", --TLCHANGE 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 ng", --TLCHANGE phrase = "reduplication ng", --TLCHANGE abbrev = "redup.", glossary = "reduplication", toplevel_category = "reduplications", aliases = { "rdp", "redup" }, })), ["abbreviation"] = create_keyword(deep_merge(DEFAULTS.transitive, { text = "Daglat ng", --TLCHANGE phrase = "daglat ng", --TLCHANGE abbrev = "abbr.", glossary = "daglat", --TLCHANGE 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 = "Akronim ng", --TLCHANGE phrase = "akronim ng", --TLCHANGE abbrev = "acronym", glossary = "akronim", --TLCHANGE aliases = { "acro" }, })), ["initialism"] = create_keyword(deep_merge(DEFAULTS.transitive, { text = "Inisyalismo ng", --TLCHANGE phrase = "inisyalismo ng", --TLCHANGE abbrev = "init.", glossary = "inisyalismo", --TLCHANGE 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 = "Mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE transitive = TRANSITIVE.NEVER, min_etymons = 1, invisible = "all", })), } -- Template-parameter supplements. export.supplements = { doublet = { text = "Doblete ng", --TLCHANGE phrase = "doblete ng", --TLCHANGE glossary = "doblete", --TLCHANGE toplevel_category = "doblete", --TLCHANGE default_conj = "at", --TLCHANGE 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 peadm365fmvwxdfj8yhs7gaupwsi47f 178079 178078 2026-09-25T03:24:12Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 gamitin ang angkop na Glosaryo 178079 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 "[[Apendise:Glosaryo#" .. term:gsub(" ", "_") .. "|" .. display_text .. "]]" end -- Extract base word and connector from text like "Borrowed from" or "calque of" -- TLCHANGE: inangkop para sa mga tekstong katulad ng "Hiniram mula sa" at ng "reduplication ng" local function split_glossary_text(text) for _, pattern in ipairs({ "^(.-)(%s+[Nn][Gg])$", "^(.-)(%s+[Mm][Uu][Ll][Aa]%s+[Ss][Aa])$" }) 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 = "Mula sa", phrase = "mula sa", --TLCHANGE term_rules = { entry_lang = true }, })), ["inherited"] = create_keyword(deep_merge(DEFAULTS.inheritance, { text = "Minana mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE glossary = "minana", --TLCHANGE 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 = "Mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE toplevel_category = "undefined derivations", })), ["derived"] = create_keyword(deep_merge(DEFAULTS.transitive, { text = "Hinango mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE abbrev = "der.", glossary = "derived terms", aliases = { "der" }, })), -- -- Affix/compound group keywords -- ["affix"] = create_keyword(deep_merge(DEFAULTS.affix_group, { text = "Mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE min_etymons = 1, aliases = { "af" }, })), ["blend"] = create_keyword(deep_merge(DEFAULTS.affix_group, { text = "Pagpapasama ng", --TLCHANGE phrase = "isang pagpapasama ng", --TLCHANGE abbrev = "blend", glossary = "pagpapasama", --TLCHANGE toplevel_category = "pagpapasama", --TLCHANGE })), ["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 = "Hiniram mula sa", --TLCHANGE phrase = "hiniram mula sa", --TLCHANGE abbrev = "bor.", glossary = "loanword", borrowing_type = "borrowed", aliases = { "borrowed" }, })), ["lbor"] = create_keyword(deep_merge(DEFAULTS.borrowing, { text = "Natutuhang paghihiram mula sa", --TLCHANGE phrase = "isang natutuhang paghihiram mula sa", --TLCHANGE 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 = "Bahagyang natutuhang paghihiram mula sa", --TLCHANGE phrase = "isang bahagyang natutuhang paghihiram mula sa", --TLCHANGE 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", "pclq" }, })), ["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 = "Paggupit ng", --TLCHANGE phrase = "paggupit ng", --TLCHANGE abbrev = "clip.", glossary = "paggupit", --TLCHANGE toplevel_category = "paggupit", --TLCHANGE 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 ng", --TLCHANGE phrase = "reduplication ng", --TLCHANGE abbrev = "redup.", glossary = "reduplication", toplevel_category = "reduplications", aliases = { "rdp", "redup" }, })), ["abbreviation"] = create_keyword(deep_merge(DEFAULTS.transitive, { text = "Daglat ng", --TLCHANGE phrase = "daglat ng", --TLCHANGE abbrev = "abbr.", glossary = "daglat", --TLCHANGE 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 = "Akronim ng", --TLCHANGE phrase = "akronim ng", --TLCHANGE abbrev = "acronym", glossary = "akronim", --TLCHANGE aliases = { "acro" }, })), ["initialism"] = create_keyword(deep_merge(DEFAULTS.transitive, { text = "Inisyalismo ng", --TLCHANGE phrase = "inisyalismo ng", --TLCHANGE abbrev = "init.", glossary = "inisyalismo", --TLCHANGE 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 = "Mula sa", --TLCHANGE phrase = "mula sa", --TLCHANGE transitive = TRANSITIVE.NEVER, min_etymons = 1, invisible = "all", })), } -- Template-parameter supplements. export.supplements = { doublet = { text = "Doblete ng", --TLCHANGE phrase = "doblete ng", --TLCHANGE glossary = "doblete", --TLCHANGE toplevel_category = "doblete", --TLCHANGE default_conj = "at", --TLCHANGE 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 ca4i4qd1ozcoiccg15csz9cj24tpnn6 Module:etymon/descendants 828 38863 178059 2026-09-25T00:01:45Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 copied over (oldid 91079123) 178059 Scribunto text/plain local export = {} local M = require("Module:module loader").init({ require = { template_parser = "Module:template parser", links = "Module:links", languages = "Module:languages", }, }) local TRACKABLE_KEYWORDS = { inherited = true, bor = true, lbor = true, slbor = true, derived = true, uder = true, } local DESCENDANT_TEMPLATES = { ["desc"] = true, ["descendant"] = true, ["desctree"] = true, ["descendants tree"] = true, } local SKIPPED_TERMS = { [""] = true, ["-"] = true, } local CHECK_MISSING_HEADER = { missing_header = true, missing_entry = false } local STRIPPED_REF_TEMPLATES = { ref = true, refn = true, } local ENABLE_DESCENDANTS_TRACKING = false local function _resolve_explicit_cache_id(explicit_id, parent_etymon) if parent_etymon then return parent_etymon.id or "*" end return explicit_id end local function _resolve_single_cache_id(id_data) return (type(id_data) == "table" and id_data.id) or id_data or "*" end local function _resolve_cache_id(lookup) if not lookup then return nil end if lookup.id then return lookup.id end if lookup.explicit_id then return _resolve_explicit_cache_id(lookup.explicit_id, lookup.parent_etymon) end if lookup.id_data ~= nil then return _resolve_single_cache_id(lookup.id_data) end return nil end local function _get_cached_check(cache_table, is_toplevel, base_key, cache_id) if not is_toplevel or not cache_table or not base_key or not cache_id then return nil end return cache_table[base_key .. ":" .. cache_id] end local function _store_checks(cache_table, lang_page_key, checks_by_id, redirected_from) if not cache_table or not lang_page_key or not checks_by_id then return end for id, check in pairs(checks_by_id) do cache_table[lang_page_key .. ":" .. id] = check end if redirected_from then for id, check in pairs(checks_by_id) do cache_table[redirected_from .. ":" .. id] = check end end end local function _strip_refs(value) if type(value) ~= "string" or value == "" then return value end local get_node_class = M.template_parser.class_else_type local function trim_local(text) return (type(text) == "string" and text:match("^%s*(.-)%s*$")) or "" end local function lower_local(text) return text and mw.ustring.lower(text) or text end local function template_name_unexpanded(template_node) return lower_local(trim_local(tostring(template_node[1]) or "")) end local function stringify_without_ref_templates(node) if type(node) ~= "table" then return node == nil and "" or tostring(node) end local node_class = get_node_class(node) if node_class == "template" and STRIPPED_REF_TEMPLATES[template_name_unexpanded(node)] then return "" end if node_class == "template" then return tostring(node) end if node_class == "heading" then return tostring(node) end if node_class and node_class ~= "wikitext" then return tostring(node) end local pieces = {} for i = 1, #node do pieces[i] = stringify_without_ref_templates(node[i]) end return table.concat(pieces) end local parsed = M.template_parser.parse(value) local cleaned = stringify_without_ref_templates(parsed) local changed = true while changed do local next_cleaned = mw.ustring.gsub(cleaned, "<%s*[Rr][Ee][Ff][^>]-/>", "") next_cleaned = mw.ustring.gsub(next_cleaned, "<%s*[Rr][Ee][Ff][^>]*>.-<%s*/%s*[Rr][Ee][Ff]%s*>", "") next_cleaned = mw.ustring.gsub(next_cleaned, "{{%s*[Rr][Ee][Ff]%s*}}", "") next_cleaned = mw.ustring.gsub(next_cleaned, "{{%s*[Rr][Ee][Ff]%s*|[^{}]-}}", "") changed = next_cleaned ~= cleaned cleaned = next_cleaned end return cleaned end local function _trim(value) if type(value) ~= "string" then return nil end return value:match("^%s*(.-)%s*$") end local function _lower(value) return value and mw.ustring.lower(value) or value end local function _is_index_in_range(index, start_index, end_index) return index and index >= start_index and index <= end_index end local function _build_parse_index(lang_section) local parsed = M.template_parser.parse(lang_section) local headings = {} local templates = {} for heading in parsed:iterate_nodes("heading") do local heading_name = heading:get_name() table.insert(headings, { index = heading.index or 1, level = heading.level or 0, name = heading_name and _lower(_trim(heading_name) or "") or nil, length = #tostring(heading), }) end for template in parsed:iterate_nodes("template") do local template_name = template:get_name() local template_args = template:get_arguments() table.insert(templates, { index = template.index or 1, name = template_name and _lower(_trim(template_name) or "") or "", args = template_args or {}, }) end return { headings = headings, templates = templates, } end local function _build_regions(lang_section, headings) local content_length = #lang_section local etymology_headings = {} for _, heading in ipairs(headings) do if heading.level == 3 and heading.name and mw.ustring.match(heading.name, "^etymology") then table.insert(etymology_headings, heading) end end if #etymology_headings == 0 then return { { start_index = 1, end_index = content_length } } end local regions = {} for i = 1, #etymology_headings do local heading = etymology_headings[i] local next_heading = etymology_headings[i + 1] local start_index = heading.index local end_index = next_heading and (next_heading.index - 1) or content_length if i == 1 and heading.index > 1 then local preamble = lang_section:sub(1, heading.index - 1) if preamble:match("%S") then start_index = 1 end end table.insert(regions, { start_index = start_index, end_index = end_index, }) end return regions end local function _count_region_etymons(region, templates, etymon_lang_code) local count = 0 for _, template in ipairs(templates) do if _is_index_in_range(template.index, region.start_index, region.end_index) and template.name == "etymon" and template.args[1] == etymon_lang_code then count = count + 1 end end return count end local function _get_descendants_sections(region, headings) local sections = {} for i = 1, #headings do local heading = headings[i] if _is_index_in_range(heading.index, region.start_index, region.end_index) and heading.name == "descendants" then local body_start = heading.index + heading.length local body_end = region.end_index for j = i + 1, #headings do local next_heading = headings[j] if next_heading.index > region.end_index then break end if next_heading.level <= heading.level then body_end = next_heading.index - 1 break end end table.insert(sections, { start_index = body_start, end_index = body_end, }) end end return sections end local function _template_lang_matches_entry(template_lang_code, entry_full_code, cache) local normalized_code = _trim(template_lang_code) if not normalized_code or normalized_code == "" then return false end local cached = cache[normalized_code] if cached ~= nil then return cached end local template_lang = M.languages.getByCode(normalized_code, nil, true) local is_match = template_lang and template_lang:getFullCode() == entry_full_code or false cache[normalized_code] = is_match return is_match end local function _template_lists_target(template_args, first_term_index, target_page_lower, entry_lang) for arg_index = first_term_index, #template_args do local template_term = _trim(template_args[arg_index]) if template_term and not SKIPPED_TERMS[template_term] then if _lower(template_term) == target_page_lower then return true end local template_page = M.links.get_link_page(template_term, entry_lang) if _lower(template_page) == target_page_lower then return true end end end return false end local function _is_target_listed_in_descendants(descendants_sections, templates, entry_title, entry_lang) if #descendants_sections == 0 then return false end local target_page = M.links.get_link_page(entry_title, entry_lang) local target_page_lower = _lower(target_page) local entry_full_code = entry_lang:getFullCode() local lang_match_cache = {} for _, section in ipairs(descendants_sections) do for _, template in ipairs(templates) do if _is_index_in_range(template.index, section.start_index, section.end_index) and DESCENDANT_TEMPLATES[template.name] then local lang_arg_index = nil for arg_index = 1, #template.args do if _template_lang_matches_entry(template.args[arg_index], entry_full_code, lang_match_cache) then lang_arg_index = arg_index break end end if lang_arg_index and _template_lists_target(template.args, lang_arg_index + 1, target_page_lower, entry_lang) then return true end end end end return false end local function _region_has_descendant_template(region, templates) for _, template in ipairs(templates) do if _is_index_in_range(template.index, region.start_index, region.end_index) and DESCENDANT_TEMPLATES[template.name] then return true end end return false end local function _get_region_check(region, parsed_index, entry_title, entry_lang) local descendants_sections = _get_descendants_sections(region, parsed_index.headings) if #descendants_sections == 0 then if _region_has_descendant_template(region, parsed_index.templates) then local fallback_sections = { { start_index = region.start_index, end_index = region.end_index, } } return { missing_header = false, missing_entry = not _is_target_listed_in_descendants(fallback_sections, parsed_index.templates, entry_title, entry_lang), } end return CHECK_MISSING_HEADER end return { missing_header = false, missing_entry = not _is_target_listed_in_descendants(descendants_sections, parsed_index.templates, entry_title, entry_lang), } end local function _build_checks_by_id(lang_section, etymon_lang_code, found_templates_for_lang, entry_title, entry_lang) local checks_by_id = {} local parsed_index = _build_parse_index(lang_section) local regions = _build_regions(lang_section, parsed_index.headings) local template_list_index = 1 local mapping_failed = false for _, region in ipairs(regions) do local region_etymon_count = _count_region_etymons(region, parsed_index.templates, etymon_lang_code) local region_check = nil if region_etymon_count > 0 then region_check = _get_region_check(region, parsed_index, entry_title, entry_lang) end for _ = 1, region_etymon_count do local found_template_args = found_templates_for_lang[template_list_index] if not found_template_args then mapping_failed = true break end checks_by_id[found_template_args.id or "*"] = region_check template_list_index = template_list_index + 1 end if mapping_failed then break end end if mapping_failed or template_list_index ~= (#found_templates_for_lang + 1) then local global_region = { start_index = 1, end_index = #lang_section } local fallback_check = _get_region_check(global_region, parsed_index, entry_title, entry_lang) checks_by_id = {} for _, template_args in ipairs(found_templates_for_lang) do checks_by_id[template_args.id or "*"] = fallback_check end end return checks_by_id end local function _compute_checks_for_page(opts) opts = opts or {} local found_templates_for_lang = opts.found_templates_for_lang or {} if #found_templates_for_lang == 0 then return {} end local entry_title = opts.entry_title local entry_lang = opts.entry_lang local etymon_lang_code = opts.etymon_lang_code if not entry_title or not entry_lang or not etymon_lang_code then return {} end local sanitized_lang_section = _strip_refs(opts.lang_section or "") return _build_checks_by_id( sanitized_lang_section, etymon_lang_code, found_templates_for_lang, entry_title, entry_lang ) end function export.cache_page_checks(opts) if not ENABLE_DESCENDANTS_TRACKING then return {} end opts = opts or {} local cache_table = opts.cached_descendants_checks local lang_page_key = opts.lang_page_key if not cache_table or not lang_page_key then return {} end local checks_by_id = _compute_checks_for_page(opts) _store_checks(cache_table, lang_page_key, checks_by_id, opts.redirected_from) return checks_by_id end function export.get_lookup_check(opts) if not ENABLE_DESCENDANTS_TRACKING then return nil end opts = opts or {} return _get_cached_check( opts.cached_descendants_checks, opts.is_toplevel, opts.base_key, _resolve_cache_id(opts.lookup) ) end function export.get_term_sync_flags(keyword, term_status, descendants_check) if not ENABLE_DESCENDANTS_TRACKING then return false, false end local should_track = term_status == "ok" and TRACKABLE_KEYWORDS[keyword] == true and descendants_check ~= nil if not should_track then return false, false end return descendants_check.missing_header or false, descendants_check.missing_entry or false end return export njoa75ldlbxvifpj024ykipfx44kfcd Module:etymon/data/text allowed 828 38864 178060 2026-09-25T00:03:22Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 copied over (oldid 92775692) 178060 Scribunto text/plain --[=[ Languages and families that may use the {{etymon}} `text=` parameter (language-community consensus). ]=] return { -- Mode: "off" = disabled, "warn" = warn only, "error" = enforce. default_mode = "warn", langs = { ["ak"] = true, ["amf"] = true, ["bg"] = true, ["bnt-sab-pro"] = true, ["cs"] = true, ["en"] = true, ["eo"] = true, ["es"] = true, ["ext"] = true, ["fa"] = true, ["gmw-msc"] = true, ["hsb"] = true, ["iir-pro"] = true, ["jbo"] = true, ["jdt"] = true, ["la"] = true, ["mul"] = true, ["ota"] = true, ["pap"] = true, ["ps"] = true, ["ro"] = true, ["sco"] = true, ["sk"] = true, ["sw"] = true, ["tg"] = true, ["tl"] = true, ["tr"] = true, ["uk"] = true, ["uz"] = true, ["sl"] = true, ["rsk"] = true, ["zlw-ocs"] = true, ["zlw-osk"] = true, ["zle-ono"] = true, ["zle-ort"] = true, ["sla-pro"] = true, ["eu"] = true, -- Austronesian ["map-pro"] = true, ["map-ata-pro"] = true, ["poz-pro"] = true, ["poz-btk-pro"] = true, ["poz-cet-pro"] = true, ["pqe-pro"] = true, ["poz-hce-pro"] = true, ["poz-oce-pro"] = true, ["poz-pol-pro"] = true, ["poz-pnp-pro"] = true, ["poz-pep-pro"] = true, ["poz-mic-pro"] = true, ["poz-lgx-pro"] = true, ["poz-msa-pro"] = true, ["poz-mcm-pro"] = true, ["cmc-pro"] = true, ["poz-mly-pro"] = true, ["poz-swa-pro"] = true, ["btk-pro"] = true, ["phi-pro"] = true, ["phi-kal-pro"] = true, ["poz-ssw-pro"] = true, ["dru-pro"] = true, ["crp-slb"] = true, }, families = { ["ber"] = true, -- Berber ["dra"] = true, -- Dravidian ["inc"] = true, -- Indo-Aryan ["iir-nur"] = true, -- Nuristani ["mun"] = true, -- Munda ["roa-gap"] = true, -- Galician-Portuguese ["sem-ara"] = true, -- Aramaic ["sem-arb"] = true, -- Arabic ["tup"] = true, -- Tupian ["bnt-sab"] = true, -- Lechitic ["zlw-lch"] = true, -- Lechitic }, } d2t0jck7ixvesghw7yzk9gdr2q8prgn Module:etymon/categories 828 38865 178062 2026-09-25T00:09:54Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 copied over (oldid 92139470) at may-isasalin 178062 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 178066 178062 2026-09-25T00:33:38Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 initial na pagsasalin 178066 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 = "salita", --TLCHANGE 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 = "salitang minana", --TLCHANGE 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 maf5cszc0xbxk309q6xs9ky3u9om80c 178071 178066 2026-09-25T02:34:15Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 iilang pagsasalin 178071 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 = "salita", --TLCHANGE 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 = "salitang minana", --TLCHANGE 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, "Pahinang may etymon") --TLCHANGE table.insert(categories, lang_name .. " na entrada na may etymon") --TLCHANGE if opts.tree then table.insert(categories, "Pahinang may etymology tree") --TLCHANGE table.insert(categories, lang_name .. " na entrada na may etymology tree") --TLCHANGE end if opts.text then table.insert(categories, lang_name .. " na entrada na may etymology text") --TLCHANGE 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, "Pahinang gumagamit ng etymon na walang ID") --TLCHANGE 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 myf8akbcm2xcjgdwo41hfjwya3ca62n 178072 178071 2026-09-25T02:35:28Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 178072 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 = "salita", --TLCHANGE 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 = "salitang minana", --TLCHANGE 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, "Pahinang may etymon") --TLCHANGE table.insert(categories, lang_name .. " na entradang may etymon") --TLCHANGE if opts.tree then table.insert(categories, "Pahinang may etymology tree") --TLCHANGE table.insert(categories, lang_name .. " na entradang may etymology tree") --TLCHANGE end if opts.text then table.insert(categories, lang_name .. " na entradang may etymology text") --TLCHANGE 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, "Pahinang gumagamit ng etymon na walang ID") --TLCHANGE 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 ngmt51vb9qj38jtj7mvurt3v9dh976d Module:roots 828 38866 178063 2026-09-25T00:22:41Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 copied over (oldid 92769520) 178063 Scribunto text/plain local export = {} local force_cat = false -- for debugging local M = require("Module:module loader").init({ require = { str_utils = "Module:string utilities", utilities = "Module:utilities", links = "Module:links", headword = "Module:headword", languages = "Module:languages", parameters = "Module:parameters", inflection_table = "Module:inflection-table", }, loadData = { radicals = "Module:roots/data/radicals", }, lazy = { concat = function() return table.concat end, insert = function() return table.insert end, }, }) local UI_STRINGS = { no_langcode = "Please provide a language code.", invalid_langcode = "Invalid language code: %s", not_a_table = "rootTable is not a table", min_radicals = "Root must have at least %s %s.", max_radicals = "Root must have at most %s %s.", unrecognized_pos = "Unrecognized %s radical %s in %s", unrecognized_mem = "Unrecognized radical %s in %s", not_single_letter = "'%s' in the root '%s' should be a single letter.", invalid_validation = "Unknown validation model: %s", only_one_root = "There should be only one root.", bad_root = "Invalid %s root '%s': %s", wrong_separator = "Radicals should be separated by '%s'.", root_vs_pagename = "Root is different from the pagename.", move_to_appendix = "The root page should be in the Appendix namespace. Please move it to : [[%s]]", appendix_subpage = "Appendix:%s roots/", root_category = "%s terms belonging to the root %s%s", derived_category = "%s terms derived from the %s root %s%s", derived_language = "%s terms derived from %s", root_heading = "\n! [[w:%s|%s]]", sense_wrapper = "&lrm; <span class=\"gloss-content\">(%s)</span>", article_semitic = "Semitic root", article_generic = "Root (linguistics)", } local DEFAULT_LINK_TARGET = "bare" local DEFAULT_HEADWORD = {} local function split_root(root, separator) if not separator then return { root } end return mw.text.split(root, separator, true) end local function radicals_word(count) return count == 1 and "radical" or "radicals" end function export.get_data_module_name(langcode) return "Module:roots/data/" .. langcode end local function try_load_data(langcode) local ok, data = pcall(mw.loadData, export.get_data_module_name(langcode)) if ok then return data end return nil end function export.get_data(langcode) return try_load_data(langcode) end local function get_lang_and_data(frame) local code = frame.args["lang"] if not code or code == "" then error(UI_STRINGS.no_langcode) end local lang = M.languages.getByCode(code) if not lang then error(UI_STRINGS.invalid_langcode:format(code)) end return lang, try_load_data(code) or {} end local function resolve_radicals(data) if data.validation == "positional" or data.radicals then return data.radicals end if data.shared_radicals then return M.radicals[data.shared_radicals] end return nil end local function transliterate_root(root_string, romanization) if not romanization then return nil end local out = root_string for _, pair in ipairs(romanization) do out = M.str_utils.gsub(out, pair[1], pair[2]) end return out end export.transliterate = transliterate_root local function validate_root(rootTable, joined_root, data, radical_set) if type(rootTable) ~= "table" then return nil, UI_STRINGS.not_a_table end local len = #rootTable if data.min_radicals and len < data.min_radicals then return nil, UI_STRINGS.min_radicals:format(data.min_radicals, radicals_word(data.min_radicals)) end if data.max_radicals and len > data.max_radicals then return nil, UI_STRINGS.max_radicals:format(data.max_radicals, radicals_word(data.max_radicals)) end if data.validation == "positional" then for i, radical in ipairs(rootTable) do local set, label if i == 1 then set, label = radical_set.initial, "initial" elseif i == len then set, label = radical_set.final, "final" else set, label = radical_set.medial, "medial" end if not set[radical] then return nil, UI_STRINGS.unrecognized_pos:format(label, radical, joined_root) end end elseif data.validation == "membership" then for _, radical in ipairs(rootTable) do if not radical_set[radical] then return nil, UI_STRINGS.unrecognized_mem:format(radical, joined_root) end end elseif data.validation == "single_letter" then for _, radical in ipairs(rootTable) do if mw.ustring.len(radical) ~= 1 then return nil, UI_STRINGS.not_single_letter:format(radical, joined_root) end end elseif data.validation ~= nil then return nil, UI_STRINGS.invalid_validation:format(tostring(data.validation)) end return true end local function link(term, alt, translit, id, lang) if term == "" or term == "&mdash;" then return term end return M.links.full_link({ term = term, alt = alt ~= term and alt or nil, tr = translit, lang = lang, id = id, }) end local function root_category(lang, source, root, qualifier) local suffix = (qualifier and qualifier ~= "") and (" (" .. qualifier .. ")") or "" if lang:getCode() == source:getCode() then return UI_STRINGS.root_category:format(lang:getFullName(), root, suffix) end return UI_STRINGS.derived_category:format( lang:getFullName(), source:getCanonicalName(), root, suffix) end local function check_root(source, root) local data = try_load_data(source:getCode()) if not data then return true end local bare = (root:gsub("^%*", "")) local radicals = split_root(bare, data.separator) if data.separator and #radicals == 1 and mw.ustring.len(bare) > 1 then return nil, UI_STRINGS.wrong_separator:format(data.separator) end return validate_root(radicals, bare, data, resolve_radicals(data)) end function export.assert_root(source, root) local ok, err = check_root(source, root) if not ok then error(UI_STRINGS.bad_root:format(source:getCanonicalName(), root, err)) end return true end function export.get_categories(lang, source, roots, ids) local categories = {} ids = ids or {} if lang:getCode() ~= source:getCode() then M.insert(categories, UI_STRINGS.derived_language:format(lang:getFullName(), source:getCanonicalName())) end for index, root in ipairs(roots) do M.insert(categories, root_category(lang, source, root, ids[index])) export.assert_root(source, root) end return categories end function export.categorize(lang, source, roots, ids, sort_key) return M.utilities.format_categories( export.get_categories(lang, source, roots, ids), lang, sort_key, nil, force_cat) end function export.show(frame) local params = { [1] = { required = true, type = "language" }, [2] = { required = true, type = "language" }, [3] = { required = true, list = true }, ["id"] = { list = true, allow_holes = true }, ["sort"] = {}, } local args = M.parameters.process(frame:getParent().args, params) return export.categorize(args[1], args[2], args[3], args.id, args.sort) end function export.root(frame) local lang, data = get_lang_and_data(frame) local radical_set = resolve_radicals(data) local link_target = data.link_target or DEFAULT_LINK_TARGET local output = {} local categories = {} local title = mw.title.getCurrentTitle() local namespace = title.nsText local params = { [1] = { list = true }, ["nocat"] = { type = "boolean" }, ["plain"] = { type = "boolean" }, ["notext"] = { type = "boolean" }, ["sense"] = {}, } if link_target == "bare" then params["head"] = true params["pagename"] = true end local args = M.parameters.process(frame:getParent().args, params) local roots = args[1] local roots_len = #roots local plain = args["plain"] if frame.args["plain"] then plain = true end local separator = data.separator local identity local subpage = "" if link_target == "appendix_subpage" then subpage = UI_STRINGS.appendix_subpage:format(lang:getCanonicalName()) local fulltitle = title.fullText fulltitle = M.str_utils.gsub(fulltitle, M.str_utils.gsub(subpage, "([^%w])", "%%%1"), "") identity = fulltitle else identity = args["pagename"] or mw.loadData("Module:headword/data").pagename end local rootLetters = {} if roots_len == 0 and not args["pagename"] and namespace == "Template" and data.preview_root then M.insert(rootLetters, split_root(data.preview_root, separator)) elseif roots_len ~= 0 then for _, root in ipairs(roots) do M.insert(rootLetters, split_root(root, separator)) end else if link_target == "bare" then local head = frame.args["head"] if args["head"] ~= nil and identity ~= lang:stripDiacritics(args["head"]) then error(UI_STRINGS.root_vs_pagename) end if head and args["head"] ~= nil then identity = args["head"] end end M.insert(rootLetters, split_root(identity, separator)) end local joined_roots = {} for i, rootLetter in ipairs(rootLetters) do M.insert(joined_roots, M.concat(rootLetter, separator)) local ok, err = validate_root(rootLetter, joined_roots[i], data, radical_set) if not ok then error(err) end end local sense = args["sense"] local sense_formatted = "" if sense ~= nil and sense ~= "" then sense_formatted = UI_STRINGS.sense_wrapper:format(sense) end if identity == joined_roots[1] then if link_target == "appendix_subpage" and namespace == "" then error(UI_STRINGS.move_to_appendix:format(subpage .. joined_roots[1])) end if roots_len > 1 then error(UI_STRINGS.only_one_root) end local headword_args = { lang = lang, pos_category = "roots", categories = {}, heads = { identity }, nomultiwordcat = true, } local headword_opts = data.headword or DEFAULT_HEADWORD if headword_opts.translits then headword_args.translits = { transliterate_root(identity, data.romanization) } end if headword_opts.noposcat then headword_args.noposcat = true elseif headword_opts.nolemmacat then headword_args.nolemmacat = true end M.insert(output, M.headword.full_headword(headword_args)) if args["nocat"] then return M.concat(output) end return M.concat(output) .. M.concat(categories) else local link_texts = {} local term_counts = {} for i, joined_root in ipairs(joined_roots) do local link_text if link_target == "appendix_subpage" then link_text = subpage .. joined_root else link_text = joined_root end local translit = data.romanization and transliterate_root(joined_root, data.romanization) or nil M.insert(link_texts, link(link_text, joined_root, translit, sense, lang) .. sense_formatted) M.insert(categories, M.utilities.format_categories( { root_category(lang, lang, joined_root, sense) }, lang, nil, nil, force_cat)) M.insert(term_counts, mw.site.stats.pagesInCategory( root_category(lang, lang, joined_root, sense), "pages")) end if args["nocat"] or plain then if args["nocat"] then return M.concat(link_texts, ", ") end return M.concat(link_texts, ", ") .. M.concat(categories) else local link_text_output = "" for i, link_text in ipairs(link_texts) do link_text_output = link_text_output .. "\n|-\n| " .. link_text .. "\n|-\n| [[:Category:" .. root_category(lang, lang, joined_roots[i], sense) .. "|" .. term_counts[i] .. " term" .. (term_counts[i] == 1 and "" or "s") .. "]]\n" end local wikicode = M.inflection_table.make_top{ title = "-", palette = data.palette, class = "floatright tr-alongside" } local article = lang:inFamily("sem") and UI_STRINGS.article_semitic or UI_STRINGS.article_generic wikicode = wikicode .. UI_STRINGS.root_heading:format(article, #term_counts == 1 and "Root" or "Roots") wikicode = wikicode .. link_text_output wikicode = wikicode .. M.inflection_table.make_bottom{} return wikicode .. M.concat(categories) end end end return export p2drxayry7tfs92f53vqrwri6gq3o6n 178065 178063 2026-09-25T00:28:34Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 isinalin ang UI_STRINGS.derived_language, hango sa katulad nito mula sa Module:etymology 178065 Scribunto text/plain local export = {} local force_cat = false -- for debugging local M = require("Module:module loader").init({ require = { str_utils = "Module:string utilities", utilities = "Module:utilities", links = "Module:links", headword = "Module:headword", languages = "Module:languages", parameters = "Module:parameters", inflection_table = "Module:inflection-table", }, loadData = { radicals = "Module:roots/data/radicals", }, lazy = { concat = function() return table.concat end, insert = function() return table.insert end, }, }) local UI_STRINGS = { no_langcode = "Please provide a language code.", invalid_langcode = "Invalid language code: %s", not_a_table = "rootTable is not a table", min_radicals = "Root must have at least %s %s.", max_radicals = "Root must have at most %s %s.", unrecognized_pos = "Unrecognized %s radical %s in %s", unrecognized_mem = "Unrecognized radical %s in %s", not_single_letter = "'%s' in the root '%s' should be a single letter.", invalid_validation = "Unknown validation model: %s", only_one_root = "There should be only one root.", bad_root = "Invalid %s root '%s': %s", wrong_separator = "Radicals should be separated by '%s'.", root_vs_pagename = "Root is different from the pagename.", move_to_appendix = "The root page should be in the Appendix namespace. Please move it to : [[%s]]", appendix_subpage = "Appendix:%s roots/", root_category = "%s terms belonging to the root %s%s", derived_category = "%s terms derived from the %s root %s%s", derived_language = "%s na salitang hinango mula sa %s", --TLCHANGE root_heading = "\n! [[w:%s|%s]]", sense_wrapper = "&lrm; <span class=\"gloss-content\">(%s)</span>", article_semitic = "Semitic root", article_generic = "Root (linguistics)", } local DEFAULT_LINK_TARGET = "bare" local DEFAULT_HEADWORD = {} local function split_root(root, separator) if not separator then return { root } end return mw.text.split(root, separator, true) end local function radicals_word(count) return count == 1 and "radical" or "radicals" end function export.get_data_module_name(langcode) return "Module:roots/data/" .. langcode end local function try_load_data(langcode) local ok, data = pcall(mw.loadData, export.get_data_module_name(langcode)) if ok then return data end return nil end function export.get_data(langcode) return try_load_data(langcode) end local function get_lang_and_data(frame) local code = frame.args["lang"] if not code or code == "" then error(UI_STRINGS.no_langcode) end local lang = M.languages.getByCode(code) if not lang then error(UI_STRINGS.invalid_langcode:format(code)) end return lang, try_load_data(code) or {} end local function resolve_radicals(data) if data.validation == "positional" or data.radicals then return data.radicals end if data.shared_radicals then return M.radicals[data.shared_radicals] end return nil end local function transliterate_root(root_string, romanization) if not romanization then return nil end local out = root_string for _, pair in ipairs(romanization) do out = M.str_utils.gsub(out, pair[1], pair[2]) end return out end export.transliterate = transliterate_root local function validate_root(rootTable, joined_root, data, radical_set) if type(rootTable) ~= "table" then return nil, UI_STRINGS.not_a_table end local len = #rootTable if data.min_radicals and len < data.min_radicals then return nil, UI_STRINGS.min_radicals:format(data.min_radicals, radicals_word(data.min_radicals)) end if data.max_radicals and len > data.max_radicals then return nil, UI_STRINGS.max_radicals:format(data.max_radicals, radicals_word(data.max_radicals)) end if data.validation == "positional" then for i, radical in ipairs(rootTable) do local set, label if i == 1 then set, label = radical_set.initial, "initial" elseif i == len then set, label = radical_set.final, "final" else set, label = radical_set.medial, "medial" end if not set[radical] then return nil, UI_STRINGS.unrecognized_pos:format(label, radical, joined_root) end end elseif data.validation == "membership" then for _, radical in ipairs(rootTable) do if not radical_set[radical] then return nil, UI_STRINGS.unrecognized_mem:format(radical, joined_root) end end elseif data.validation == "single_letter" then for _, radical in ipairs(rootTable) do if mw.ustring.len(radical) ~= 1 then return nil, UI_STRINGS.not_single_letter:format(radical, joined_root) end end elseif data.validation ~= nil then return nil, UI_STRINGS.invalid_validation:format(tostring(data.validation)) end return true end local function link(term, alt, translit, id, lang) if term == "" or term == "&mdash;" then return term end return M.links.full_link({ term = term, alt = alt ~= term and alt or nil, tr = translit, lang = lang, id = id, }) end local function root_category(lang, source, root, qualifier) local suffix = (qualifier and qualifier ~= "") and (" (" .. qualifier .. ")") or "" if lang:getCode() == source:getCode() then return UI_STRINGS.root_category:format(lang:getFullName(), root, suffix) end return UI_STRINGS.derived_category:format( lang:getFullName(), source:getCanonicalName(), root, suffix) end local function check_root(source, root) local data = try_load_data(source:getCode()) if not data then return true end local bare = (root:gsub("^%*", "")) local radicals = split_root(bare, data.separator) if data.separator and #radicals == 1 and mw.ustring.len(bare) > 1 then return nil, UI_STRINGS.wrong_separator:format(data.separator) end return validate_root(radicals, bare, data, resolve_radicals(data)) end function export.assert_root(source, root) local ok, err = check_root(source, root) if not ok then error(UI_STRINGS.bad_root:format(source:getCanonicalName(), root, err)) end return true end function export.get_categories(lang, source, roots, ids) local categories = {} ids = ids or {} if lang:getCode() ~= source:getCode() then M.insert(categories, UI_STRINGS.derived_language:format(lang:getFullName(), source:getCanonicalName())) end for index, root in ipairs(roots) do M.insert(categories, root_category(lang, source, root, ids[index])) export.assert_root(source, root) end return categories end function export.categorize(lang, source, roots, ids, sort_key) return M.utilities.format_categories( export.get_categories(lang, source, roots, ids), lang, sort_key, nil, force_cat) end function export.show(frame) local params = { [1] = { required = true, type = "language" }, [2] = { required = true, type = "language" }, [3] = { required = true, list = true }, ["id"] = { list = true, allow_holes = true }, ["sort"] = {}, } local args = M.parameters.process(frame:getParent().args, params) return export.categorize(args[1], args[2], args[3], args.id, args.sort) end function export.root(frame) local lang, data = get_lang_and_data(frame) local radical_set = resolve_radicals(data) local link_target = data.link_target or DEFAULT_LINK_TARGET local output = {} local categories = {} local title = mw.title.getCurrentTitle() local namespace = title.nsText local params = { [1] = { list = true }, ["nocat"] = { type = "boolean" }, ["plain"] = { type = "boolean" }, ["notext"] = { type = "boolean" }, ["sense"] = {}, } if link_target == "bare" then params["head"] = true params["pagename"] = true end local args = M.parameters.process(frame:getParent().args, params) local roots = args[1] local roots_len = #roots local plain = args["plain"] if frame.args["plain"] then plain = true end local separator = data.separator local identity local subpage = "" if link_target == "appendix_subpage" then subpage = UI_STRINGS.appendix_subpage:format(lang:getCanonicalName()) local fulltitle = title.fullText fulltitle = M.str_utils.gsub(fulltitle, M.str_utils.gsub(subpage, "([^%w])", "%%%1"), "") identity = fulltitle else identity = args["pagename"] or mw.loadData("Module:headword/data").pagename end local rootLetters = {} if roots_len == 0 and not args["pagename"] and namespace == "Template" and data.preview_root then M.insert(rootLetters, split_root(data.preview_root, separator)) elseif roots_len ~= 0 then for _, root in ipairs(roots) do M.insert(rootLetters, split_root(root, separator)) end else if link_target == "bare" then local head = frame.args["head"] if args["head"] ~= nil and identity ~= lang:stripDiacritics(args["head"]) then error(UI_STRINGS.root_vs_pagename) end if head and args["head"] ~= nil then identity = args["head"] end end M.insert(rootLetters, split_root(identity, separator)) end local joined_roots = {} for i, rootLetter in ipairs(rootLetters) do M.insert(joined_roots, M.concat(rootLetter, separator)) local ok, err = validate_root(rootLetter, joined_roots[i], data, radical_set) if not ok then error(err) end end local sense = args["sense"] local sense_formatted = "" if sense ~= nil and sense ~= "" then sense_formatted = UI_STRINGS.sense_wrapper:format(sense) end if identity == joined_roots[1] then if link_target == "appendix_subpage" and namespace == "" then error(UI_STRINGS.move_to_appendix:format(subpage .. joined_roots[1])) end if roots_len > 1 then error(UI_STRINGS.only_one_root) end local headword_args = { lang = lang, pos_category = "roots", categories = {}, heads = { identity }, nomultiwordcat = true, } local headword_opts = data.headword or DEFAULT_HEADWORD if headword_opts.translits then headword_args.translits = { transliterate_root(identity, data.romanization) } end if headword_opts.noposcat then headword_args.noposcat = true elseif headword_opts.nolemmacat then headword_args.nolemmacat = true end M.insert(output, M.headword.full_headword(headword_args)) if args["nocat"] then return M.concat(output) end return M.concat(output) .. M.concat(categories) else local link_texts = {} local term_counts = {} for i, joined_root in ipairs(joined_roots) do local link_text if link_target == "appendix_subpage" then link_text = subpage .. joined_root else link_text = joined_root end local translit = data.romanization and transliterate_root(joined_root, data.romanization) or nil M.insert(link_texts, link(link_text, joined_root, translit, sense, lang) .. sense_formatted) M.insert(categories, M.utilities.format_categories( { root_category(lang, lang, joined_root, sense) }, lang, nil, nil, force_cat)) M.insert(term_counts, mw.site.stats.pagesInCategory( root_category(lang, lang, joined_root, sense), "pages")) end if args["nocat"] or plain then if args["nocat"] then return M.concat(link_texts, ", ") end return M.concat(link_texts, ", ") .. M.concat(categories) else local link_text_output = "" for i, link_text in ipairs(link_texts) do link_text_output = link_text_output .. "\n|-\n| " .. link_text .. "\n|-\n| [[:Category:" .. root_category(lang, lang, joined_roots[i], sense) .. "|" .. term_counts[i] .. " term" .. (term_counts[i] == 1 and "" or "s") .. "]]\n" end local wikicode = M.inflection_table.make_top{ title = "-", palette = data.palette, class = "floatright tr-alongside" } local article = lang:inFamily("sem") and UI_STRINGS.article_semitic or UI_STRINGS.article_generic wikicode = wikicode .. UI_STRINGS.root_heading:format(article, #term_counts == 1 and "Root" or "Roots") wikicode = wikicode .. link_text_output wikicode = wikicode .. M.inflection_table.make_bottom{} return wikicode .. M.concat(categories) end end end return export jjh9h1x20g8mu7flof7rveo4rcgmbhq Module:etymon 828 38867 178067 2026-09-25T00:39:15Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 copied over (oldid 92736184) 178067 Scribunto text/plain --[=[ This module implements the {{etymon}} template for structured etymology data on Wiktionary. It enables the creation of etymology trees and text by parsing etymon chains, scraping linked pages for their own {{etymon}} data, and recursively building a tree of derivational relationships. Authors: - Original implementation: [[User:Ioaxxere]] - Full refactor (September 2025): [[User:Fenakhay]] ([[Special:Diff/86717746]]) Modules: - [[Module:etymon]]: main module handling parsing, validation, tree building, and page scraping - [[Module:etymon/data]]: keyword definitions, configuration, and status constants - [[Module:etymon/tree]]: etymology tree rendering - [[Module:etymon/text]]: etymology text generation - [[Module:etymon/categories]]: category generation logic - [[Module:etymon/tracking]]: tracking ]=] local export = {} local __state = { cached_etymon_args = {}, cached_etymon_pages = {}, cached_descendants_checks = {}, senseid_parent_etymon = {}, available_etymon_ids = {}, single_etymons = {}, entry_title = nil, entry_lang_code = nil, current_page_has_inline_etymology = false, current_page_has_redundant_etymology = false, used_idless_etymon = false, toplevel_has_inline_etymology = false, toplevel_redundant_etymology = false, toplevel_idless_etymon = false, has_mismatched_id = false, linked_page_multiple_etymons_idless = false, linked_page_partial_etymology_sections = false, partial_etymology_targets = {}, skip_partial_etymology_category = false, max_depth_reached = 0, total_nodes = 0, language_count = {}, toplevel_keyword_stats = {}, id_stats = nil, warnings = {}, } local function reset_invocation_state() __state.current_page_has_inline_etymology = false __state.current_page_has_redundant_etymology = false __state.used_idless_etymon = false __state.toplevel_has_inline_etymology = false __state.toplevel_redundant_etymology = false __state.toplevel_idless_etymon = false __state.has_mismatched_id = false __state.linked_page_multiple_etymons_idless = false __state.linked_page_partial_etymology_sections = false __state.max_depth_reached = 0 __state.total_nodes = 0 __state.language_count = {} __state.toplevel_keyword_stats = {} __state.warnings = {} end local M = require("Module:module loader").init({ require = { data = "Module:etymon/data", tree = "Module:etymon/tree", text = "Module:etymon/text", categories = "Module:etymon/categories", tracking = "Module:etymon/tracking", descendants = "Module:etymon/descendants", anchors = "Module:anchors", etydate = "Module:etydate", etymology = "Module:etymology", families = "Module:families", languages = "Module:languages", languages_errorgetby = "Module:languages/errorGetBy", links = "Module:links", pages = "Module:pages", parameters = "Module:parameters", string_utilities = "Module:string utilities", template_parser = "Module:template parser", utilities = "Module:utilities", debug = "Module:debug", en_utilities = "Module:en-utilities", parameter_utilities = "Module:parameter utilities", parse_utilities = "Module:parse utilities", template_styles = "Module:TemplateStyles", script_utilities = "Module:script utilities", JSON = "Module:JSON", yesno = "Module:yesno", }, loadData = { headword_data = "Module:headword/data", parameters_data = "Module:parameters/data", text_allowed = "Module:etymon/data/text_allowed", }, }) local Util = {} function Util.format_error(message, preview_only) if preview_only and not M.pages.is_preview() then return nil end return '<span class="error">' .. message .. '</span>' end function Util.add_warning(message, preview_only) local formatted = Util.format_error(message, preview_only) if formatted then table.insert(__state.warnings, formatted) end end function Util.is_text_param_allowed_for_lang(lang) if not lang or type(lang) ~= "table" then return false end local types = lang.getTypes and lang:getTypes() if types and types.family then local code = lang.getCode and lang:getCode() return code and M.text_allowed.families[code] == true end local full_code = lang.getFullCode and lang:getFullCode() if full_code and M.text_allowed.langs[full_code] then return true end if lang.inFamily then for family_code in pairs(M.text_allowed.families) do if lang:inFamily(family_code) then return true end end end return false end function Util.get_lang(code, no_error) if no_error then return M.languages.getByCode(code, nil, true) end return M.languages.getByCode(code, nil, true) or M.languages_errorgetby.code(code, true, true) end -- Match a term language against a text=:lang stop target (supports etymology-only codes). function Util.lang_matches_stop_code(term_lang, stop_code) if not term_lang or not stop_code or stop_code == "" then return false end local stop_lang = Util.get_lang(stop_code, true) if not stop_lang then return false end if term_lang:getCode() == stop_lang:getCode() then return true end if stop_lang:getFullCode() == stop_lang:getCode() then return term_lang:getFullCode() == stop_lang:getCode() end return false end function Util.get_family(code) return M.families.getByCode(code) end function Util.get_lang_exception(lang) -- Families have no language-specific exceptions if lang.getTypes and lang:getTypes().family then return nil end local code = lang:getCode() local lang_exceptions = M.data.config.lang_exceptions if lang_exceptions[code] then return lang_exceptions[code] end for norm_code, exc in pairs(lang_exceptions) do if exc.normalize_to and code == exc.normalize_to then return exc end if exc.normalize_from_families then local should_normalize = false for _, family in ipairs(exc.normalize_from_families) do if lang:inFamily(family) then should_normalize = true break end end if should_normalize and exc.normalize_exclude_families then for _, family in ipairs(exc.normalize_exclude_families) do if lang:inFamily(family) then should_normalize = false break end end end if should_normalize then local ret = {} for k, v in pairs(exc) do ret[k] = v end ret.suppress_tr = nil return ret end end end return nil end function Util.get_norm_lang(lang) local exc = Util.get_lang_exception(lang) if exc and exc.normalize_to then return M.languages.getByCode(exc.normalize_to) end return lang end function Util.resolve_context_lang(lang, node_args) if type(node_args) ~= "table" then return lang end if node_args.status == M.data.STATUS.INLINE then return lang end if not (lang.hasType and lang:hasType("etymology-only")) then return lang end local full = lang.getFull and lang:getFull() if not full or full:getCode() == lang:getCode() then return lang end if full.hasAncestor and full:hasAncestor(lang) then return lang end return full end -- Add default values for boolean modifiers (e.g., <unc> becomes <unc:1>) -- This is needed because Module:parse utilities expects boolean modifiers to have explicit values function Util.add_boolean_defaults(str, param_mods) local result = str for name, spec in pairs(param_mods) do if spec.type == "boolean" then -- Replace <name> with <name:1> (but not <name:...> which already has a value) result = result:gsub("<" .. name .. ">", "<" .. name .. ":1>") end end return result end local REQUEST_TEMPLATE_PARAM_MODS = { rfe = M.parameter_utilities.construct_param_mods { { param = { "sort", "y", "m", "fragment", "section" } }, { param = { "nocat", "box", "noes" }, type = "boolean" }, }, etystub = M.parameter_utilities.construct_param_mods { { param = "sort" }, { param = { "nocat", "nocap", "nodot" }, type = "boolean" }, }, } function Util.expand_request_template(frame, template_name, param_value, lang_code) local param_mods = REQUEST_TEMPLATE_PARAM_MODS[template_name] local with_defaults = Util.add_boolean_defaults(param_value, param_mods) local parsed = M.parse_utilities.parse_inline_modifiers(with_defaults, { param_mods = param_mods, generate_obj = function(text) if M.yesno(text, false) then return { is_boolean = true } end return { text = text } end, }) local template_args = { [1] = lang_code } for name in pairs(param_mods) do template_args[name] = parsed[name] end if not parsed.is_boolean then template_args[2] = parsed.text end return " " .. frame:expandTemplate({ title = template_name, args = template_args, }) end -- Centralized term formatting: handles suppress_term (-), unknown_term (empty/+), and regular terms function Util.format_term(term, is_toplevel, opts) opts = opts or {} -- suppress_term (-) returns nil if term.suppress_term then return nil end local lang = term.lang local exc = Util.get_lang_exception(lang) if is_toplevel then local display_text = term.alt or term.title or "" local sc = term.sc or lang:findBestScript(display_text) local bold_text = tostring(mw.html.create("strong") :addClass("selflink") :wikitext(display_text)) return M.script_utilities.tag_text(bold_text, lang, sc, "term") end local link_params = { lang = lang } link_params.term = not term.unknown_term and term.title or nil link_params.alt = term.alt link_params.id = (not term.unknown_term and term.id and term.id ~= "") and term.id or nil if not (exc and exc.suppress_tr) then link_params.tr = term.tr link_params.ts = term.ts else link_params.suppress_tr = true end link_params.lit = (opts.lit ~= "suppress") and term.lit or nil if opts.gloss ~= "suppress" then link_params.gloss = term.gloss end link_params.genders = term.genders if opts.pos ~= "suppress" then link_params.pos = term.pos link_params.ng = term.ng link_params.infl = term.infl end if exc and exc.suppress_tr then link_params.lit = nil end if opts.tree_ql ~= "suppress" then if term.q then link_params.q = term.q end if term.qq then link_params.qq = term.qq end if term.l then link_params.l = term.l end if term.ll then link_params.ll = term.ll end link_params.show_decorations = term.q or term.qq or term.l or term.ll end return M.links.full_link(link_params, "term") end local __is_content_page_cached function Util.is_content_page() if __is_content_page_cached == nil then __is_content_page_cached = M.pages.is_content_page(mw.title.getCurrentTitle()) end return __is_content_page_cached end local __page_data_cached function Util.get_page_data() if not __page_data_cached then __page_data_cached = M.headword_data.page end return __page_data_cached end -- Extract base keyword from param (without modifiers) local function get_keyword_base(param) if type(param) ~= "string" then return nil end local base = param:match("^:?([^<]+)") or param:gsub("^:", "") return base end local function is_keyword(param, allow_colon_less) if type(param) ~= "string" then return false end local keywords = M.data.keywords if param:sub(1, 1) == ":" then local base = get_keyword_base(param) return keywords[base] ~= nil end if allow_colon_less then local base = get_keyword_base(param) return keywords[base] ~= nil end return false end local function get_keyword(param, allow_colon_less) if type(param) ~= "string" then return nil end local keywords = M.data.keywords if param:sub(1, 1) == ":" then return get_keyword_base(param) end if allow_colon_less then local base = get_keyword_base(param) if keywords[base] then return base end end return nil end local function normalize_keyword(keyword) if keyword:sub(1, 1) == ":" then return keyword end return ":" .. keyword end -- Resolve keyword (possibly an alias) to its canonical form. Used only at input boundaries local function get_canonical_keyword(keyword) if not keyword then return keyword end return M.data.keyword_canonical[keyword] or keyword end local function is_affix_group_keyword(keyword) local config = keyword and M.data.keywords[keyword] return config and config.affix_categories or false end local function reject_removed_surf_keyword(param) local base = get_keyword_base(param) if base == "surf" then error("The `:surf` keyword has been removed. Use `<surf>` on a formation keyword instead (e.g. `:af<surf>`, `:bor<surf>`).") end end local function copy_keyword_info(source) local copy = {} for k, v in pairs(source) do copy[k] = v end return copy end local function lowercase_glossary_display(text) return text:gsub("(%[%[Appendix:Glossary#[^|]+|)([^%]])([^%]]*)%]%]", function(prefix, first, rest) return prefix .. mw.ustring.lower(first) .. rest .. "]]" end) end local function surf_should_keep_formation_phrase(base) if not base.phrase then return false end if base.glossary then return true end return not (base.phrase == "from" and (base.text == "From" or base.text == "from")) end -- Runtime overrides when <surf> is present on a keyword. local function get_effective_keyword_info(keyword, modifiers) local base = M.data.keywords[keyword] if not base or not modifiers or not modifiers.surf then return base end local effective = copy_keyword_info(base) local surf_text = "By [[Appendix:Glossary#surface_analysis|surface analysis]]," local surf_phrase = "by surface analysis," effective.new_sentence = true effective.invisible = "tree" if surf_should_keep_formation_phrase(base) then effective.phrase = surf_phrase .. " " .. base.phrase if base.text then effective.text = surf_text .. " " .. lowercase_glossary_display(base.text) else effective.text = surf_text .. " " .. base.phrase end else effective.text = surf_text effective.phrase = surf_phrase end return effective end -- Build text/phrase for nominalization with <g:code> (uses data module for codes only). local function get_nominalization_label_for_g(code) if not code or code == "" then return nil end local codes = M.data.nominalization_g_codes local adj = codes[code] if not adj and #code == 2 then local gender_adj = codes[code:sub(1, 1)] local number_adj = codes[code:sub(2, 2)] if gender_adj and number_adj then adj = gender_adj .. " " .. number_adj end end if not adj then return nil end local text = adj:gsub("^%l", function(c) return string.upper(c) end) .. " [[Appendix:Glossary#nominalization|nominalization]] of" local phrase = M.en_utilities.add_indefinite_article(adj .. " [[Appendix:Glossary#nominalization|nominalization]] of", false) return { text = text, phrase = phrase } end local EtymonParser = {} -- Keyword modifier definitions EtymonParser.keyword_param_mods = M.parameter_utilities.construct_param_mods { { group = "ref" }, { param = "conj" }, -- conjunction for alternatives: "and", "or", "and/or", etc. { param = { "unc", "surf" }, type = "boolean" }, { param = "text", restrict = { keywords = { "from", "derived" } } }, { param = "lit", restrict = { affix_group = true } }, { param = "g", restrict = { keywords = { "nominalization" } } }, { param = "senseid", restrict = { keywords = { "semantic loan" } } }, } -- Term modifier definitions EtymonParser.etymon_param_mods = M.parameter_utilities.construct_param_mods { {group = {"link", "q", "l", "ref", "infl"}, exclude = {"sc"}}, {param = {"ety", "postype"}}, {param = "unc", type = "boolean"}, {param = "aftype", restrict = {affix_group = true}}, {param = {"bor", "slbor", "lbor"}, type = "boolean", restrict = {affix_group = true}}, } local function get_clean_param_mods(param_mods) local clean = {} for mod_name, mod_def in pairs(param_mods) do clean[mod_name] = {} for key, value in pairs(mod_def) do if key ~= "restrict" then clean[mod_name][key] = value end end end return clean end function EtymonParser.check_modifier_restrictions(modifiers, current_keyword, param_mods) for mod_name, mod_value in pairs(modifiers) do -- Only check restrictions if the modifier has a non-false/nil value if mod_value then local mod_def = param_mods[mod_name] if mod_def and mod_def.restrict then if mod_def.restrict.affix_group then if not is_affix_group_keyword(current_keyword) then local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">" error("The modifier `" .. mod_display .. "` is only allowed for affix-group keywords (e.g. `:af`, `:blend`, `:univ`).") end elseif mod_def.restrict.keywords then local allowed_keywords = mod_def.restrict.keywords local is_allowed = false for _, allowed_keyword in ipairs(allowed_keywords) do if current_keyword == allowed_keyword then is_allowed = true break end end if not is_allowed then local keyword_list = {} for _, kw in ipairs(allowed_keywords) do table.insert(keyword_list, ":" .. kw) end local keyword_str = table.concat(keyword_list, #keyword_list == 2 and " or " or ", ") if #keyword_list > 2 then -- Replace last comma with "or" keyword_str = keyword_str:gsub(", ([^,]+)$", " or %1") end local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">" error("The modifier `" .. mod_display .. "` is only allowed for the keyword" .. (#keyword_list > 1 and "s " or " ") .. keyword_str .. ".") end end end end end end local TERM_RULE_DISALLOW = { suppress = { field = "suppress_term", label = "suppressed" }, unknown = { field = "unknown_term", label = "unknown" }, family = { field = "is_family", label = "family" }, } function EtymonParser.check_etymon_limits(count, limits, label, opts) if not limits then return end opts = opts or {} local min_etymons = limits.min_etymons if min_etymons == nil and not opts.skip_default_min then min_etymons = 1 end if min_etymons and count < min_etymons then if min_etymons > 1 then error("Detected " .. label .. " group with fewer than " .. min_etymons .. " etymons.") else error("Detected " .. label .. " with no etymons.") end end if limits.max_etymons and count > limits.max_etymons then local unit = (limits.max_etymons == 1) and "etymon" or "etymons" error("Detected " .. label .. " with more than " .. limits.max_etymons .. " " .. unit .. ".") end end function EtymonParser.check_term_rules(etymon_data, entry_lang, rules, label) label = label or "term" if rules and rules.disallow then local disallowed = {} for _, typ in ipairs(rules.disallow) do local spec = TERM_RULE_DISALLOW[typ] if spec and etymon_data[spec.field] then table.insert(disallowed, spec.label) end end if #disallowed > 0 then error(label .. " does not support " .. mw.text.listToText(disallowed, "or") .. " etymons.") end end if etymon_data.is_family then if rules and rules.family == "disallowed" then error(label .. " does not support family codes" .. (rules.family_suffix or ".")) elseif not etymon_data.suppress_term then error("Family codes require suppressed term (use family:-).") end end if rules then if rules.require_term and (not etymon_data.term or etymon_data.term == "") then error(label .. " requires a term for each listed form.") end if rules.entry_lang then if Util.get_norm_lang(etymon_data.lang):getFullCode() ~= Util.get_norm_lang(entry_lang):getFullCode() then error(label .. " terms must be in the entry language (" .. entry_lang:getFullCode() .. "), got '" .. etymon_data.lang:getFullCode() .. "'.") end end if rules.ancestor_check then M.etymology.check_ancestor(entry_lang, etymon_data.lang) end elseif etymon_data.is_family and not etymon_data.suppress_term then error("Family codes require suppressed term (use family:-).") end end function EtymonParser.check_keyword_term(etymon_data, entry_lang, keyword) local config = M.data.keywords[keyword] EtymonParser.check_term_rules(etymon_data, entry_lang, config and config.term_rules, "`:" .. keyword .. "`") end function EtymonParser.check_supplement_term(etymon_data, entry_lang, supplement_type) local config = M.data.supplements[supplement_type] EtymonParser.check_term_rules(etymon_data, entry_lang, config and config.term_rules, "|" .. supplement_type .. "=") end -- Parse keyword with modifiers (e.g., ":bor<unc>" or ":bor<ref:{{R:example}}>") function EtymonParser.parse_keyword_modifiers(param) if type(param) ~= "string" then return nil, {} end local base_keyword = get_keyword_base(param) if not base_keyword then return nil, {} end local canonical_keyword = get_canonical_keyword(base_keyword) -- Check if there are any modifiers if not param:find("<", 1, true) then return canonical_keyword, {} end -- Parse modifiers using the same mechanism as etymon parsing local rest_with_defaults = Util.add_boolean_defaults(param, EtymonParser.keyword_param_mods) local function generate_obj(ignored) return {} end local parsed = M.parse_utilities.parse_inline_modifiers(rest_with_defaults:gsub("^:?[^<]+", ""), { param_mods = get_clean_param_mods(EtymonParser.keyword_param_mods), generate_obj = generate_obj }) local modifiers = { unc = parsed.unc or false, refs = parsed.refs, text = parsed.text, lit = parsed.lit, conj = parsed.conj, g = parsed.g, surf = parsed.surf or false, senseid = parsed.senseid, } -- Validate modifiers against restrictions EtymonParser.check_modifier_restrictions(modifiers, canonical_keyword, EtymonParser.keyword_param_mods) return canonical_keyword, modifiers end local function normalize_keyword_param(keyword_with_mods) local trimmed = M.string_utilities.trim(keyword_with_mods) reject_removed_surf_keyword(trimmed:match("^:") and trimmed or (":" .. trimmed)) local base = get_keyword_base(trimmed) if not base or not M.data.keywords[base] then error("Invalid keyword '" .. trimmed .. "' in inline etymology") end local canonical_base = get_canonical_keyword(base) local without_colon = trimmed:gsub("^:", "") local mods_part = without_colon:sub(#base + 1) local kw_param = normalize_keyword(canonical_base .. mods_part) EtymonParser.parse_keyword_modifiers(kw_param) return kw_param end local function get_keyword_mod_names() local names = {} for mod_name in pairs(EtymonParser.keyword_param_mods) do names[mod_name] = true end return names end local function parse_inline_ety_run(ety_string) local body = ety_string or "" if body == "" then error("Empty inline etymology") end local keyword_mod_names = get_keyword_mod_names() local pos = 1 local len = #body local function parse_err(msg) error(msg .. " in inline etymology: '" .. body .. "'") end local function peek_double() return body:sub(pos, pos + 1) == "<<" end local function mod_name_from_unwrapped(unwrapped) return unwrapped:match("^<([^:>]+)") end local function is_keyword_mod(unwrapped) local name = mod_name_from_unwrapped(unwrapped) return name and keyword_mod_names[name] or false end local function read_double_bracket() if not peek_double() then return nil end local start = pos pos = pos + 2 while pos <= len - 1 do if body:sub(pos, pos + 1) == ">>" then local token = body:sub(start, pos + 1) pos = pos + 2 return token, token:sub(2, -2) end pos = pos + 1 end parse_err("Unmatched <<") end local function read_angle_cell() if body:sub(pos, pos) ~= "<" or peek_double() then return nil end local open = pos pos = pos + 1 local depth = 1 local i = pos while i <= len do local ch = body:sub(i, i) if ch == "<" then depth = depth + 1 elseif ch == ">" then depth = depth - 1 if depth == 0 then local inner = body:sub(open + 1, i - 1) pos = i + 1 return inner end end i = i + 1 end parse_err("Unmatched <") end local function read_bare_run() local start = pos while pos <= len and body:sub(pos, pos) ~= "<" do pos = pos + 1 end return body:sub(start, pos - 1) end local function absorb_double_keyword_mods(keyword_str) while peek_double() do local saved = pos local _, unwrapped = read_double_bracket() if is_keyword_mod(unwrapped) then keyword_str = keyword_str .. unwrapped else pos = saved break end end return keyword_str end local kw_start = pos while pos <= len and body:sub(pos, pos) ~= "<" do pos = pos + 1 end local keyword = body:sub(kw_start, pos - 1) if keyword:match("^%s*$") then parse_err("Missing keyword") end keyword = absorb_double_keyword_mods(keyword) local cells = {} while pos <= len do if peek_double() then local _, unwrapped = read_double_bracket() if is_keyword_mod(unwrapped) then parse_err("Unexpected keyword modifier " .. unwrapped .. " outside of a keyword") end table.insert(cells, "+" .. unwrapped) elseif body:sub(pos, pos) == "<" then local inner = read_angle_cell() if inner ~= "" then table.insert(cells, inner) end else local bare = read_bare_run() if bare ~= "" then if bare:sub(1, 1) ~= ":" then parse_err("Unexpected bare text '" .. bare .. "' (use :keyword for nested keywords in inline etymology)") end if not is_keyword(bare, true) then parse_err("Invalid keyword '" .. bare .. "' in inline etymology") end table.insert(cells, absorb_double_keyword_mods(bare)) end end end return { keyword = keyword, cells = cells, } end function EtymonParser.inline_ety_to_pipe(ety_string) local run = parse_inline_ety_run(ety_string) if not run.keyword or run.keyword:match("^%s*$") then return "|" end local pipe_parts = { normalize_keyword_param(M.string_utilities.trim(run.keyword)) } for _, segment in ipairs(run.cells) do if is_keyword(segment, true) then table.insert(pipe_parts, normalize_keyword_param(segment)) else table.insert(pipe_parts, segment) end end return "|" .. table.concat(pipe_parts, "|") .. "|" end function EtymonParser.pipe_to_inline_ety(pipe_string) local cells = {} for cell in pipe_string:gmatch("([^|]+)") do if cell ~= "" then table.insert(cells, cell) end end if #cells == 0 then return "" end local inline_parts = {} for index, cell in ipairs(cells) do local base = get_keyword_base(cell) if base and M.data.keywords[base] then local without_colon = cell:gsub("^:", "") local kw_base, mods = without_colon:match("^([^<]+)(.*)$") local inline_kw = (kw_base or without_colon) .. (mods or ""):gsub("<([^>]+)>", "<<%1>>") if index > 1 then inline_kw = ":" .. inline_kw end table.insert(inline_parts, inline_kw) elseif cell:sub(1, 1) == "+" then local mod = cell:sub(2) if mod:match("^<.->$") then mod = mod:sub(2, -2) end table.insert(inline_parts, "<<" .. mod .. ">>") else table.insert(inline_parts, "<" .. cell .. ">") end end return table.concat(inline_parts, "") end function EtymonParser.parse_inline_ety(ety_string, context_lang) local run = parse_inline_ety_run(ety_string) local keyword = M.string_utilities.trim(run.keyword) reject_removed_surf_keyword(":" .. keyword) if not is_keyword(keyword, true) then error("Invalid keyword '" .. keyword .. "' in inline etymology <ety:" .. keyword .. "...>") end local args = { context_lang:getCode(), normalize_keyword_param(keyword) } for _, segment in ipairs(run.cells) do if is_keyword(segment, true) then table.insert(args, normalize_keyword_param(segment)) else table.insert(args, segment) end end return args end function EtymonParser.parse_etymon(param, context_lang) if is_keyword(param) then return nil end if type(param) ~= "string" then return nil end local lang, rest local is_family = false local before_bracket = param:match("^([^<]*)") or param local lang_code, rest_match = before_bracket:match("^([a-zA-Z][a-zA-Z0-9._-]*):(.*)$") if lang_code then local potential_lang = Util.get_lang(lang_code, true) if potential_lang then lang = potential_lang rest = param:sub(#lang_code + 2) else local potential_family = Util.get_family(lang_code) if potential_family then lang = potential_family rest = param:sub(#lang_code + 2) is_family = true else lang = context_lang rest = param end end else lang = context_lang rest = param end M.tracking.track_term(rest) if rest == "" or rest == "+" then return { lang = lang, term = nil, unknown_term = true, is_family = is_family, } end if rest == "-" then return { lang = lang, term = nil, suppress_term = true, is_family = is_family, } end if not rest:find("<", 1, true) then return { lang = lang, term = M.string_utilities.trim(rest), is_family = is_family, } end local term_text = rest:match("^([^<]*)") or "" local is_unknown = (term_text == "" or term_text == "+") local is_suppress = (term_text == "-") local function generate_obj(ignored_term) return { term = (is_unknown or is_suppress) and nil or M.string_utilities.trim(term_text) } end local rest_with_defaults = Util.add_boolean_defaults(rest, EtymonParser.etymon_param_mods) local parsed_obj = M.parse_utilities.parse_inline_modifiers(rest_with_defaults, { param_mods = get_clean_param_mods(EtymonParser.etymon_param_mods), generate_obj = generate_obj }) if parsed_obj.id and parsed_obj.id:match("^!") then parsed_obj.id = parsed_obj.id:sub(2) parsed_obj.override = true end parsed_obj.lang = lang parsed_obj.is_family = is_family if is_unknown then parsed_obj.unknown_term = true elseif is_suppress then parsed_obj.suppress_term = true end return parsed_obj end function EtymonParser.validate(lang, args, id, title, pos, starts_with_lang_code) -- id is now optional, so only validate if provided if id then if mw.ustring.len(id) < 2 then error("The `id` parameter must have at least two characters.") end if id == title or id == Util.get_page_data().pagename then error("The `id` parameter must not be the same as the page title.") end end local valid_pos = { prefix = true, suffix = true, interfix = true, infix = true, root = true, word = true } if pos and not valid_pos[pos] then error("Unknown value provided for `pos`. Valid values: " .. table.concat(require("Module:table").keysToList(valid_pos), ", ") .. ".") end local current_keyword = "from" local current_keyword_explicit = false local keyword_etymons = {} local keywords = M.data.keywords local function checkKeyword() local config = keywords[current_keyword] if current_keyword == "from" and not current_keyword_explicit and #keyword_etymons == 0 then keyword_etymons = {} return end EtymonParser.check_etymon_limits(#keyword_etymons, config, "`:" .. current_keyword .. "`") keyword_etymons = {} end local start_index = starts_with_lang_code and 2 or 1 for i = start_index, #args do local param = args[i] if type(param) ~= "string" then elseif param:sub(1, 1) == ":" and not is_keyword(param) then reject_removed_surf_keyword(param) error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?") elseif is_keyword(param) then checkKeyword() current_keyword = get_canonical_keyword(get_keyword(param)) current_keyword_explicit = true else local etymon_data = EtymonParser.parse_etymon(param, lang) if etymon_data then table.insert(keyword_etymons, param) EtymonParser.check_keyword_term(etymon_data, lang, current_keyword) -- Check modifier restrictions EtymonParser.check_modifier_restrictions(etymon_data, current_keyword, EtymonParser.etymon_param_mods) -- postype must be "root" or "word" local VALID_POSTYPES = { root = true, word = true } if etymon_data.postype and not VALID_POSTYPES[etymon_data.postype] then error("Invalid <postype:" .. etymon_data.postype .. ">; must be \"root\" or \"word\".") end if etymon_data.ety then local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang) EtymonParser.validate(etymon_data.lang, inline_args, nil, nil, nil, true) end else table.insert(keyword_etymons, param) end end end checkKeyword() end local DataRetriever = {} local function format_etymon_id_hint(id_data, idx) local id = type(id_data) == "table" and id_data.id or id_data local pos = type(id_data) == "table" and id_data.pos if id and id ~= "" and id ~= "*" then return '"' .. id .. '"' end if pos and pos ~= "" then return "unnamed (|pos=" .. pos .. "|)" end return "etymon #" .. idx .. " (no |id= on page)" end local function etymon_target_page_link(page, norm_lang) return M.links.full_link({ term = page, lang = norm_lang, no_generate_alternants = true, }, "term") end -- Summarize {{etymon}} id slots on a linked page for preview warnings. local function summarize_available_etymon_ids(ids) local id_list = {} local all_idless = true local target_has_idless = false local any_pos = false for i, id_data in ipairs(ids) do local id = type(id_data) == "table" and id_data.id or id_data local pos = type(id_data) == "table" and id_data.pos if id and id ~= "" and id ~= "*" then all_idless = false else target_has_idless = true end if pos and pos ~= "" then any_pos = true end table.insert(id_list, format_etymon_id_hint(id_data, i)) end return { id_list = id_list, all_idless = all_idless, target_has_idless = target_has_idless, any_pos = any_pos, count = #ids, options_text = mw.text.listToText(id_list), } end local function ambiguous_etymon_suggestion(page_link, summary) if summary.all_idless then if summary.any_pos then return " None set `|id=` yet; add a unique `|id=` to each on " .. page_link .. ", then `<id:identifier>` after the term here. Section order / hints: " .. summary.options_text .. "." end return " None set `|id=` yet; add a unique `|id=` to each {{etymon}} in that section from top to bottom, then `<id:identifier>` after the term here (same value as `|id=`)." end return " Specify which one with `<id:identifier>` after the term. Options: " .. summary.options_text .. "." end local function warn_ambiguous_etymon_link(page, norm_lang, ids, is_toplevel) local page_link = etymon_target_page_link(page, norm_lang) local summary = summarize_available_etymon_ids(ids) if is_toplevel and summary.target_has_idless then __state.linked_page_multiple_etymons_idless = true end local lang_name = norm_lang:getCanonicalName() local lead = "Etymology link to " .. page_link .. " is ambiguous (" .. summary.count .. " {{etymon}} templates for " .. lang_name .. ")." Util.add_warning(lead .. ambiguous_etymon_suggestion(page_link, summary), true) end local function is_mismatched_explicit_id(base_key, cached_args, parent_etymon) return cached_args == M.data.STATUS.MISSING and not parent_etymon and #(__state.available_etymon_ids[base_key] or {}) > 0 end local function maybe_flag_partial_etymology_reference(base_key, etymon_data, cached_args, is_toplevel) if not is_toplevel or __state.skip_partial_etymology_category then return end if not __state.partial_etymology_targets[base_key] then return end if etymon_data.id and type(cached_args) == "table" then return end __state.linked_page_partial_etymology_sections = true end local function is_nonlemma_etymon_template(template_args) return template_args and M.yesno(template_args.nl, false) end local function warn_mismatched_explicit_id(page, norm_lang, base_key, etymon_id) local page_link = etymon_target_page_link(page, norm_lang) local summary = summarize_available_etymon_ids(__state.available_etymon_ids[base_key] or {}) local lang_name = norm_lang:getCanonicalName() local lead = "Etymology link to " .. page_link .. " uses `<id:" .. etymon_id .. ">`, but no {{etymon}} on that page has `|id=" .. etymon_id .. "|` for " .. lang_name .. "." Util.add_warning(lead .. " Valid IDs: " .. summary.options_text .. ".", true) end -- Given an etymon data, scrape its page and cache the result in the global state object. function DataRetriever.cache_page_etymons(etymon_page, etymon_title, key, etymon_lang, etymon_id, redirected_from, descendants_is_toplevel) local content = etymon_title:getContent() if not content then __state.cached_etymon_args[key] = M.data.STATUS.REDLINK return end -- Check if the linked page is a redirect. If it is, the template parsing -- code below will be effectively skipped, and `scrape_page` will be called -- again on the redirect target (see the bottom of this function) local lang_section_for_descendants = nil local redirect_target = etymon_title.redirect_target if not redirect_target then content = M.pages.get_section(content, etymon_lang:getFullName(), 2) if not content then __state.cached_etymon_args[key] = M.data.STATUS.MISSING return end lang_section_for_descendants = content end local etymon_lang_code = etymon_lang:getFullCode() local lang_page_key = etymon_lang_code .. ":" .. etymon_page local found_templates_for_lang = {} local found_ids = {} local get_node_class = M.template_parser.class_else_type -- Look for all {{etymon}} templates within the page content using the template parser -- This way the same page is never parsed more than once -- Build a map from senseids to their parent etymonids. local active_etymon_args = nil local etymology_section_count = 0 local etymology_sections_with_etymon = 0 local current_etymology_has_etymon = false local current_etymology_has_nonlemma = false local function finalize_current_etymology_section() if etymology_section_count == 0 then return end if current_etymology_has_etymon or current_etymology_has_nonlemma then etymology_sections_with_etymon = etymology_sections_with_etymon + 1 end current_etymology_has_etymon = false current_etymology_has_nonlemma = false end for node in M.template_parser.parse(content):iterate_nodes() do local node_class = get_node_class(node) if node_class == "heading" then -- A new L2 or etymology section acts as a barrier: an {{etymon}} usage -- used previously cannot be the parent of any subsequent senseids. -- Note that we don't have to check for L2s due to the usage of `M.pages.get_section` above. if node:get_name():find("^Etymology") then finalize_current_etymology_section() etymology_section_count = etymology_section_count + 1 active_etymon_args = nil end elseif node_class == "template" then local template_name = node:get_name() if template_name == "etymon" then local template_args = node:get_arguments() -- Check if this etymon is for our language if template_args[1] == etymon_lang_code then if is_nonlemma_etymon_template(template_args) then if etymology_section_count > 0 then current_etymology_has_nonlemma = true end else if etymology_section_count > 0 then current_etymology_has_etymon = true end table.insert(found_templates_for_lang, template_args) if template_args.id then local etymon_key = lang_page_key .. ":" .. template_args.id __state.cached_etymon_args[etymon_key] = template_args __state.cached_etymon_pages[etymon_key] = tostring(etymon_page) table.insert(found_ids, template_args.id) active_etymon_args = template_args else -- Store idless etymon with default key local etymon_key = lang_page_key .. ":*" __state.cached_etymon_args[etymon_key] = template_args __state.cached_etymon_pages[etymon_key] = tostring(etymon_page) table.insert(found_ids, "*") active_etymon_args = template_args end end end elseif active_etymon_args and template_name == "senseid" then local template_args = node:get_arguments() -- This should always be true for proper usages of {{senseid}}. if template_args[1] == etymon_lang_code and template_args[2] then local sense_id_key = lang_page_key .. ":" .. template_args[2] __state.senseid_parent_etymon[sense_id_key] = active_etymon_args __state.cached_etymon_pages[sense_id_key] = tostring(etymon_page) end end end end finalize_current_etymology_section() if lang_section_for_descendants and etymology_section_count > 1 and etymology_sections_with_etymon > 0 and etymology_sections_with_etymon < etymology_section_count then __state.partial_etymology_targets[lang_page_key] = true end if descendants_is_toplevel and lang_section_for_descendants and #found_templates_for_lang > 0 then M.descendants.cache_page_checks({ lang_section = lang_section_for_descendants, etymon_lang_code = etymon_lang_code, found_templates_for_lang = found_templates_for_lang, entry_title = __state.entry_title, entry_lang_code = __state.entry_lang_code, entry_lang = __state.entry_lang_code and Util.get_lang(__state.entry_lang_code, true) or nil, cached_descendants_checks = __state.cached_descendants_checks, lang_page_key = lang_page_key, redirected_from = redirected_from, }) end local id_data_list = {} for _, args in ipairs(found_templates_for_lang) do local id = args.id or "*" table.insert(id_data_list, { id = id, pos = args.pos }) end __state.available_etymon_ids[lang_page_key] = id_data_list if #found_templates_for_lang == 1 then __state.single_etymons[lang_page_key] = found_templates_for_lang[1] end if redirected_from and __state.available_etymon_ids[lang_page_key] then __state.available_etymon_ids[redirected_from] = __state.available_etymon_ids[redirected_from] or {} for _, id_data in ipairs(__state.available_etymon_ids[lang_page_key]) do table.insert(__state.available_etymon_ids[redirected_from], id_data) end end if __state.cached_etymon_args[key] ~= nil or __state.senseid_parent_etymon[key] ~= nil then -- All done! return elseif redirect_target and not redirected_from then -- Try scraping the redirect. etymon_page = redirect_target.prefixedText DataRetriever.cache_page_etymons(etymon_page, redirect_target, lang_page_key .. ":" .. etymon_id, etymon_lang, etymon_id, lang_page_key, descendants_is_toplevel) __state.cached_etymon_args[key] = __state.cached_etymon_args[etymon_lang_code .. ":" .. etymon_page .. ":" .. etymon_id] else __state.cached_etymon_args[key] = M.data.STATUS.MISSING end end local function has_linkable_term(etymon_data) if etymon_data.is_family or etymon_data.suppress_term or etymon_data.unknown_term then return false end local term = etymon_data.term if term == nil or term == "" then return false end return M.string_utilities.trim(term) ~= "" end local function record_term_id_tracking(etymon_data) if not has_linkable_term(etymon_data) then return end local term_page = M.links.get_link_page(etymon_data.term, etymon_data.lang) M.tracking.record_term_id_usage(__state.id_stats, etymon_data, term_page) end -- Given an etymon object, scrape its page (if necessary) and return its own etymon arguments as well as the page name. function DataRetriever.get_etymon_args(etymon_data, is_toplevel) if not has_linkable_term(etymon_data) then return M.data.STATUS.MISSING, nil, nil, nil end local page = M.links.get_link_page(etymon_data.term, etymon_data.lang) local norm_lang = Util.get_norm_lang(etymon_data.lang) local base_key = norm_lang:getFullCode() .. ":" .. page if etymon_data.id then local key = base_key .. ":" .. etymon_data.id local cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key] if cached_args == nil then local title = mw.title.new(page) if not title then error('Invalid page title "' .. page .. '" encountered.') end DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, is_toplevel) end cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key] -- refresh -- Get etymon_id from parent if this was resolved via senseid local parent_etymon = __state.senseid_parent_etymon[key] local resolved_etymon_id = parent_etymon and parent_etymon.id local descendants_check = M.descendants.get_lookup_check({ cached_descendants_checks = __state.cached_descendants_checks, is_toplevel = is_toplevel, base_key = base_key, lookup = { explicit_id = etymon_data.id, parent_etymon = parent_etymon, }, }) if is_toplevel and descendants_check == nil then local title = mw.title.new(page) if title then DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, true) descendants_check = M.descendants.get_lookup_check({ cached_descendants_checks = __state.cached_descendants_checks, is_toplevel = true, base_key = base_key, lookup = { explicit_id = etymon_data.id, parent_etymon = parent_etymon, }, }) end end local mismatched_id = is_mismatched_explicit_id(base_key, cached_args, parent_etymon) if mismatched_id and is_toplevel then __state.has_mismatched_id = true M.tracking.record_mismatched_id_usage(__state.id_stats, norm_lang, page, etymon_data.id) warn_mismatched_explicit_id(page, norm_lang, base_key, etymon_data.id) end maybe_flag_partial_etymology_reference(base_key, etymon_data, cached_args, is_toplevel) return cached_args, __state.cached_etymon_pages[key], resolved_etymon_id, descendants_check else __state.used_idless_etymon = true if is_toplevel then __state.toplevel_idless_etymon = true end if __state.available_etymon_ids[base_key] == nil then local title = mw.title.new(page) if not title then error('Invalid page title "' .. page .. '" encountered.') end DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, is_toplevel) end local ids = __state.available_etymon_ids[base_key] or {} local count = #ids -- Try to filter by postype if available and we have multiple candidates if count > 1 and etymon_data.postype then local matching_ids = {} for _, id_data in ipairs(ids) do if id_data.pos == etymon_data.postype then table.insert(matching_ids, id_data) end end if #matching_ids == 1 then local matched_id = matching_ids[1].id local matched_key = base_key .. ":" .. matched_id M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "postype") local descendants_check = M.descendants.get_lookup_check({ cached_descendants_checks = __state.cached_descendants_checks, is_toplevel = is_toplevel, base_key = base_key, lookup = { id = matched_id }, }) if is_toplevel and descendants_check == nil then local title = mw.title.new(page) if title then DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true) descendants_check = M.descendants.get_lookup_check({ cached_descendants_checks = __state.cached_descendants_checks, is_toplevel = true, base_key = base_key, lookup = { id = matched_id }, }) end end local matched_args = __state.cached_etymon_args[matched_key] maybe_flag_partial_etymology_reference(base_key, etymon_data, matched_args, is_toplevel) return matched_args, __state.cached_etymon_pages[matched_key], nil, descendants_check end end if count == 1 then local only_id_data = ids[1] local only_id = (type(only_id_data) == "table" and only_id_data.id) or only_id_data or "*" M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "single") local descendants_check = M.descendants.get_lookup_check({ cached_descendants_checks = __state.cached_descendants_checks, is_toplevel = is_toplevel, base_key = base_key, lookup = { id_data = only_id_data }, }) if is_toplevel and descendants_check == nil then local title = mw.title.new(page) if title then DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true) descendants_check = M.descendants.get_lookup_check({ cached_descendants_checks = __state.cached_descendants_checks, is_toplevel = true, base_key = base_key, lookup = { id_data = only_id_data }, }) end end local single_args = __state.single_etymons[base_key] maybe_flag_partial_etymology_reference(base_key, etymon_data, single_args, is_toplevel) return single_args, __state.cached_etymon_pages[base_key .. ":" .. only_id], nil, descendants_check elseif count > 1 then M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "ambiguous") warn_ambiguous_etymon_link(page, norm_lang, ids, is_toplevel) maybe_flag_partial_etymology_reference(base_key, etymon_data, M.data.STATUS.AMBIGUOUS, is_toplevel) return M.data.STATUS.AMBIGUOUS, nil, nil, nil else M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "missing") maybe_flag_partial_etymology_reference(base_key, etymon_data, M.data.STATUS.MISSING, is_toplevel) return M.data.STATUS.MISSING, nil, nil, nil end end end local function keyword_invisible_in_tree(keyword_info) if not keyword_info then return false end local inv = keyword_info.invisible return inv == "all" or inv == true or inv == "tree" end -- True when the node has at least one top-level child container visible in the tree. local function node_has_visible_tree_children(node) for _, container in ipairs(node.children or {}) do if not keyword_invisible_in_tree(container.keyword_info) then return true end end return false end -- Count visible term nodes in the tree. local function get_visible_tree_depth(node, skip_child_rendering) local max_depth = 1 if skip_child_rendering or not node then return max_depth end for _, container in ipairs(node.children or {}) do local keyword_info = container.keyword_info if not keyword_invisible_in_tree(keyword_info) then local skip_grandchildren = keyword_info and keyword_info.no_child_categories for _, term in ipairs(container.terms or {}) do if term.is_duplicate then if term.original_has_children then max_depth = math.max(max_depth, 2) end else max_depth = math.max(max_depth, 1 + get_visible_tree_depth(term, skip_grandchildren)) end end end end return max_depth end local function as_param_list(val) if val == nil then return {} end if type(val) == "table" then return val end if type(val) == "string" and val ~= "" then return { val } end return {} end local TreeBuilder = {} -- Build a unique key for deduplication in the seen table function TreeBuilder.build_key(lang, title, args) local norm_lang_code = Util.get_norm_lang(lang):getFullCode() local is_table = type(args) == "table" local id = (is_table and args.id) or "" if title then return norm_lang_code .. ":" .. M.links.get_link_page(title, lang) .. ":" .. id end if is_table and args.status == M.data.STATUS.INLINE then local content_parts = {} for i = 1, #args do content_parts[i] = tostring(args[i]) end return norm_lang_code .. ":*:" .. id .. "\0" .. table.concat(content_parts, "\0") end return norm_lang_code .. ":*:" .. id end -- Copy parsed etymon modifiers onto a tree/supplement term node. function TreeBuilder.apply_etymon_fields(term, etymon_data) term.id = etymon_data.id term.gloss = etymon_data.gloss term.tr = etymon_data.tr term.ts = etymon_data.ts term.alt = etymon_data.alt term.genders = etymon_data.genders term.pos = etymon_data.pos term.ng = etymon_data.ng term.infl = etymon_data.infl term.refs = etymon_data.refs term.is_uncertain = etymon_data.unc term.lit = etymon_data.lit term.q = etymon_data.q term.qq = etymon_data.qq term.l = etymon_data.l term.ll = etymon_data.ll term.suppress_term = etymon_data.suppress_term term.unknown_term = etymon_data.unknown_term term.is_family = etymon_data.is_family term.override = etymon_data.override term.aftype = etymon_data.aftype term.postype = etymon_data.postype term.bor = etymon_data.bor term.lbor = etymon_data.lbor term.slbor = etymon_data.slbor end function TreeBuilder.build_supplement_term(etymon_data, entry_lang, supplement_type) EtymonParser.check_supplement_term(etymon_data, entry_lang, supplement_type) local term = { lang = etymon_data.lang, title = etymon_data.term, children = {}, status = M.data.STATUS.OK, } TreeBuilder.apply_etymon_fields(term, etymon_data) return term end function TreeBuilder.build_supplement_terms(entry_lang, supplement_type, param_value) local terms = {} for _, term_param in ipairs(as_param_list(param_value)) do if type(term_param) == "string" and term_param ~= "" then local etymon_data = EtymonParser.parse_etymon(term_param, entry_lang) if etymon_data then table.insert(terms, TreeBuilder.build_supplement_term(etymon_data, entry_lang, supplement_type)) end end end return terms end -- Attach a |param= supplement defined in etymon_data.supplements (e.g. doublet=). function TreeBuilder.append_term_supplement(data_tree, entry_lang, supplement_type, param_value) local config = M.data.supplements[supplement_type] if not config then error("Unknown supplement '" .. tostring(supplement_type) .. "'.") end local terms = TreeBuilder.build_supplement_terms(entry_lang, supplement_type, param_value) if #terms == 0 then return end data_tree.supplements = data_tree.supplements or {} table.insert(data_tree.supplements, { type = supplement_type, config = config, terms = terms, }) M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, supplement_type, entry_lang, entry_lang, true) end function TreeBuilder.build(lang, title, args, seen, depth, stop_recursion) seen = seen or {} depth = depth or 0 local is_toplevel = (depth == 0) if depth > __state.max_depth_reached then __state.max_depth_reached = depth end __state.total_nodes = __state.total_nodes + 1 local lang_code = lang:getCode() __state.language_count[lang_code] = (__state.language_count[lang_code] or 0) + 1 local current_id = (type(args) == "table" and args.id) or "" local key = TreeBuilder.build_key(lang, title, args) local node = { lang = lang, title = title, id = current_id, args = args, children = {}, status = M.data.STATUS.OK } if type(args) ~= "table" or seen[key] then node.status = args or M.data.STATUS.MISSING -- Mark as duplicate if we've seen this node before if seen[key] then node.is_duplicate = true node.duplicate_key = key local original_node = seen[key] if type(original_node) == "table" and original_node.children and #original_node.children > 0 then node.original_has_children = true end end return node end node.status = args.status or M.data.STATUS.OK seen[key] = node -- If stop_recursion is set, skip parsing children but check for visible children if stop_recursion then local keywords = M.data.keywords local has_visible_children = false for i = 2, #args do local param = args[i] if type(param) == "string" then local keyword_base = get_keyword_base(param) if keyword_base and keywords[keyword_base] then local _, kw_modifiers = EtymonParser.parse_keyword_modifiers(param:sub(1, 1) == ":" and param or (":" .. param)) if not keyword_invisible_in_tree(get_effective_keyword_info(keyword_base, kw_modifiers)) then has_visible_children = true break end elseif param:sub(1, 1) ~= ":" then -- It's a term (not a keyword), so there are visible children has_visible_children = true break end end end node.has_visible_children = has_visible_children return node end -- Parse args into keyword containers local current_keyword = "from" local current_keyword_modifiers = {} local current_container = nil local function ensure_container() if not current_container or current_container.keyword ~= current_keyword then local keyword_info = get_effective_keyword_info(current_keyword, current_keyword_modifiers) current_container = { keyword = current_keyword, keyword_info = keyword_info, keyword_modifiers = current_keyword_modifiers, terms = {}, } table.insert(node.children, current_container) -- Override keyword text/phrase for nominalization with <g:code> if current_keyword_modifiers.g and current_keyword == "nominalization" then local labels = get_nominalization_label_for_g(current_keyword_modifiers.g) if not labels then local codes = {} for c in pairs(M.data.nominalization_g_codes) do table.insert(codes, c) end table.sort(codes) error("Invalid <g:" .. tostring(current_keyword_modifiers.g) .. ">. Supported codes for nominalization: " .. table.concat(codes, ", ")) end current_container.keyword_info = copy_keyword_info(keyword_info) current_container.keyword_info.text = labels.text current_container.keyword_info.phrase = labels.phrase end end return current_container end local parse_context_lang = Util.resolve_context_lang(lang, args) for i = 2, #args do local param = args[i] if is_keyword(param) then local keyword, modifiers = EtymonParser.parse_keyword_modifiers(param) if not keyword then error("Invalid keyword '" .. param .. "'.") end current_keyword = keyword current_keyword_modifiers = modifiers current_container = nil -- Force new container for new keyword elseif type(param) == "string" and param:sub(1, 1) == ":" then reject_removed_surf_keyword(param) error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?") elseif type(param) == "string" then local etymon_data = EtymonParser.parse_etymon(param, parse_context_lang) if etymon_data then -- Track keyword usage at top level M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, current_keyword, lang, etymon_data.lang, is_toplevel) local term_node = {} local container -- Handle suppress_term (-) and unknown_term (empty or +) directly if etymon_data.suppress_term or etymon_data.unknown_term then container = ensure_container() if etymon_data.ety then local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang) inline_args.id = etymon_data.id inline_args.status = M.data.STATUS.INLINE term_node = TreeBuilder.build(etymon_data.lang, nil, inline_args, seen, depth + 1) else term_node = { lang = etymon_data.lang, children = {}, status = M.data.STATUS.OK, } end TreeBuilder.apply_etymon_fields(term_node, etymon_data) else -- Regular term: fetch arguments from page record_term_id_tracking(etymon_data) local etymon_args, page_of, resolved_etymon_id, descendants_check = DataRetriever.get_etymon_args(etymon_data, is_toplevel) -- Check for <ety> inline parameter doesn't override the scraped arguments, unless the latter are missing if etymon_data.ety then if etymon_args == M.data.STATUS.REDLINK or etymon_args == M.data.STATUS.MISSING then __state.current_page_has_inline_etymology = true if is_toplevel then __state.toplevel_has_inline_etymology = true end local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang) -- Track inline ety keywords too local inline_keyword = get_keyword(inline_args[2], true) if inline_keyword and #inline_args >= 3 then local inline_etymon = EtymonParser.parse_etymon(inline_args[3], etymon_data.lang) if inline_etymon then M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, inline_keyword, etymon_data.lang, inline_etymon.lang, is_toplevel) end end inline_args.id = etymon_data.id inline_args.status = M.data.STATUS.INLINE etymon_args = inline_args term_node.page_of = __state.cached_etymon_pages[key] -- term node is on the same page as the parent else -- Scraped arguments exist, <ety> is redundant and ignored __state.current_page_has_redundant_etymology = true if is_toplevel then __state.toplevel_redundant_etymology = true end end end -- Ensure container exists before checking keyword info container = ensure_container() -- Check if current keyword has no_child_categories - if so, stop recursion local keyword_info = container.keyword_info local should_stop_recursion = (stop_recursion or (keyword_info and keyword_info.no_child_categories)) term_node = TreeBuilder.build(etymon_data.lang, etymon_data.term, etymon_args, seen, depth + 1, should_stop_recursion) term_node.target_key = Util.get_norm_lang(etymon_data.lang):getFullCode() .. ":" .. M.links.get_link_page(etymon_data.term, etymon_data.lang) term_node.etymon_id = resolved_etymon_id -- The actual etymon id when resolved via senseid term_node.page_of = page_of TreeBuilder.apply_etymon_fields(term_node, etymon_data) term_node.missing_descendants_header, term_node.missing_descendants_entry = M.descendants.get_term_sync_flags(current_keyword, term_node.status, descendants_check) end table.insert(container.terms, term_node) end end end return node end -- Convert etymology tree to JSON-serializable table local function tree_to_json(node) local obj = { term = node.title, lang = node.lang:getCode(), lang_name = node.lang:getCanonicalName(), id = (node.id and node.id ~= "") and node.id or nil, status = node.status, is_uncertain = node.is_uncertain or nil, is_duplicate = node.is_duplicate or nil, gloss = node.gloss, transliteration = node.tr, transcription = node.ts, alt = node.alt, g = node.genders, pos = node.pos, ng = node.ng, infl = node.infl, children = {}, } for _, container in ipairs(node.children or {}) do local keyword_info = container.keyword_info if keyword_info then local container_obj = { keyword = container.keyword, keyword_label = keyword_info.text, keyword_abbrev = keyword_info.abbrev, is_group = keyword_info.is_group or nil, is_invisible = keyword_info.invisible or nil, is_uncertain = (container.keyword_modifiers and container.keyword_modifiers.unc) or nil, terms = {}, } for _, term in ipairs(container.terms or {}) do table.insert(container_obj.terms, tree_to_json(term)) end table.insert(obj.children, container_obj) end end return obj end -- Build and return the etymology data tree for a given term. function export.get_tree(lang, title, args, options) options = options or {} __state.entry_title = title __state.entry_lang_code = lang:getCode() __state.id_stats = M.tracking.new_id_stats() __state.skip_partial_etymology_category = options.skip_partial_etymology_category == true if options.validate then EtymonParser.validate(lang, args, options.id, title, options.pos, false) end local lang_code = lang:getCode() local start_index = (args[1] == lang_code) and 2 or 1 local tree_args = { [1] = lang_code, id = options.id or args.id } for i = start_index, #args do table.insert(tree_args, args[i]) end __state.cached_etymon_args[lang_code .. ":" .. title .. ":" .. (tree_args.id or "")] = tree_args local ety_data_tree = TreeBuilder.build(lang, title, tree_args) if options.json then return M.JSON.toJSON(tree_to_json(ety_data_tree)) end return ety_data_tree end -- Given a language code, page name and optionally the id= parameter, -- render the tree and only the etymology tree for the relevant page. -- Fetches and parses the corresponding {{etymon}} from the requested page, -- and any further pages needed to render the tree. -- Parameters can be passed either through the #invoke or as -- template parameters *through* an #invoke. function export.render_tree_for_etymon_on_page(frame) local frame_args = frame.args local parent_args = frame:getParent().args local langcode = frame_args[1] or parent_args[1] local pagename = frame_args[2] or parent_args[2] local id = frame_args["id"] or parent_args["id"] local display_title = frame_args["title"] or parent_args["title"] local parsed_title = mw.title.new(pagename, 0) local title if parsed_title.namespace == 0 then title = M.pages.safe_page_name(parsed_title) elseif parsed_title.namespace == 118 then title = "*" .. M.pages.safe_page_name(parsed_title) else error("Unsupported namespace for render_tree_for_etymon_on_page: " .. parsed_title.namespace) end local lang = Util.get_lang(langcode) __state.entry_title = title __state.entry_lang_code = lang:getCode() __state.id_stats = M.tracking.new_id_stats() -- Construct etymon_data for DataRetriever.get_args. local etymon_data = { lang = lang, term = title, id = id } local args, pagename = DataRetriever.get_etymon_args(etymon_data, true) if args == M.data.STATUS.MISSING then error("The etymon template was not found (language " .. langcode .. ", title '" .. title .. "'" .. (id and ", ID '" .. id .. "'" or ", no ID given") .. "). Page contents may have changed in the interim.") end local tree_title = display_title or title if lang:stripDiacritics(M.links.remove_links(tree_title)) ~= lang:stripDiacritics(M.links.remove_links(title)) then M.tracking.track_title_pagename_mismatch(lang) end reset_invocation_state() local ety_data_tree = export.get_tree(lang, tree_title, args, { validate = true, id = id, }) local output = {} table.insert(output, M.template_styles("Module:etymon/styles.css")) table.insert(output, M.tree.render({ data_tree = ety_data_tree, format_term_func = function(term, is_toplevel) return Util.format_term(term, is_toplevel, { gloss = "suppress", pos = "suppress", lit = "suppress", tree_ql = "suppress", }) end, })) return table.concat(output) end function export.main(frame) local parent_args = frame:getParent().args local args = M.parameters.process(parent_args, M.parameters_data.etymon) local lang = args[1] local etymon_args = args[2] local id = args.id local title = args.title local text = args.text local tree = args.tree local etydate = args.etydate local doublet = args.doublet local rfe = args.rfe local etystub = args.etystub local is_nonlemma = M.yesno(args.nl, false) local page_data = Util.get_page_data() if not title then title = page_data.pagename if page_data.namespace == "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 = M.parameter_utilities.construct_param_mods { { group = "ref" }, { param = "refn" }, { param = "nocap", type = "boolean" }, } local function generate_etydate_obj(etydate_text) local etydate_specs = {} for spec in etydate_text:gmatch("[^,]+") do table.insert(etydate_specs, mw.text.trim(spec)) end return { [1] = etydate_specs } end local parsed_etydate = M.parse_utilities.parse_inline_modifiers(etydate, { param_mods = etydate_param_mods, generate_obj = generate_etydate_obj }) local etydate_args = { [1] = parsed_etydate[1], nocap = parsed_etydate.nocap or false, } ety_data_tree.supplements = ety_data_tree.supplements or {} table.insert(ety_data_tree.supplements, { type = "etydate", etydate_text = M.etydate.format_etydate(etydate_args, { omit_refs = true }), etydate_refs = (parsed_etydate.refs and #parsed_etydate.refs > 0) and parsed_etydate.refs or nil, }) end TreeBuilder.append_term_supplement(ety_data_tree, lang, "doublet", doublet) local has_visible_children = node_has_visible_tree_children(ety_data_tree) -- Suppress trees for multiword entries and one-step chains local visible_tree_depth = get_visible_tree_depth(ety_data_tree) local is_trivial_tree = visible_tree_depth <= 2 local is_multiword = title:find("%s") ~= nil or title:find("_") ~= nil if tree and (is_multiword or is_trivial_tree) then tree = false end if tree then table.insert(output, M.template_styles("Module:etymon/styles.css")) table.insert(output, M.tree.render({ data_tree = ety_data_tree, format_term_func = function(term, is_toplevel) return Util.format_term(term, is_toplevel, { gloss = "suppress", pos = "suppress", lit = "suppress", tree_ql = "suppress", }) end, })) end local tree_disallowed = lang_exc and lang_exc.disallow and lang_exc.disallow.tree local ety_tree_json = M.JSON.toJSON(tree_to_json(ety_data_tree)) local anchor = M.anchors.etymonid(lang, id, { no_tree = args.notree, title = title, empty_tree = (not has_visible_children) or tree_disallowed, ety_tree_json = ety_tree_json, }) table.insert(output, anchor) local text_stop_lang_missing = nil if text then local max_depth, stop_at_blue_link, stop_at_lang, stop_at_lang_or_bluelink if text == "++" then max_depth, stop_at_blue_link = false, false elseif text == "+" then max_depth, stop_at_blue_link = 1, false elseif text == "*" then max_depth, stop_at_blue_link = false, true elseif text:match("^:[^*]+%*$") then -- Stop at a specific language OR first bluelink after it, e.g., ":ota*" -- If the target language is a redlink, continue to the first bluelink local lang_code = text:match("^:([^*]+)%*$") if lang_code and lang_code ~= "" then local lang_obj = Util.get_lang(lang_code, true) if lang_obj then stop_at_lang_or_bluelink = lang_code else Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.') max_depth, stop_at_blue_link = false, false end else Util.add_warning('Empty language code in text parameter. Showing full chain instead.') max_depth, stop_at_blue_link = false, false end elseif text:sub(1, 1) == ":" then -- Stop at a specific language, e.g., ":ar" stops at first Arabic term local lang_code = text:sub(2) if lang_code ~= "" then -- Validate the language code local lang_obj = Util.get_lang(lang_code, true) if lang_obj then stop_at_lang = lang_code else Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.') max_depth, stop_at_blue_link = false, false -- default to ++ end else Util.add_warning('Empty language code in text parameter. Showing full chain instead.') max_depth, stop_at_blue_link = false, false -- default to ++ end else local num = tonumber(text) if num and num >= 1 then max_depth, stop_at_blue_link = num, false else error('Invalid text value "' .. text .. '". Valid values are: "++" (full chain), "+" (first step only), "*" (until first blue link), a number (max steps), ":lang" (stop at language), or ":lang*" (stop at language or first bluelink if redlink)') end end local text_output, text_render_meta = M.text.render({ data_tree = ety_data_tree, format_term_func = Util.format_term, lang_matches_stop_code = Util.lang_matches_stop_code, max_depth = max_depth, stop_at_blue_link = stop_at_blue_link, curr_page = page_data.pagename, nodot = args.nodot, dot = args.dot, stop_at_lang = stop_at_lang, stop_at_lang_or_bluelink = stop_at_lang_or_bluelink, }) table.insert(output, text_output) if stop_at_lang and text_render_meta and not text_render_meta.stop_lang_reached then M.tracking.track_text_stop_lang_missing(lang, stop_at_lang) text_stop_lang_missing = stop_at_lang end end if rfe then table.insert(output, Util.expand_request_template(frame, "rfe", rfe, lang:getCode())) end if etystub then table.insert(output, Util.expand_request_template(frame, "etystub", etystub, lang:getCode())) end if is_nonlemma then table.insert(output, " " .. frame:expandTemplate({ title = "nonlemma", args = {}, })) end local categories = {} if Util.is_content_page() then M.tracking.track_tree_metrics({ max_depth_reached = __state.max_depth_reached, total_nodes = __state.total_nodes, language_count = __state.language_count, lang = lang, }) categories = M.categories.build({ data_tree = ety_data_tree, page_lang = lang, available_etymon_ids = __state.available_etymon_ids, senseid_parent_etymon = __state.senseid_parent_etymon, get_norm_lang_func = Util.get_norm_lang, lang_exc = lang_exc, suppress_categories = lang_exc and lang_exc.suppress_categories, nocat = args.nocat, tree = tree, text = text, exnihilo = args.exnihilo, toplevel_has_inline_etymology = __state.toplevel_has_inline_etymology, toplevel_redundant_etymology = __state.toplevel_redundant_etymology, toplevel_idless_etymon = __state.toplevel_idless_etymon, has_mismatched_id = __state.has_mismatched_id, linked_page_multiple_etymons_idless = __state.linked_page_multiple_etymons_idless, linked_page_partial_etymology_sections = __state.linked_page_partial_etymology_sections, text_stop_lang_missing = text_stop_lang_missing, }) M.tracking.track_keywords(__state.toplevel_keyword_stats, lang) M.tracking.track_page_id(lang, id) M.tracking.track_ids(__state.id_stats, lang) end if #categories > 0 then table.insert(output, M.categories.format(categories, lang)) end if __state.warnings then for i, warning in ipairs(__state.warnings) do table.insert(output, (i == 1 and "\n" or "") .. warning .. "\n") end end return table.concat(output) end return export 83f0fr5rgmxbm610bma1l6w6fdin4eo 178080 178067 2026-09-25T03:44:05Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 maiayos ang kawing na tumuro sa English Wiktionary 178080 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: [[en:User:Ioaxxere]] - Full refactor (September 2025): [[en:User:Fenakhay]] ([[en:Special:Diff/86717746]]) Modules: - [[Module:etymon]]: main module handling parsing, validation, tree building, and page scraping - [[Module:etymon/data]]: keyword definitions, configuration, and status constants - [[Module:etymon/tree]]: etymology tree rendering - [[Module:etymon/text]]: etymology text generation - [[Module:etymon/categories]]: category generation logic - [[Module:etymon/tracking]]: tracking ]=] local export = {} local __state = { cached_etymon_args = {}, cached_etymon_pages = {}, cached_descendants_checks = {}, senseid_parent_etymon = {}, available_etymon_ids = {}, single_etymons = {}, entry_title = nil, entry_lang_code = nil, current_page_has_inline_etymology = false, current_page_has_redundant_etymology = false, used_idless_etymon = false, toplevel_has_inline_etymology = false, toplevel_redundant_etymology = false, toplevel_idless_etymon = false, has_mismatched_id = false, linked_page_multiple_etymons_idless = false, linked_page_partial_etymology_sections = false, partial_etymology_targets = {}, skip_partial_etymology_category = false, max_depth_reached = 0, total_nodes = 0, language_count = {}, toplevel_keyword_stats = {}, id_stats = nil, warnings = {}, } local function reset_invocation_state() __state.current_page_has_inline_etymology = false __state.current_page_has_redundant_etymology = false __state.used_idless_etymon = false __state.toplevel_has_inline_etymology = false __state.toplevel_redundant_etymology = false __state.toplevel_idless_etymon = false __state.has_mismatched_id = false __state.linked_page_multiple_etymons_idless = false __state.linked_page_partial_etymology_sections = false __state.max_depth_reached = 0 __state.total_nodes = 0 __state.language_count = {} __state.toplevel_keyword_stats = {} __state.warnings = {} end local M = require("Module:module loader").init({ require = { data = "Module:etymon/data", tree = "Module:etymon/tree", text = "Module:etymon/text", categories = "Module:etymon/categories", tracking = "Module:etymon/tracking", descendants = "Module:etymon/descendants", anchors = "Module:anchors", etydate = "Module:etydate", etymology = "Module:etymology", families = "Module:families", languages = "Module:languages", languages_errorgetby = "Module:languages/errorGetBy", links = "Module:links", pages = "Module:pages", parameters = "Module:parameters", string_utilities = "Module:string utilities", template_parser = "Module:template parser", utilities = "Module:utilities", debug = "Module:debug", en_utilities = "Module:en-utilities", parameter_utilities = "Module:parameter utilities", parse_utilities = "Module:parse utilities", template_styles = "Module:TemplateStyles", script_utilities = "Module:script utilities", JSON = "Module:JSON", yesno = "Module:yesno", }, loadData = { headword_data = "Module:headword/data", parameters_data = "Module:parameters/data", text_allowed = "Module:etymon/data/text_allowed", }, }) local Util = {} function Util.format_error(message, preview_only) if preview_only and not M.pages.is_preview() then return nil end return '<span class="error">' .. message .. '</span>' end function Util.add_warning(message, preview_only) local formatted = Util.format_error(message, preview_only) if formatted then table.insert(__state.warnings, formatted) end end function Util.is_text_param_allowed_for_lang(lang) if not lang or type(lang) ~= "table" then return false end local types = lang.getTypes and lang:getTypes() if types and types.family then local code = lang.getCode and lang:getCode() return code and M.text_allowed.families[code] == true end local full_code = lang.getFullCode and lang:getFullCode() if full_code and M.text_allowed.langs[full_code] then return true end if lang.inFamily then for family_code in pairs(M.text_allowed.families) do if lang:inFamily(family_code) then return true end end end return false end function Util.get_lang(code, no_error) if no_error then return M.languages.getByCode(code, nil, true) end return M.languages.getByCode(code, nil, true) or M.languages_errorgetby.code(code, true, true) end -- Match a term language against a text=:lang stop target (supports etymology-only codes). function Util.lang_matches_stop_code(term_lang, stop_code) if not term_lang or not stop_code or stop_code == "" then return false end local stop_lang = Util.get_lang(stop_code, true) if not stop_lang then return false end if term_lang:getCode() == stop_lang:getCode() then return true end if stop_lang:getFullCode() == stop_lang:getCode() then return term_lang:getFullCode() == stop_lang:getCode() end return false end function Util.get_family(code) return M.families.getByCode(code) end function Util.get_lang_exception(lang) -- Families have no language-specific exceptions if lang.getTypes and lang:getTypes().family then return nil end local code = lang:getCode() local lang_exceptions = M.data.config.lang_exceptions if lang_exceptions[code] then return lang_exceptions[code] end for norm_code, exc in pairs(lang_exceptions) do if exc.normalize_to and code == exc.normalize_to then return exc end if exc.normalize_from_families then local should_normalize = false for _, family in ipairs(exc.normalize_from_families) do if lang:inFamily(family) then should_normalize = true break end end if should_normalize and exc.normalize_exclude_families then for _, family in ipairs(exc.normalize_exclude_families) do if lang:inFamily(family) then should_normalize = false break end end end if should_normalize then local ret = {} for k, v in pairs(exc) do ret[k] = v end ret.suppress_tr = nil return ret end end end return nil end function Util.get_norm_lang(lang) local exc = Util.get_lang_exception(lang) if exc and exc.normalize_to then return M.languages.getByCode(exc.normalize_to) end return lang end function Util.resolve_context_lang(lang, node_args) if type(node_args) ~= "table" then return lang end if node_args.status == M.data.STATUS.INLINE then return lang end if not (lang.hasType and lang:hasType("etymology-only")) then return lang end local full = lang.getFull and lang:getFull() if not full or full:getCode() == lang:getCode() then return lang end if full.hasAncestor and full:hasAncestor(lang) then return lang end return full end -- Add default values for boolean modifiers (e.g., <unc> becomes <unc:1>) -- This is needed because Module:parse utilities expects boolean modifiers to have explicit values function Util.add_boolean_defaults(str, param_mods) local result = str for name, spec in pairs(param_mods) do if spec.type == "boolean" then -- Replace <name> with <name:1> (but not <name:...> which already has a value) result = result:gsub("<" .. name .. ">", "<" .. name .. ":1>") end end return result end local REQUEST_TEMPLATE_PARAM_MODS = { rfe = M.parameter_utilities.construct_param_mods { { param = { "sort", "y", "m", "fragment", "section" } }, { param = { "nocat", "box", "noes" }, type = "boolean" }, }, etystub = M.parameter_utilities.construct_param_mods { { param = "sort" }, { param = { "nocat", "nocap", "nodot" }, type = "boolean" }, }, } function Util.expand_request_template(frame, template_name, param_value, lang_code) local param_mods = REQUEST_TEMPLATE_PARAM_MODS[template_name] local with_defaults = Util.add_boolean_defaults(param_value, param_mods) local parsed = M.parse_utilities.parse_inline_modifiers(with_defaults, { param_mods = param_mods, generate_obj = function(text) if M.yesno(text, false) then return { is_boolean = true } end return { text = text } end, }) local template_args = { [1] = lang_code } for name in pairs(param_mods) do template_args[name] = parsed[name] end if not parsed.is_boolean then template_args[2] = parsed.text end return " " .. frame:expandTemplate({ title = template_name, args = template_args, }) end -- Centralized term formatting: handles suppress_term (-), unknown_term (empty/+), and regular terms function Util.format_term(term, is_toplevel, opts) opts = opts or {} -- suppress_term (-) returns nil if term.suppress_term then return nil end local lang = term.lang local exc = Util.get_lang_exception(lang) if is_toplevel then local display_text = term.alt or term.title or "" local sc = term.sc or lang:findBestScript(display_text) local bold_text = tostring(mw.html.create("strong") :addClass("selflink") :wikitext(display_text)) return M.script_utilities.tag_text(bold_text, lang, sc, "term") end local link_params = { lang = lang } link_params.term = not term.unknown_term and term.title or nil link_params.alt = term.alt link_params.id = (not term.unknown_term and term.id and term.id ~= "") and term.id or nil if not (exc and exc.suppress_tr) then link_params.tr = term.tr link_params.ts = term.ts else link_params.suppress_tr = true end link_params.lit = (opts.lit ~= "suppress") and term.lit or nil if opts.gloss ~= "suppress" then link_params.gloss = term.gloss end link_params.genders = term.genders if opts.pos ~= "suppress" then link_params.pos = term.pos link_params.ng = term.ng link_params.infl = term.infl end if exc and exc.suppress_tr then link_params.lit = nil end if opts.tree_ql ~= "suppress" then if term.q then link_params.q = term.q end if term.qq then link_params.qq = term.qq end if term.l then link_params.l = term.l end if term.ll then link_params.ll = term.ll end link_params.show_decorations = term.q or term.qq or term.l or term.ll end return M.links.full_link(link_params, "term") end local __is_content_page_cached function Util.is_content_page() if __is_content_page_cached == nil then __is_content_page_cached = M.pages.is_content_page(mw.title.getCurrentTitle()) end return __is_content_page_cached end local __page_data_cached function Util.get_page_data() if not __page_data_cached then __page_data_cached = M.headword_data.page end return __page_data_cached end -- Extract base keyword from param (without modifiers) local function get_keyword_base(param) if type(param) ~= "string" then return nil end local base = param:match("^:?([^<]+)") or param:gsub("^:", "") return base end local function is_keyword(param, allow_colon_less) if type(param) ~= "string" then return false end local keywords = M.data.keywords if param:sub(1, 1) == ":" then local base = get_keyword_base(param) return keywords[base] ~= nil end if allow_colon_less then local base = get_keyword_base(param) return keywords[base] ~= nil end return false end local function get_keyword(param, allow_colon_less) if type(param) ~= "string" then return nil end local keywords = M.data.keywords if param:sub(1, 1) == ":" then return get_keyword_base(param) end if allow_colon_less then local base = get_keyword_base(param) if keywords[base] then return base end end return nil end local function normalize_keyword(keyword) if keyword:sub(1, 1) == ":" then return keyword end return ":" .. keyword end -- Resolve keyword (possibly an alias) to its canonical form. Used only at input boundaries local function get_canonical_keyword(keyword) if not keyword then return keyword end return M.data.keyword_canonical[keyword] or keyword end local function is_affix_group_keyword(keyword) local config = keyword and M.data.keywords[keyword] return config and config.affix_categories or false end local function reject_removed_surf_keyword(param) local base = get_keyword_base(param) if base == "surf" then error("The `:surf` keyword has been removed. Use `<surf>` on a formation keyword instead (e.g. `:af<surf>`, `:bor<surf>`).") end end local function copy_keyword_info(source) local copy = {} for k, v in pairs(source) do copy[k] = v end return copy end local function lowercase_glossary_display(text) return text:gsub("(%[%[Appendix:Glossary#[^|]+|)([^%]])([^%]]*)%]%]", function(prefix, first, rest) return prefix .. mw.ustring.lower(first) .. rest .. "]]" end) end local function surf_should_keep_formation_phrase(base) if not base.phrase then return false end if base.glossary then return true end return not (base.phrase == "from" and (base.text == "From" or base.text == "from")) end -- Runtime overrides when <surf> is present on a keyword. local function get_effective_keyword_info(keyword, modifiers) local base = M.data.keywords[keyword] if not base or not modifiers or not modifiers.surf then return base end local effective = copy_keyword_info(base) local surf_text = "By [[Appendix:Glossary#surface_analysis|surface analysis]]," local surf_phrase = "by surface analysis," effective.new_sentence = true effective.invisible = "tree" if surf_should_keep_formation_phrase(base) then effective.phrase = surf_phrase .. " " .. base.phrase if base.text then effective.text = surf_text .. " " .. lowercase_glossary_display(base.text) else effective.text = surf_text .. " " .. base.phrase end else effective.text = surf_text effective.phrase = surf_phrase end return effective end -- Build text/phrase for nominalization with <g:code> (uses data module for codes only). local function get_nominalization_label_for_g(code) if not code or code == "" then return nil end local codes = M.data.nominalization_g_codes local adj = codes[code] if not adj and #code == 2 then local gender_adj = codes[code:sub(1, 1)] local number_adj = codes[code:sub(2, 2)] if gender_adj and number_adj then adj = gender_adj .. " " .. number_adj end end if not adj then return nil end local text = adj:gsub("^%l", function(c) return string.upper(c) end) .. " [[Appendix:Glossary#nominalization|nominalization]] of" local phrase = M.en_utilities.add_indefinite_article(adj .. " [[Appendix:Glossary#nominalization|nominalization]] of", false) return { text = text, phrase = phrase } end local EtymonParser = {} -- Keyword modifier definitions EtymonParser.keyword_param_mods = M.parameter_utilities.construct_param_mods { { group = "ref" }, { param = "conj" }, -- conjunction for alternatives: "and", "or", "and/or", etc. { param = { "unc", "surf" }, type = "boolean" }, { param = "text", restrict = { keywords = { "from", "derived" } } }, { param = "lit", restrict = { affix_group = true } }, { param = "g", restrict = { keywords = { "nominalization" } } }, { param = "senseid", restrict = { keywords = { "semantic loan" } } }, } -- Term modifier definitions EtymonParser.etymon_param_mods = M.parameter_utilities.construct_param_mods { {group = {"link", "q", "l", "ref", "infl"}, exclude = {"sc"}}, {param = {"ety", "postype"}}, {param = "unc", type = "boolean"}, {param = "aftype", restrict = {affix_group = true}}, {param = {"bor", "slbor", "lbor"}, type = "boolean", restrict = {affix_group = true}}, } local function get_clean_param_mods(param_mods) local clean = {} for mod_name, mod_def in pairs(param_mods) do clean[mod_name] = {} for key, value in pairs(mod_def) do if key ~= "restrict" then clean[mod_name][key] = value end end end return clean end function EtymonParser.check_modifier_restrictions(modifiers, current_keyword, param_mods) for mod_name, mod_value in pairs(modifiers) do -- Only check restrictions if the modifier has a non-false/nil value if mod_value then local mod_def = param_mods[mod_name] if mod_def and mod_def.restrict then if mod_def.restrict.affix_group then if not is_affix_group_keyword(current_keyword) then local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">" error("The modifier `" .. mod_display .. "` is only allowed for affix-group keywords (e.g. `:af`, `:blend`, `:univ`).") end elseif mod_def.restrict.keywords then local allowed_keywords = mod_def.restrict.keywords local is_allowed = false for _, allowed_keyword in ipairs(allowed_keywords) do if current_keyword == allowed_keyword then is_allowed = true break end end if not is_allowed then local keyword_list = {} for _, kw in ipairs(allowed_keywords) do table.insert(keyword_list, ":" .. kw) end local keyword_str = table.concat(keyword_list, #keyword_list == 2 and " or " or ", ") if #keyword_list > 2 then -- Replace last comma with "or" keyword_str = keyword_str:gsub(", ([^,]+)$", " or %1") end local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">" error("The modifier `" .. mod_display .. "` is only allowed for the keyword" .. (#keyword_list > 1 and "s " or " ") .. keyword_str .. ".") end end end end end end local TERM_RULE_DISALLOW = { suppress = { field = "suppress_term", label = "suppressed" }, unknown = { field = "unknown_term", label = "unknown" }, family = { field = "is_family", label = "family" }, } function EtymonParser.check_etymon_limits(count, limits, label, opts) if not limits then return end opts = opts or {} local min_etymons = limits.min_etymons if min_etymons == nil and not opts.skip_default_min then min_etymons = 1 end if min_etymons and count < min_etymons then if min_etymons > 1 then error("Detected " .. label .. " group with fewer than " .. min_etymons .. " etymons.") else error("Detected " .. label .. " with no etymons.") end end if limits.max_etymons and count > limits.max_etymons then local unit = (limits.max_etymons == 1) and "etymon" or "etymons" error("Detected " .. label .. " with more than " .. limits.max_etymons .. " " .. unit .. ".") end end function EtymonParser.check_term_rules(etymon_data, entry_lang, rules, label) label = label or "term" if rules and rules.disallow then local disallowed = {} for _, typ in ipairs(rules.disallow) do local spec = TERM_RULE_DISALLOW[typ] if spec and etymon_data[spec.field] then table.insert(disallowed, spec.label) end end if #disallowed > 0 then error(label .. " does not support " .. mw.text.listToText(disallowed, "or") .. " etymons.") end end if etymon_data.is_family then if rules and rules.family == "disallowed" then error(label .. " does not support family codes" .. (rules.family_suffix or ".")) elseif not etymon_data.suppress_term then error("Family codes require suppressed term (use family:-).") end end if rules then if rules.require_term and (not etymon_data.term or etymon_data.term == "") then error(label .. " requires a term for each listed form.") end if rules.entry_lang then if Util.get_norm_lang(etymon_data.lang):getFullCode() ~= Util.get_norm_lang(entry_lang):getFullCode() then error(label .. " terms must be in the entry language (" .. entry_lang:getFullCode() .. "), got '" .. etymon_data.lang:getFullCode() .. "'.") end end if rules.ancestor_check then M.etymology.check_ancestor(entry_lang, etymon_data.lang) end elseif etymon_data.is_family and not etymon_data.suppress_term then error("Family codes require suppressed term (use family:-).") end end function EtymonParser.check_keyword_term(etymon_data, entry_lang, keyword) local config = M.data.keywords[keyword] EtymonParser.check_term_rules(etymon_data, entry_lang, config and config.term_rules, "`:" .. keyword .. "`") end function EtymonParser.check_supplement_term(etymon_data, entry_lang, supplement_type) local config = M.data.supplements[supplement_type] EtymonParser.check_term_rules(etymon_data, entry_lang, config and config.term_rules, "|" .. supplement_type .. "=") end -- Parse keyword with modifiers (e.g., ":bor<unc>" or ":bor<ref:{{R:example}}>") function EtymonParser.parse_keyword_modifiers(param) if type(param) ~= "string" then return nil, {} end local base_keyword = get_keyword_base(param) if not base_keyword then return nil, {} end local canonical_keyword = get_canonical_keyword(base_keyword) -- Check if there are any modifiers if not param:find("<", 1, true) then return canonical_keyword, {} end -- Parse modifiers using the same mechanism as etymon parsing local rest_with_defaults = Util.add_boolean_defaults(param, EtymonParser.keyword_param_mods) local function generate_obj(ignored) return {} end local parsed = M.parse_utilities.parse_inline_modifiers(rest_with_defaults:gsub("^:?[^<]+", ""), { param_mods = get_clean_param_mods(EtymonParser.keyword_param_mods), generate_obj = generate_obj }) local modifiers = { unc = parsed.unc or false, refs = parsed.refs, text = parsed.text, lit = parsed.lit, conj = parsed.conj, g = parsed.g, surf = parsed.surf or false, senseid = parsed.senseid, } -- Validate modifiers against restrictions EtymonParser.check_modifier_restrictions(modifiers, canonical_keyword, EtymonParser.keyword_param_mods) return canonical_keyword, modifiers end local function normalize_keyword_param(keyword_with_mods) local trimmed = M.string_utilities.trim(keyword_with_mods) reject_removed_surf_keyword(trimmed:match("^:") and trimmed or (":" .. trimmed)) local base = get_keyword_base(trimmed) if not base or not M.data.keywords[base] then error("Invalid keyword '" .. trimmed .. "' in inline etymology") end local canonical_base = get_canonical_keyword(base) local without_colon = trimmed:gsub("^:", "") local mods_part = without_colon:sub(#base + 1) local kw_param = normalize_keyword(canonical_base .. mods_part) EtymonParser.parse_keyword_modifiers(kw_param) return kw_param end local function get_keyword_mod_names() local names = {} for mod_name in pairs(EtymonParser.keyword_param_mods) do names[mod_name] = true end return names end local function parse_inline_ety_run(ety_string) local body = ety_string or "" if body == "" then error("Empty inline etymology") end local keyword_mod_names = get_keyword_mod_names() local pos = 1 local len = #body local function parse_err(msg) error(msg .. " in inline etymology: '" .. body .. "'") end local function peek_double() return body:sub(pos, pos + 1) == "<<" end local function mod_name_from_unwrapped(unwrapped) return unwrapped:match("^<([^:>]+)") end local function is_keyword_mod(unwrapped) local name = mod_name_from_unwrapped(unwrapped) return name and keyword_mod_names[name] or false end local function read_double_bracket() if not peek_double() then return nil end local start = pos pos = pos + 2 while pos <= len - 1 do if body:sub(pos, pos + 1) == ">>" then local token = body:sub(start, pos + 1) pos = pos + 2 return token, token:sub(2, -2) end pos = pos + 1 end parse_err("Unmatched <<") end local function read_angle_cell() if body:sub(pos, pos) ~= "<" or peek_double() then return nil end local open = pos pos = pos + 1 local depth = 1 local i = pos while i <= len do local ch = body:sub(i, i) if ch == "<" then depth = depth + 1 elseif ch == ">" then depth = depth - 1 if depth == 0 then local inner = body:sub(open + 1, i - 1) pos = i + 1 return inner end end i = i + 1 end parse_err("Unmatched <") end local function read_bare_run() local start = pos while pos <= len and body:sub(pos, pos) ~= "<" do pos = pos + 1 end return body:sub(start, pos - 1) end local function absorb_double_keyword_mods(keyword_str) while peek_double() do local saved = pos local _, unwrapped = read_double_bracket() if is_keyword_mod(unwrapped) then keyword_str = keyword_str .. unwrapped else pos = saved break end end return keyword_str end local kw_start = pos while pos <= len and body:sub(pos, pos) ~= "<" do pos = pos + 1 end local keyword = body:sub(kw_start, pos - 1) if keyword:match("^%s*$") then parse_err("Missing keyword") end keyword = absorb_double_keyword_mods(keyword) local cells = {} while pos <= len do if peek_double() then local _, unwrapped = read_double_bracket() if is_keyword_mod(unwrapped) then parse_err("Unexpected keyword modifier " .. unwrapped .. " outside of a keyword") end table.insert(cells, "+" .. unwrapped) elseif body:sub(pos, pos) == "<" then local inner = read_angle_cell() if inner ~= "" then table.insert(cells, inner) end else local bare = read_bare_run() if bare ~= "" then if bare:sub(1, 1) ~= ":" then parse_err("Unexpected bare text '" .. bare .. "' (use :keyword for nested keywords in inline etymology)") end if not is_keyword(bare, true) then parse_err("Invalid keyword '" .. bare .. "' in inline etymology") end table.insert(cells, absorb_double_keyword_mods(bare)) end end end return { keyword = keyword, cells = cells, } end function EtymonParser.inline_ety_to_pipe(ety_string) local run = parse_inline_ety_run(ety_string) if not run.keyword or run.keyword:match("^%s*$") then return "|" end local pipe_parts = { normalize_keyword_param(M.string_utilities.trim(run.keyword)) } for _, segment in ipairs(run.cells) do if is_keyword(segment, true) then table.insert(pipe_parts, normalize_keyword_param(segment)) else table.insert(pipe_parts, segment) end end return "|" .. table.concat(pipe_parts, "|") .. "|" end function EtymonParser.pipe_to_inline_ety(pipe_string) local cells = {} for cell in pipe_string:gmatch("([^|]+)") do if cell ~= "" then table.insert(cells, cell) end end if #cells == 0 then return "" end local inline_parts = {} for index, cell in ipairs(cells) do local base = get_keyword_base(cell) if base and M.data.keywords[base] then local without_colon = cell:gsub("^:", "") local kw_base, mods = without_colon:match("^([^<]+)(.*)$") local inline_kw = (kw_base or without_colon) .. (mods or ""):gsub("<([^>]+)>", "<<%1>>") if index > 1 then inline_kw = ":" .. inline_kw end table.insert(inline_parts, inline_kw) elseif cell:sub(1, 1) == "+" then local mod = cell:sub(2) if mod:match("^<.->$") then mod = mod:sub(2, -2) end table.insert(inline_parts, "<<" .. mod .. ">>") else table.insert(inline_parts, "<" .. cell .. ">") end end return table.concat(inline_parts, "") end function EtymonParser.parse_inline_ety(ety_string, context_lang) local run = parse_inline_ety_run(ety_string) local keyword = M.string_utilities.trim(run.keyword) reject_removed_surf_keyword(":" .. keyword) if not is_keyword(keyword, true) then error("Invalid keyword '" .. keyword .. "' in inline etymology <ety:" .. keyword .. "...>") end local args = { context_lang:getCode(), normalize_keyword_param(keyword) } for _, segment in ipairs(run.cells) do if is_keyword(segment, true) then table.insert(args, normalize_keyword_param(segment)) else table.insert(args, segment) end end return args end function EtymonParser.parse_etymon(param, context_lang) if is_keyword(param) then return nil end if type(param) ~= "string" then return nil end local lang, rest local is_family = false local before_bracket = param:match("^([^<]*)") or param local lang_code, rest_match = before_bracket:match("^([a-zA-Z][a-zA-Z0-9._-]*):(.*)$") if lang_code then local potential_lang = Util.get_lang(lang_code, true) if potential_lang then lang = potential_lang rest = param:sub(#lang_code + 2) else local potential_family = Util.get_family(lang_code) if potential_family then lang = potential_family rest = param:sub(#lang_code + 2) is_family = true else lang = context_lang rest = param end end else lang = context_lang rest = param end M.tracking.track_term(rest) if rest == "" or rest == "+" then return { lang = lang, term = nil, unknown_term = true, is_family = is_family, } end if rest == "-" then return { lang = lang, term = nil, suppress_term = true, is_family = is_family, } end if not rest:find("<", 1, true) then return { lang = lang, term = M.string_utilities.trim(rest), is_family = is_family, } end local term_text = rest:match("^([^<]*)") or "" local is_unknown = (term_text == "" or term_text == "+") local is_suppress = (term_text == "-") local function generate_obj(ignored_term) return { term = (is_unknown or is_suppress) and nil or M.string_utilities.trim(term_text) } end local rest_with_defaults = Util.add_boolean_defaults(rest, EtymonParser.etymon_param_mods) local parsed_obj = M.parse_utilities.parse_inline_modifiers(rest_with_defaults, { param_mods = get_clean_param_mods(EtymonParser.etymon_param_mods), generate_obj = generate_obj }) if parsed_obj.id and parsed_obj.id:match("^!") then parsed_obj.id = parsed_obj.id:sub(2) parsed_obj.override = true end parsed_obj.lang = lang parsed_obj.is_family = is_family if is_unknown then parsed_obj.unknown_term = true elseif is_suppress then parsed_obj.suppress_term = true end return parsed_obj end function EtymonParser.validate(lang, args, id, title, pos, starts_with_lang_code) -- id is now optional, so only validate if provided if id then if mw.ustring.len(id) < 2 then error("The `id` parameter must have at least two characters.") end if id == title or id == Util.get_page_data().pagename then error("The `id` parameter must not be the same as the page title.") end end local valid_pos = { prefix = true, suffix = true, interfix = true, infix = true, root = true, word = true } if pos and not valid_pos[pos] then error("Unknown value provided for `pos`. Valid values: " .. table.concat(require("Module:table").keysToList(valid_pos), ", ") .. ".") end local current_keyword = "from" local current_keyword_explicit = false local keyword_etymons = {} local keywords = M.data.keywords local function checkKeyword() local config = keywords[current_keyword] if current_keyword == "from" and not current_keyword_explicit and #keyword_etymons == 0 then keyword_etymons = {} return end EtymonParser.check_etymon_limits(#keyword_etymons, config, "`:" .. current_keyword .. "`") keyword_etymons = {} end local start_index = starts_with_lang_code and 2 or 1 for i = start_index, #args do local param = args[i] if type(param) ~= "string" then elseif param:sub(1, 1) == ":" and not is_keyword(param) then reject_removed_surf_keyword(param) error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?") elseif is_keyword(param) then checkKeyword() current_keyword = get_canonical_keyword(get_keyword(param)) current_keyword_explicit = true else local etymon_data = EtymonParser.parse_etymon(param, lang) if etymon_data then table.insert(keyword_etymons, param) EtymonParser.check_keyword_term(etymon_data, lang, current_keyword) -- Check modifier restrictions EtymonParser.check_modifier_restrictions(etymon_data, current_keyword, EtymonParser.etymon_param_mods) -- postype must be "root" or "word" local VALID_POSTYPES = { root = true, word = true } if etymon_data.postype and not VALID_POSTYPES[etymon_data.postype] then error("Invalid <postype:" .. etymon_data.postype .. ">; must be \"root\" or \"word\".") end if etymon_data.ety then local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang) EtymonParser.validate(etymon_data.lang, inline_args, nil, nil, nil, true) end else table.insert(keyword_etymons, param) end end end checkKeyword() end local DataRetriever = {} local function format_etymon_id_hint(id_data, idx) local id = type(id_data) == "table" and id_data.id or id_data local pos = type(id_data) == "table" and id_data.pos if id and id ~= "" and id ~= "*" then return '"' .. id .. '"' end if pos and pos ~= "" then return "unnamed (|pos=" .. pos .. "|)" end return "etymon #" .. idx .. " (no |id= on page)" end local function etymon_target_page_link(page, norm_lang) return M.links.full_link({ term = page, lang = norm_lang, no_generate_alternants = true, }, "term") end -- Summarize {{etymon}} id slots on a linked page for preview warnings. local function summarize_available_etymon_ids(ids) local id_list = {} local all_idless = true local target_has_idless = false local any_pos = false for i, id_data in ipairs(ids) do local id = type(id_data) == "table" and id_data.id or id_data local pos = type(id_data) == "table" and id_data.pos if id and id ~= "" and id ~= "*" then all_idless = false else target_has_idless = true end if pos and pos ~= "" then any_pos = true end table.insert(id_list, format_etymon_id_hint(id_data, i)) end return { id_list = id_list, all_idless = all_idless, target_has_idless = target_has_idless, any_pos = any_pos, count = #ids, options_text = mw.text.listToText(id_list), } end local function ambiguous_etymon_suggestion(page_link, summary) if summary.all_idless then if summary.any_pos then return " None set `|id=` yet; add a unique `|id=` to each on " .. page_link .. ", then `<id:identifier>` after the term here. Section order / hints: " .. summary.options_text .. "." end return " None set `|id=` yet; add a unique `|id=` to each {{etymon}} in that section from top to bottom, then `<id:identifier>` after the term here (same value as `|id=`)." end return " Specify which one with `<id:identifier>` after the term. Options: " .. summary.options_text .. "." end local function warn_ambiguous_etymon_link(page, norm_lang, ids, is_toplevel) local page_link = etymon_target_page_link(page, norm_lang) local summary = summarize_available_etymon_ids(ids) if is_toplevel and summary.target_has_idless then __state.linked_page_multiple_etymons_idless = true end local lang_name = norm_lang:getCanonicalName() local lead = "Etymology link to " .. page_link .. " is ambiguous (" .. summary.count .. " {{etymon}} templates for " .. lang_name .. ")." Util.add_warning(lead .. ambiguous_etymon_suggestion(page_link, summary), true) end local function is_mismatched_explicit_id(base_key, cached_args, parent_etymon) return cached_args == M.data.STATUS.MISSING and not parent_etymon and #(__state.available_etymon_ids[base_key] or {}) > 0 end local function maybe_flag_partial_etymology_reference(base_key, etymon_data, cached_args, is_toplevel) if not is_toplevel or __state.skip_partial_etymology_category then return end if not __state.partial_etymology_targets[base_key] then return end if etymon_data.id and type(cached_args) == "table" then return end __state.linked_page_partial_etymology_sections = true end local function is_nonlemma_etymon_template(template_args) return template_args and M.yesno(template_args.nl, false) end local function warn_mismatched_explicit_id(page, norm_lang, base_key, etymon_id) local page_link = etymon_target_page_link(page, norm_lang) local summary = summarize_available_etymon_ids(__state.available_etymon_ids[base_key] or {}) local lang_name = norm_lang:getCanonicalName() local lead = "Etymology link to " .. page_link .. " uses `<id:" .. etymon_id .. ">`, but no {{etymon}} on that page has `|id=" .. etymon_id .. "|` for " .. lang_name .. "." Util.add_warning(lead .. " Valid IDs: " .. summary.options_text .. ".", true) end -- Given an etymon data, scrape its page and cache the result in the global state object. function DataRetriever.cache_page_etymons(etymon_page, etymon_title, key, etymon_lang, etymon_id, redirected_from, descendants_is_toplevel) local content = etymon_title:getContent() if not content then __state.cached_etymon_args[key] = M.data.STATUS.REDLINK return end -- Check if the linked page is a redirect. If it is, the template parsing -- code below will be effectively skipped, and `scrape_page` will be called -- again on the redirect target (see the bottom of this function) local lang_section_for_descendants = nil local redirect_target = etymon_title.redirect_target if not redirect_target then content = M.pages.get_section(content, etymon_lang:getFullName(), 2) if not content then __state.cached_etymon_args[key] = M.data.STATUS.MISSING return end lang_section_for_descendants = content end local etymon_lang_code = etymon_lang:getFullCode() local lang_page_key = etymon_lang_code .. ":" .. etymon_page local found_templates_for_lang = {} local found_ids = {} local get_node_class = M.template_parser.class_else_type -- Look for all {{etymon}} templates within the page content using the template parser -- This way the same page is never parsed more than once -- Build a map from senseids to their parent etymonids. local active_etymon_args = nil local etymology_section_count = 0 local etymology_sections_with_etymon = 0 local current_etymology_has_etymon = false local current_etymology_has_nonlemma = false local function finalize_current_etymology_section() if etymology_section_count == 0 then return end if current_etymology_has_etymon or current_etymology_has_nonlemma then etymology_sections_with_etymon = etymology_sections_with_etymon + 1 end current_etymology_has_etymon = false current_etymology_has_nonlemma = false end for node in M.template_parser.parse(content):iterate_nodes() do local node_class = get_node_class(node) if node_class == "heading" then -- A new L2 or etymology section acts as a barrier: an {{etymon}} usage -- used previously cannot be the parent of any subsequent senseids. -- Note that we don't have to check for L2s due to the usage of `M.pages.get_section` above. if node:get_name():find("^Etymology") then finalize_current_etymology_section() etymology_section_count = etymology_section_count + 1 active_etymon_args = nil end elseif node_class == "template" then local template_name = node:get_name() if template_name == "etymon" then local template_args = node:get_arguments() -- Check if this etymon is for our language if template_args[1] == etymon_lang_code then if is_nonlemma_etymon_template(template_args) then if etymology_section_count > 0 then current_etymology_has_nonlemma = true end else if etymology_section_count > 0 then current_etymology_has_etymon = true end table.insert(found_templates_for_lang, template_args) if template_args.id then local etymon_key = lang_page_key .. ":" .. template_args.id __state.cached_etymon_args[etymon_key] = template_args __state.cached_etymon_pages[etymon_key] = tostring(etymon_page) table.insert(found_ids, template_args.id) active_etymon_args = template_args else -- Store idless etymon with default key local etymon_key = lang_page_key .. ":*" __state.cached_etymon_args[etymon_key] = template_args __state.cached_etymon_pages[etymon_key] = tostring(etymon_page) table.insert(found_ids, "*") active_etymon_args = template_args end end end elseif active_etymon_args and template_name == "senseid" then local template_args = node:get_arguments() -- This should always be true for proper usages of {{senseid}}. if template_args[1] == etymon_lang_code and template_args[2] then local sense_id_key = lang_page_key .. ":" .. template_args[2] __state.senseid_parent_etymon[sense_id_key] = active_etymon_args __state.cached_etymon_pages[sense_id_key] = tostring(etymon_page) end end end end finalize_current_etymology_section() if lang_section_for_descendants and etymology_section_count > 1 and etymology_sections_with_etymon > 0 and etymology_sections_with_etymon < etymology_section_count then __state.partial_etymology_targets[lang_page_key] = true end if descendants_is_toplevel and lang_section_for_descendants and #found_templates_for_lang > 0 then M.descendants.cache_page_checks({ lang_section = lang_section_for_descendants, etymon_lang_code = etymon_lang_code, found_templates_for_lang = found_templates_for_lang, entry_title = __state.entry_title, entry_lang_code = __state.entry_lang_code, entry_lang = __state.entry_lang_code and Util.get_lang(__state.entry_lang_code, true) or nil, cached_descendants_checks = __state.cached_descendants_checks, lang_page_key = lang_page_key, redirected_from = redirected_from, }) end local id_data_list = {} for _, args in ipairs(found_templates_for_lang) do local id = args.id or "*" table.insert(id_data_list, { id = id, pos = args.pos }) end __state.available_etymon_ids[lang_page_key] = id_data_list if #found_templates_for_lang == 1 then __state.single_etymons[lang_page_key] = found_templates_for_lang[1] end if redirected_from and __state.available_etymon_ids[lang_page_key] then __state.available_etymon_ids[redirected_from] = __state.available_etymon_ids[redirected_from] or {} for _, id_data in ipairs(__state.available_etymon_ids[lang_page_key]) do table.insert(__state.available_etymon_ids[redirected_from], id_data) end end if __state.cached_etymon_args[key] ~= nil or __state.senseid_parent_etymon[key] ~= nil then -- All done! return elseif redirect_target and not redirected_from then -- Try scraping the redirect. etymon_page = redirect_target.prefixedText DataRetriever.cache_page_etymons(etymon_page, redirect_target, lang_page_key .. ":" .. etymon_id, etymon_lang, etymon_id, lang_page_key, descendants_is_toplevel) __state.cached_etymon_args[key] = __state.cached_etymon_args[etymon_lang_code .. ":" .. etymon_page .. ":" .. etymon_id] else __state.cached_etymon_args[key] = M.data.STATUS.MISSING end end local function has_linkable_term(etymon_data) if etymon_data.is_family or etymon_data.suppress_term or etymon_data.unknown_term then return false end local term = etymon_data.term if term == nil or term == "" then return false end return M.string_utilities.trim(term) ~= "" end local function record_term_id_tracking(etymon_data) if not has_linkable_term(etymon_data) then return end local term_page = M.links.get_link_page(etymon_data.term, etymon_data.lang) M.tracking.record_term_id_usage(__state.id_stats, etymon_data, term_page) end -- Given an etymon object, scrape its page (if necessary) and return its own etymon arguments as well as the page name. function DataRetriever.get_etymon_args(etymon_data, is_toplevel) if not has_linkable_term(etymon_data) then return M.data.STATUS.MISSING, nil, nil, nil end local page = M.links.get_link_page(etymon_data.term, etymon_data.lang) local norm_lang = Util.get_norm_lang(etymon_data.lang) local base_key = norm_lang:getFullCode() .. ":" .. page if etymon_data.id then local key = base_key .. ":" .. etymon_data.id local cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key] if cached_args == nil then local title = mw.title.new(page) if not title then error('Invalid page title "' .. page .. '" encountered.') end DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, is_toplevel) end cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key] -- refresh -- Get etymon_id from parent if this was resolved via senseid local parent_etymon = __state.senseid_parent_etymon[key] local resolved_etymon_id = parent_etymon and parent_etymon.id local descendants_check = M.descendants.get_lookup_check({ cached_descendants_checks = __state.cached_descendants_checks, is_toplevel = is_toplevel, base_key = base_key, lookup = { explicit_id = etymon_data.id, parent_etymon = parent_etymon, }, }) if is_toplevel and descendants_check == nil then local title = mw.title.new(page) if title then DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, true) descendants_check = M.descendants.get_lookup_check({ cached_descendants_checks = __state.cached_descendants_checks, is_toplevel = true, base_key = base_key, lookup = { explicit_id = etymon_data.id, parent_etymon = parent_etymon, }, }) end end local mismatched_id = is_mismatched_explicit_id(base_key, cached_args, parent_etymon) if mismatched_id and is_toplevel then __state.has_mismatched_id = true M.tracking.record_mismatched_id_usage(__state.id_stats, norm_lang, page, etymon_data.id) warn_mismatched_explicit_id(page, norm_lang, base_key, etymon_data.id) end maybe_flag_partial_etymology_reference(base_key, etymon_data, cached_args, is_toplevel) return cached_args, __state.cached_etymon_pages[key], resolved_etymon_id, descendants_check else __state.used_idless_etymon = true if is_toplevel then __state.toplevel_idless_etymon = true end if __state.available_etymon_ids[base_key] == nil then local title = mw.title.new(page) if not title then error('Invalid page title "' .. page .. '" encountered.') end DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, is_toplevel) end local ids = __state.available_etymon_ids[base_key] or {} local count = #ids -- Try to filter by postype if available and we have multiple candidates if count > 1 and etymon_data.postype then local matching_ids = {} for _, id_data in ipairs(ids) do if id_data.pos == etymon_data.postype then table.insert(matching_ids, id_data) end end if #matching_ids == 1 then local matched_id = matching_ids[1].id local matched_key = base_key .. ":" .. matched_id M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "postype") local descendants_check = M.descendants.get_lookup_check({ cached_descendants_checks = __state.cached_descendants_checks, is_toplevel = is_toplevel, base_key = base_key, lookup = { id = matched_id }, }) if is_toplevel and descendants_check == nil then local title = mw.title.new(page) if title then DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true) descendants_check = M.descendants.get_lookup_check({ cached_descendants_checks = __state.cached_descendants_checks, is_toplevel = true, base_key = base_key, lookup = { id = matched_id }, }) end end local matched_args = __state.cached_etymon_args[matched_key] maybe_flag_partial_etymology_reference(base_key, etymon_data, matched_args, is_toplevel) return matched_args, __state.cached_etymon_pages[matched_key], nil, descendants_check end end if count == 1 then local only_id_data = ids[1] local only_id = (type(only_id_data) == "table" and only_id_data.id) or only_id_data or "*" M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "single") local descendants_check = M.descendants.get_lookup_check({ cached_descendants_checks = __state.cached_descendants_checks, is_toplevel = is_toplevel, base_key = base_key, lookup = { id_data = only_id_data }, }) if is_toplevel and descendants_check == nil then local title = mw.title.new(page) if title then DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true) descendants_check = M.descendants.get_lookup_check({ cached_descendants_checks = __state.cached_descendants_checks, is_toplevel = true, base_key = base_key, lookup = { id_data = only_id_data }, }) end end local single_args = __state.single_etymons[base_key] maybe_flag_partial_etymology_reference(base_key, etymon_data, single_args, is_toplevel) return single_args, __state.cached_etymon_pages[base_key .. ":" .. only_id], nil, descendants_check elseif count > 1 then M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "ambiguous") warn_ambiguous_etymon_link(page, norm_lang, ids, is_toplevel) maybe_flag_partial_etymology_reference(base_key, etymon_data, M.data.STATUS.AMBIGUOUS, is_toplevel) return M.data.STATUS.AMBIGUOUS, nil, nil, nil else M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "missing") maybe_flag_partial_etymology_reference(base_key, etymon_data, M.data.STATUS.MISSING, is_toplevel) return M.data.STATUS.MISSING, nil, nil, nil end end end local function keyword_invisible_in_tree(keyword_info) if not keyword_info then return false end local inv = keyword_info.invisible return inv == "all" or inv == true or inv == "tree" end -- True when the node has at least one top-level child container visible in the tree. local function node_has_visible_tree_children(node) for _, container in ipairs(node.children or {}) do if not keyword_invisible_in_tree(container.keyword_info) then return true end end return false end -- Count visible term nodes in the tree. local function get_visible_tree_depth(node, skip_child_rendering) local max_depth = 1 if skip_child_rendering or not node then return max_depth end for _, container in ipairs(node.children or {}) do local keyword_info = container.keyword_info if not keyword_invisible_in_tree(keyword_info) then local skip_grandchildren = keyword_info and keyword_info.no_child_categories for _, term in ipairs(container.terms or {}) do if term.is_duplicate then if term.original_has_children then max_depth = math.max(max_depth, 2) end else max_depth = math.max(max_depth, 1 + get_visible_tree_depth(term, skip_grandchildren)) end end end end return max_depth end local function as_param_list(val) if val == nil then return {} end if type(val) == "table" then return val end if type(val) == "string" and val ~= "" then return { val } end return {} end local TreeBuilder = {} -- Build a unique key for deduplication in the seen table function TreeBuilder.build_key(lang, title, args) local norm_lang_code = Util.get_norm_lang(lang):getFullCode() local is_table = type(args) == "table" local id = (is_table and args.id) or "" if title then return norm_lang_code .. ":" .. M.links.get_link_page(title, lang) .. ":" .. id end if is_table and args.status == M.data.STATUS.INLINE then local content_parts = {} for i = 1, #args do content_parts[i] = tostring(args[i]) end return norm_lang_code .. ":*:" .. id .. "\0" .. table.concat(content_parts, "\0") end return norm_lang_code .. ":*:" .. id end -- Copy parsed etymon modifiers onto a tree/supplement term node. function TreeBuilder.apply_etymon_fields(term, etymon_data) term.id = etymon_data.id term.gloss = etymon_data.gloss term.tr = etymon_data.tr term.ts = etymon_data.ts term.alt = etymon_data.alt term.genders = etymon_data.genders term.pos = etymon_data.pos term.ng = etymon_data.ng term.infl = etymon_data.infl term.refs = etymon_data.refs term.is_uncertain = etymon_data.unc term.lit = etymon_data.lit term.q = etymon_data.q term.qq = etymon_data.qq term.l = etymon_data.l term.ll = etymon_data.ll term.suppress_term = etymon_data.suppress_term term.unknown_term = etymon_data.unknown_term term.is_family = etymon_data.is_family term.override = etymon_data.override term.aftype = etymon_data.aftype term.postype = etymon_data.postype term.bor = etymon_data.bor term.lbor = etymon_data.lbor term.slbor = etymon_data.slbor end function TreeBuilder.build_supplement_term(etymon_data, entry_lang, supplement_type) EtymonParser.check_supplement_term(etymon_data, entry_lang, supplement_type) local term = { lang = etymon_data.lang, title = etymon_data.term, children = {}, status = M.data.STATUS.OK, } TreeBuilder.apply_etymon_fields(term, etymon_data) return term end function TreeBuilder.build_supplement_terms(entry_lang, supplement_type, param_value) local terms = {} for _, term_param in ipairs(as_param_list(param_value)) do if type(term_param) == "string" and term_param ~= "" then local etymon_data = EtymonParser.parse_etymon(term_param, entry_lang) if etymon_data then table.insert(terms, TreeBuilder.build_supplement_term(etymon_data, entry_lang, supplement_type)) end end end return terms end -- Attach a |param= supplement defined in etymon_data.supplements (e.g. doublet=). function TreeBuilder.append_term_supplement(data_tree, entry_lang, supplement_type, param_value) local config = M.data.supplements[supplement_type] if not config then error("Unknown supplement '" .. tostring(supplement_type) .. "'.") end local terms = TreeBuilder.build_supplement_terms(entry_lang, supplement_type, param_value) if #terms == 0 then return end data_tree.supplements = data_tree.supplements or {} table.insert(data_tree.supplements, { type = supplement_type, config = config, terms = terms, }) M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, supplement_type, entry_lang, entry_lang, true) end function TreeBuilder.build(lang, title, args, seen, depth, stop_recursion) seen = seen or {} depth = depth or 0 local is_toplevel = (depth == 0) if depth > __state.max_depth_reached then __state.max_depth_reached = depth end __state.total_nodes = __state.total_nodes + 1 local lang_code = lang:getCode() __state.language_count[lang_code] = (__state.language_count[lang_code] or 0) + 1 local current_id = (type(args) == "table" and args.id) or "" local key = TreeBuilder.build_key(lang, title, args) local node = { lang = lang, title = title, id = current_id, args = args, children = {}, status = M.data.STATUS.OK } if type(args) ~= "table" or seen[key] then node.status = args or M.data.STATUS.MISSING -- Mark as duplicate if we've seen this node before if seen[key] then node.is_duplicate = true node.duplicate_key = key local original_node = seen[key] if type(original_node) == "table" and original_node.children and #original_node.children > 0 then node.original_has_children = true end end return node end node.status = args.status or M.data.STATUS.OK seen[key] = node -- If stop_recursion is set, skip parsing children but check for visible children if stop_recursion then local keywords = M.data.keywords local has_visible_children = false for i = 2, #args do local param = args[i] if type(param) == "string" then local keyword_base = get_keyword_base(param) if keyword_base and keywords[keyword_base] then local _, kw_modifiers = EtymonParser.parse_keyword_modifiers(param:sub(1, 1) == ":" and param or (":" .. param)) if not keyword_invisible_in_tree(get_effective_keyword_info(keyword_base, kw_modifiers)) then has_visible_children = true break end elseif param:sub(1, 1) ~= ":" then -- It's a term (not a keyword), so there are visible children has_visible_children = true break end end end node.has_visible_children = has_visible_children return node end -- Parse args into keyword containers local current_keyword = "from" local current_keyword_modifiers = {} local current_container = nil local function ensure_container() if not current_container or current_container.keyword ~= current_keyword then local keyword_info = get_effective_keyword_info(current_keyword, current_keyword_modifiers) current_container = { keyword = current_keyword, keyword_info = keyword_info, keyword_modifiers = current_keyword_modifiers, terms = {}, } table.insert(node.children, current_container) -- Override keyword text/phrase for nominalization with <g:code> if current_keyword_modifiers.g and current_keyword == "nominalization" then local labels = get_nominalization_label_for_g(current_keyword_modifiers.g) if not labels then local codes = {} for c in pairs(M.data.nominalization_g_codes) do table.insert(codes, c) end table.sort(codes) error("Invalid <g:" .. tostring(current_keyword_modifiers.g) .. ">. Supported codes for nominalization: " .. table.concat(codes, ", ")) end current_container.keyword_info = copy_keyword_info(keyword_info) current_container.keyword_info.text = labels.text current_container.keyword_info.phrase = labels.phrase end end return current_container end local parse_context_lang = Util.resolve_context_lang(lang, args) for i = 2, #args do local param = args[i] if is_keyword(param) then local keyword, modifiers = EtymonParser.parse_keyword_modifiers(param) if not keyword then error("Invalid keyword '" .. param .. "'.") end current_keyword = keyword current_keyword_modifiers = modifiers current_container = nil -- Force new container for new keyword elseif type(param) == "string" and param:sub(1, 1) == ":" then reject_removed_surf_keyword(param) error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?") elseif type(param) == "string" then local etymon_data = EtymonParser.parse_etymon(param, parse_context_lang) if etymon_data then -- Track keyword usage at top level M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, current_keyword, lang, etymon_data.lang, is_toplevel) local term_node = {} local container -- Handle suppress_term (-) and unknown_term (empty or +) directly if etymon_data.suppress_term or etymon_data.unknown_term then container = ensure_container() if etymon_data.ety then local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang) inline_args.id = etymon_data.id inline_args.status = M.data.STATUS.INLINE term_node = TreeBuilder.build(etymon_data.lang, nil, inline_args, seen, depth + 1) else term_node = { lang = etymon_data.lang, children = {}, status = M.data.STATUS.OK, } end TreeBuilder.apply_etymon_fields(term_node, etymon_data) else -- Regular term: fetch arguments from page record_term_id_tracking(etymon_data) local etymon_args, page_of, resolved_etymon_id, descendants_check = DataRetriever.get_etymon_args(etymon_data, is_toplevel) -- Check for <ety> inline parameter doesn't override the scraped arguments, unless the latter are missing if etymon_data.ety then if etymon_args == M.data.STATUS.REDLINK or etymon_args == M.data.STATUS.MISSING then __state.current_page_has_inline_etymology = true if is_toplevel then __state.toplevel_has_inline_etymology = true end local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang) -- Track inline ety keywords too local inline_keyword = get_keyword(inline_args[2], true) if inline_keyword and #inline_args >= 3 then local inline_etymon = EtymonParser.parse_etymon(inline_args[3], etymon_data.lang) if inline_etymon then M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, inline_keyword, etymon_data.lang, inline_etymon.lang, is_toplevel) end end inline_args.id = etymon_data.id inline_args.status = M.data.STATUS.INLINE etymon_args = inline_args term_node.page_of = __state.cached_etymon_pages[key] -- term node is on the same page as the parent else -- Scraped arguments exist, <ety> is redundant and ignored __state.current_page_has_redundant_etymology = true if is_toplevel then __state.toplevel_redundant_etymology = true end end end -- Ensure container exists before checking keyword info container = ensure_container() -- Check if current keyword has no_child_categories - if so, stop recursion local keyword_info = container.keyword_info local should_stop_recursion = (stop_recursion or (keyword_info and keyword_info.no_child_categories)) term_node = TreeBuilder.build(etymon_data.lang, etymon_data.term, etymon_args, seen, depth + 1, should_stop_recursion) term_node.target_key = Util.get_norm_lang(etymon_data.lang):getFullCode() .. ":" .. M.links.get_link_page(etymon_data.term, etymon_data.lang) term_node.etymon_id = resolved_etymon_id -- The actual etymon id when resolved via senseid term_node.page_of = page_of TreeBuilder.apply_etymon_fields(term_node, etymon_data) term_node.missing_descendants_header, term_node.missing_descendants_entry = M.descendants.get_term_sync_flags(current_keyword, term_node.status, descendants_check) end table.insert(container.terms, term_node) end end end return node end -- Convert etymology tree to JSON-serializable table local function tree_to_json(node) local obj = { term = node.title, lang = node.lang:getCode(), lang_name = node.lang:getCanonicalName(), id = (node.id and node.id ~= "") and node.id or nil, status = node.status, is_uncertain = node.is_uncertain or nil, is_duplicate = node.is_duplicate or nil, gloss = node.gloss, transliteration = node.tr, transcription = node.ts, alt = node.alt, g = node.genders, pos = node.pos, ng = node.ng, infl = node.infl, children = {}, } for _, container in ipairs(node.children or {}) do local keyword_info = container.keyword_info if keyword_info then local container_obj = { keyword = container.keyword, keyword_label = keyword_info.text, keyword_abbrev = keyword_info.abbrev, is_group = keyword_info.is_group or nil, is_invisible = keyword_info.invisible or nil, is_uncertain = (container.keyword_modifiers and container.keyword_modifiers.unc) or nil, terms = {}, } for _, term in ipairs(container.terms or {}) do table.insert(container_obj.terms, tree_to_json(term)) end table.insert(obj.children, container_obj) end end return obj end -- Build and return the etymology data tree for a given term. function export.get_tree(lang, title, args, options) options = options or {} __state.entry_title = title __state.entry_lang_code = lang:getCode() __state.id_stats = M.tracking.new_id_stats() __state.skip_partial_etymology_category = options.skip_partial_etymology_category == true if options.validate then EtymonParser.validate(lang, args, options.id, title, options.pos, false) end local lang_code = lang:getCode() local start_index = (args[1] == lang_code) and 2 or 1 local tree_args = { [1] = lang_code, id = options.id or args.id } for i = start_index, #args do table.insert(tree_args, args[i]) end __state.cached_etymon_args[lang_code .. ":" .. title .. ":" .. (tree_args.id or "")] = tree_args local ety_data_tree = TreeBuilder.build(lang, title, tree_args) if options.json then return M.JSON.toJSON(tree_to_json(ety_data_tree)) end return ety_data_tree end -- Given a language code, page name and optionally the id= parameter, -- render the tree and only the etymology tree for the relevant page. -- Fetches and parses the corresponding {{etymon}} from the requested page, -- and any further pages needed to render the tree. -- Parameters can be passed either through the #invoke or as -- template parameters *through* an #invoke. function export.render_tree_for_etymon_on_page(frame) local frame_args = frame.args local parent_args = frame:getParent().args local langcode = frame_args[1] or parent_args[1] local pagename = frame_args[2] or parent_args[2] local id = frame_args["id"] or parent_args["id"] local display_title = frame_args["title"] or parent_args["title"] local parsed_title = mw.title.new(pagename, 0) local title if parsed_title.namespace == 0 then title = M.pages.safe_page_name(parsed_title) elseif parsed_title.namespace == 118 then title = "*" .. M.pages.safe_page_name(parsed_title) else error("Unsupported namespace for render_tree_for_etymon_on_page: " .. parsed_title.namespace) end local lang = Util.get_lang(langcode) __state.entry_title = title __state.entry_lang_code = lang:getCode() __state.id_stats = M.tracking.new_id_stats() -- Construct etymon_data for DataRetriever.get_args. local etymon_data = { lang = lang, term = title, id = id } local args, pagename = DataRetriever.get_etymon_args(etymon_data, true) if args == M.data.STATUS.MISSING then error("The etymon template was not found (language " .. langcode .. ", title '" .. title .. "'" .. (id and ", ID '" .. id .. "'" or ", no ID given") .. "). Page contents may have changed in the interim.") end local tree_title = display_title or title if lang:stripDiacritics(M.links.remove_links(tree_title)) ~= lang:stripDiacritics(M.links.remove_links(title)) then M.tracking.track_title_pagename_mismatch(lang) end reset_invocation_state() local ety_data_tree = export.get_tree(lang, tree_title, args, { validate = true, id = id, }) local output = {} table.insert(output, M.template_styles("Module:etymon/styles.css")) table.insert(output, M.tree.render({ data_tree = ety_data_tree, format_term_func = function(term, is_toplevel) return Util.format_term(term, is_toplevel, { gloss = "suppress", pos = "suppress", lit = "suppress", tree_ql = "suppress", }) end, })) return table.concat(output) end function export.main(frame) local parent_args = frame:getParent().args local args = M.parameters.process(parent_args, M.parameters_data.etymon) local lang = args[1] local etymon_args = args[2] local id = args.id local title = args.title local text = args.text local tree = args.tree local etydate = args.etydate local doublet = args.doublet local rfe = args.rfe local etystub = args.etystub local is_nonlemma = M.yesno(args.nl, false) local page_data = Util.get_page_data() if not title then title = page_data.pagename if page_data.namespace == "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 = M.parameter_utilities.construct_param_mods { { group = "ref" }, { param = "refn" }, { param = "nocap", type = "boolean" }, } local function generate_etydate_obj(etydate_text) local etydate_specs = {} for spec in etydate_text:gmatch("[^,]+") do table.insert(etydate_specs, mw.text.trim(spec)) end return { [1] = etydate_specs } end local parsed_etydate = M.parse_utilities.parse_inline_modifiers(etydate, { param_mods = etydate_param_mods, generate_obj = generate_etydate_obj }) local etydate_args = { [1] = parsed_etydate[1], nocap = parsed_etydate.nocap or false, } ety_data_tree.supplements = ety_data_tree.supplements or {} table.insert(ety_data_tree.supplements, { type = "etydate", etydate_text = M.etydate.format_etydate(etydate_args, { omit_refs = true }), etydate_refs = (parsed_etydate.refs and #parsed_etydate.refs > 0) and parsed_etydate.refs or nil, }) end TreeBuilder.append_term_supplement(ety_data_tree, lang, "doublet", doublet) local has_visible_children = node_has_visible_tree_children(ety_data_tree) -- Suppress trees for multiword entries and one-step chains local visible_tree_depth = get_visible_tree_depth(ety_data_tree) local is_trivial_tree = visible_tree_depth <= 2 local is_multiword = title:find("%s") ~= nil or title:find("_") ~= nil if tree and (is_multiword or is_trivial_tree) then tree = false end if tree then table.insert(output, M.template_styles("Module:etymon/styles.css")) table.insert(output, M.tree.render({ data_tree = ety_data_tree, format_term_func = function(term, is_toplevel) return Util.format_term(term, is_toplevel, { gloss = "suppress", pos = "suppress", lit = "suppress", tree_ql = "suppress", }) end, })) end local tree_disallowed = lang_exc and lang_exc.disallow and lang_exc.disallow.tree local ety_tree_json = M.JSON.toJSON(tree_to_json(ety_data_tree)) local anchor = M.anchors.etymonid(lang, id, { no_tree = args.notree, title = title, empty_tree = (not has_visible_children) or tree_disallowed, ety_tree_json = ety_tree_json, }) table.insert(output, anchor) local text_stop_lang_missing = nil if text then local max_depth, stop_at_blue_link, stop_at_lang, stop_at_lang_or_bluelink if text == "++" then max_depth, stop_at_blue_link = false, false elseif text == "+" then max_depth, stop_at_blue_link = 1, false elseif text == "*" then max_depth, stop_at_blue_link = false, true elseif text:match("^:[^*]+%*$") then -- Stop at a specific language OR first bluelink after it, e.g., ":ota*" -- If the target language is a redlink, continue to the first bluelink local lang_code = text:match("^:([^*]+)%*$") if lang_code and lang_code ~= "" then local lang_obj = Util.get_lang(lang_code, true) if lang_obj then stop_at_lang_or_bluelink = lang_code else Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.') max_depth, stop_at_blue_link = false, false end else Util.add_warning('Empty language code in text parameter. Showing full chain instead.') max_depth, stop_at_blue_link = false, false end elseif text:sub(1, 1) == ":" then -- Stop at a specific language, e.g., ":ar" stops at first Arabic term local lang_code = text:sub(2) if lang_code ~= "" then -- Validate the language code local lang_obj = Util.get_lang(lang_code, true) if lang_obj then stop_at_lang = lang_code else Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.') max_depth, stop_at_blue_link = false, false -- default to ++ end else Util.add_warning('Empty language code in text parameter. Showing full chain instead.') max_depth, stop_at_blue_link = false, false -- default to ++ end else local num = tonumber(text) if num and num >= 1 then max_depth, stop_at_blue_link = num, false else error('Invalid text value "' .. text .. '". Valid values are: "++" (full chain), "+" (first step only), "*" (until first blue link), a number (max steps), ":lang" (stop at language), or ":lang*" (stop at language or first bluelink if redlink)') end end local text_output, text_render_meta = M.text.render({ data_tree = ety_data_tree, format_term_func = Util.format_term, lang_matches_stop_code = Util.lang_matches_stop_code, max_depth = max_depth, stop_at_blue_link = stop_at_blue_link, curr_page = page_data.pagename, nodot = args.nodot, dot = args.dot, stop_at_lang = stop_at_lang, stop_at_lang_or_bluelink = stop_at_lang_or_bluelink, }) table.insert(output, text_output) if stop_at_lang and text_render_meta and not text_render_meta.stop_lang_reached then M.tracking.track_text_stop_lang_missing(lang, stop_at_lang) text_stop_lang_missing = stop_at_lang end end if rfe then table.insert(output, Util.expand_request_template(frame, "rfe", rfe, lang:getCode())) end if etystub then table.insert(output, Util.expand_request_template(frame, "etystub", etystub, lang:getCode())) end if is_nonlemma then table.insert(output, " " .. frame:expandTemplate({ title = "nonlemma", args = {}, })) end local categories = {} if Util.is_content_page() then M.tracking.track_tree_metrics({ max_depth_reached = __state.max_depth_reached, total_nodes = __state.total_nodes, language_count = __state.language_count, lang = lang, }) categories = M.categories.build({ data_tree = ety_data_tree, page_lang = lang, available_etymon_ids = __state.available_etymon_ids, senseid_parent_etymon = __state.senseid_parent_etymon, get_norm_lang_func = Util.get_norm_lang, lang_exc = lang_exc, suppress_categories = lang_exc and lang_exc.suppress_categories, nocat = args.nocat, tree = tree, text = text, exnihilo = args.exnihilo, toplevel_has_inline_etymology = __state.toplevel_has_inline_etymology, toplevel_redundant_etymology = __state.toplevel_redundant_etymology, toplevel_idless_etymon = __state.toplevel_idless_etymon, has_mismatched_id = __state.has_mismatched_id, linked_page_multiple_etymons_idless = __state.linked_page_multiple_etymons_idless, linked_page_partial_etymology_sections = __state.linked_page_partial_etymology_sections, text_stop_lang_missing = text_stop_lang_missing, }) M.tracking.track_keywords(__state.toplevel_keyword_stats, lang) M.tracking.track_page_id(lang, id) M.tracking.track_ids(__state.id_stats, lang) end if #categories > 0 then table.insert(output, M.categories.format(categories, lang)) end if __state.warnings then for i, warning in ipairs(__state.warnings) do table.insert(output, (i == 1 and "\n" or "") .. warning .. "\n") end end return table.concat(output) end return export nihqsrccbms2zp66z9nzufncmtjq7oa Module:etymon/text 828 38868 178068 2026-09-25T00:40:40Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 copied over (oldid 92736183) 178068 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 entry_lang = data_tree.lang local function lowercase_glossary_link_display(wikitext) return wikitext:gsub("(%[%[Appendix:Glossary#[^|]+|)([^%]])([^%]]*)%]%]", 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 local function get_term_refs(term, defined_here) if defined_here and term.refs and next(term.refs) then return M.references.format_references(term.refs) end return "" end -- Build a text part for a single term local function build_term_part(term, current_lang, defined_here) 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, defined_here), 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 local defined_here = (node.page_of or curr_page) == curr_page -- Get keyword references local keyword_refs = "" if defined_here and keyword_modifiers.refs and next(keyword_modifiers.refs) then keyword_refs = M.references.format_references(keyword_modifiers.refs) 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, defined_here) 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, true) 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 55fpa9rl4jqvgxk9ggp38excqfa50nk Module:etymon/tree 828 38869 178069 2026-09-25T00:41:46Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 copied over (oldid 91079116) 178069 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 = '[[Appendix:Glossary#' .. 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 ok2w6x96bozaxhyj2cy4qex9anfbtx5 Module:etymon/styles.css 828 38870 178076 2026-09-25T02:57:05Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 Nilikha ang pahina na may '/* 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;...' 178076 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 178077 178076 2026-09-25T02:57:49Z ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ 5686 Inilipat ni ᜏᜒᜃᜈᜒᜄᜐᜒᜈᜃ᜕ ang pahinang [[Module:etymon/style.css]] sa [[Module:etymon/styles.css]] nang walang iniwang redirect: Misspelled title 178076 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