Module:category tree/poscatboiler
- The following documentation is located at Module:category tree/poscatboiler/documentation. [edit]
- Useful links: root page • root page’s subpages • links • transclusions • testcases • sandbox
This module implements the poscatboiler category tree system, which generates the descriptions and categorization for all category pages on Wiktionary (other than those with manual wikicoding, which should be converted to use the category tree modules). For historical reasons, there is a separation between the generic category tree code in Module:category tree and a single implementation in Module:category tree/poscatboiler. The plan is to merge the two into a single category tree module.
For more information, including an introduction to the category tree system and a description of how to add or modify categories, see Module:category tree/data/documentation.
The data that specifies how particular categories are handled is contained in submodules, which are directly under Module:category tree. The module Module:category tree/data lists all the submodules.
SubpagesSubpages
- affixes and compounds
- characters
- data
- data/documentation
- documentation
- entry maintenance
- etymology
- families
- family-data
- figures of speech
- grammatical classes
- hierarchy
- hierarchy/documentation
- lang
- lang-specific-raw
- lang/acm
- lang/acw
- lang/acy
- lang/aeb
- lang/afb
- lang/ajp
- lang/akk
- lang/ang
- lang/apc
- lang/apd
- lang/ar
- lang/arn
- lang/ary
- lang/arz
- lang/ayl
- lang/az
- lang/bcl
- lang/be
- lang/bg
- lang/bku
- lang/ca
- lang/cbk
- lang/ceb
- lang/cpi
- lang/cs
- lang/csb
- lang/cu
- lang/de
- lang/documentation
- lang/el
- lang/en
- lang/enm
- lang/eo
- lang/es
- lang/et
- lang/eu
- lang/fa
- lang/fax
- lang/fi
- lang/fr
- lang/fy
- lang/gl
- lang/gmh
- lang/gmw-pro
- lang/gn
- lang/goh
- lang/got
- lang/grk-pro
- lang/gu
- lang/he
- lang/hi
- lang/hil
- lang/hnn
- lang/hrx
- lang/hsb
- lang/hu
- lang/id
- lang/ilo
- lang/inc-ash
- lang/ine-pro
- lang/ira-pro
- lang/is
- lang/it
- lang/ja
- lang/jpx
- lang/jv
- lang/klj
- lang/kn
- lang/kne
- lang/krj
- lang/la
- lang/lo
- lang/mdh
- lang/mk
- lang/moh
- lang/mr
- lang/mrw
- lang/ms
- lang/mt
- lang/mul
- lang/mvi
- lang/nan-hbl
- lang/nb
- lang/ne
- lang/nl
- lang/nn
- lang/non
- lang/ny
- lang/odt
- lang/orv
- lang/pag
- lang/pam
- lang/phl
- lang/pi
- lang/pl
- lang/pra
- lang/pt
- lang/qfa-kor
- lang/ro
- lang/rsk
- lang/ru
- lang/rue
- lang/sa
- lang/sc
- lang/sei
- lang/sem-arb
- lang/sga
- lang/shn
- lang/shu
- lang/sk
- lang/skr
- lang/sw
- lang/szl
- lang/te
- lang/tg
- lang/th
- lang/tl
- lang/tpw
- lang/tsg
- lang/uk
- lang/ur
- lang/vec
- lang/vep
- lang/vi
- lang/war
- lang/yrl
- lang/zhx
- lang/zle-ono
- lang/zlw-ocs
- languages
- lects
- lemmas
- lexical properties
- miscellaneous
- modules
- names
- non-lemma forms
- phrases
- poscatboiler
- poscatboiler/documentation
- pragmatic properties
- rhymes
- scripts
- scripts/blocks
- scripts/blocks/documentation
- semantic classes
- shortenings
- speech acts
- styles.css
- symbols
- templates
- terms by script
- topic
- topic/Animals
- topic/Body
- topic/Buildings and structures
- topic/Communication
- topic/Culture
- topic/Design
- topic/Food and drink
- topic/Games
- topic/History
- topic/Human
- topic/Lifeforms
- topic/Mathematics
- topic/Miscellaneous
- topic/Music
- topic/Names
- topic/Nature
- topic/Numbers
- topic/People
- topic/Philosophy
- topic/Physical actions
- topic/Places
- topic/Plants
- topic/Religion
- topic/Sciences
- topic/Sex
- topic/Society
- topic/Sports
- topic/Technology
- topic/Thesaurus
- topic/Time
- topic/Transport
- topic/data
- topic/documentation
- topic/hierarchy
- topic/hierarchy/documentation
- topic/thesaurus data
- topic/thesaurus data/documentation
- topic/utilities
- transliterations
- unicode
- utilities
- wiktionary maintenance
- wiktionary users
- word of the day
local lang_independent_data = require("Module:category tree/data")
local lang_specific_module = "Module:category tree/lang"
local lang_specific_module_prefix = lang_specific_module .. "/"
local labels_utilities_module = "Module:labels/utilities"
local template_parser_module = "Module:template parser"
local concat = table.concat
local dump = mw.dumpObject
local insert = table.insert
local is_callable = require("Module:fun").is_callable
local lcfirst = require("Module:string utilities").lcfirst
local list_to_set = require("Module:table").listToSet
local make_title = mw.title.makeTitle
local new_title = mw.title.new
local parse = require(template_parser_module).parse
local sparse_concat = require("Module:table").sparseConcat
local tostring = tostring
local type = type
local ucfirst = require("Module:string utilities").ucfirst
local uupper = require("Module:string utilities").upper
local function get_lang(...)
local _get_lang = require("Module:languages").getByCode
function get_lang(...)
return _get_lang(...) or require("Module:languages/errorGetBy").code(...)
end
return get_lang(...)
end
local function get_script(...)
local _get_script = require("Module:scripts").getByCode
function get_script(code)
return _get_script(code) or require("Module:languages/error")(code, true, "script code")
end
return get_script(...)
end
-- Category object
local Category = {}
Category.__index = Category
function Category:get_originating_info()
local originating_info = ""
if self._info.originating_label then
originating_info = " (originating from label \"" .. self._info.originating_label .. "\" in module [[" .. self._info.originating_module .. "]])"
end
return originating_info
end
local valid_keys = list_to_set{"code", "label", "sc", "raw", "args", "also", "called_from_inside", "originating_label", "originating_module"}
function Category.new(info)
for key in pairs(info) do
if not valid_keys[key] then
error("The parameter \"" .. key .. "\" was not recognized.")
end
end
local self = setmetatable({}, Category)
self._info = info
if not self._info.label then
error("No label was specified.")
end
self:initCommon()
if not self._data then
error("The " .. (self._info.raw and "raw " or "") .. "label \"" .. self._info.label .. "\" does not exist" .. self:get_originating_info() .. ".")
end
return self
end
function Category:initCommon()
local args_handled = false
if self._info.raw then
-- Check if the category exists
local raw_categories = lang_independent_data["RAW_CATEGORIES"]
self._data = raw_categories[self._info.label]
if self._data then
if self._data.lang then
self._lang = get_lang(self._data.lang)
self._info.code = self._lang:getCode()
end
if self._data.sc then
self._sc = get_script(self._data.sc)
self._info.sc = self._sc:getCode()
end
else
-- Go through raw handlers
local data = {
category = self._info.label,
args = self._info.args or {},
called_from_inside = self._info.called_from_inside,
}
for _, handler in ipairs(lang_independent_data["RAW_HANDLERS"]) do
self._data, args_handled = handler.handler(data)
if self._data then
self._data.module = self._data.module or handler.module
break
end
end
if self._data then
-- Update the label if the handler specified a canonical name for it.
if self._data.canonical_name then
self._info.canonical_name = self._data.canonical_name
end
if self._data.lang then
if type(self._data.lang) ~= "string" then
error("Received non-string value " .. dump(self._data.lang) .. " for self._data.lang, label \"" .. self._info.label .. "\"" .. self:get_originating_info() .. ".")
end
self._lang = get_lang(self._data.lang)
self._info.code = self._lang:getCode()
end
if self._data.sc then
if type(self._data.sc) ~= "string" then
error("Received non-string value " .. dump(self._data.sc) .. " for self._data.sc, label \"" .. self._info.label .. "\"" .. self:get_originating_info() .. ".")
end
self._sc = get_script(self._data.sc)
self._info.sc = self._sc:getCode()
end
end
end
else
-- Already parsed into language + label
if self._info.code then
self._lang = get_lang(self._info.code)
else
self._lang = nil
end
if self._info.sc then
self._sc = get_script(self._info.sc)
else
self._sc = nil
end
self._info.orig_label = self._info.label
if not self._lang then
-- Umbrella categories without a preceding language always begin with a capital letter, but the actual label may be
-- lowercase (cf. [[:Category:Nouns by language]] with label 'nouns' with per-language [[:Category:English nouns]];
-- but [[:Category:Reddit slang by language]] with label 'Reddit slang' with per-language
-- [[:Category:English Reddit slang]]). Since the label is almost always lowercase, we lowercase it for umbrella
-- categories, storing the original into `orig_label`, and correct it later if needed.
self._info.label = lcfirst(self._info.label)
end
-- First, check lang-specific labels and handlers if this is not an umbrella category.
if self._lang then
local langs_with_modules = require(lang_specific_module)
local obj, seen = self._lang, {}
repeat
if langs_with_modules[obj:getCode()] then
local module = lang_specific_module_prefix .. obj:getCode()
local labels_and_handlers = require(module)
if labels_and_handlers.LABELS then
self._data = labels_and_handlers.LABELS[self._info.label]
if self._data then
if self._data.umbrella == nil and self._data.umbrella_parents == nil then
self._data.umbrella = false
end
self._data.module = self._data.module or module
end
end
if not self._data and labels_and_handlers.HANDLERS then
for _, handler in ipairs(labels_and_handlers.HANDLERS) do
local data = {
label = self._info.label,
lang = self._lang,
sc = self._sc,
args = self._info.args or {},
called_from_inside = self._info.called_from_inside,
}
self._data, args_handled = handler(data)
if self._data then
if self._data.umbrella == nil and self._data.umbrella_parents == nil then
self._data.umbrella = false
end
self._data.module = self._data.module or module
break
end
end
end
if self._data then
break
end
end
seen[obj:getCode()] = true
obj = obj:getFamily()
until not obj or seen[obj:getCode()]
end
-- Then check lang-independent labels.
if not self._data then
local labels = lang_independent_data["LABELS"]
self._data = labels[self._info.label]
-- See comment above about uppercase- vs. lowercase-initial labels, which are indistinguishable
-- in umbrella categories.
if not self._data then
self._data = labels[self._info.orig_label]
if self._data then
self._info.label = self._info.orig_label
end
end
end
-- Then check lang-independent handlers.
if not self._data then
local data = {
label = self._info.label,
lang = self._lang,
sc = self._sc,
args = self._info.args or {},
called_from_inside = self._info.called_from_inside,
}
for _, handler in ipairs(lang_independent_data["HANDLERS"]) do
self._data, args_handled = handler.handler(data)
if self._data then
self._data.module = self._data.module or handler.module
break
end
end
end
end
if not args_handled and self._data and self._info.args and next(self._info.args) then
local module_text = " (handled in [[" .. (self._data.module or "UNKNOWN").. "]])"
local args_text = {}
for k, v in pairs(self._info.args) do
insert(args_text, k .. "=" .. ((type(v) == "string" or type(v) == "number") and v or dump(v)))
end
error("poscatboiler label '" .. self._info.label .. "' " .. module_text .. " doesn't accept extra args " ..
concat(args_text, ", "))
end
if self._sc and not self._lang then
error("Umbrella categories cannot have a script specified.")
end
end
function Category:convert_spec_to_string(desc)
if not desc then
return desc
end
local desc_type = type(desc)
if desc_type == "string" then
return desc
elseif desc_type == "number" then
return tostring(desc)
elseif not is_callable(desc) then
error("Internal error: `desc` must be a string, number, function, callable table or nil; received a " .. desc_type)
end
desc = desc{
lang = self._lang,
sc = self._sc,
label = self._info.label,
raw = self._info.raw,
}
if not desc then
return desc
end
desc_type = type(desc)
if desc_type == "string" then
return desc
end
error("Internal error: the value returned by `desc` must be a string or nil; received a " .. desc_type)
end
local function add_obj_args(args, obj, obj_type)
if obj then
args[obj_type .. "code"] = obj:getCode()
args[obj_type .. "name"] = obj:getCanonicalName()
args[obj_type .. "disp"] = obj:getDisplayForm()
args[obj_type .. "cat"] = obj:getCategoryName()
args[obj_type .. "link"] = obj:makeCategoryLink()
end
end
-- Expands `desc` like a template, passing values for specs like {{{langname}}}.
function Category:substitute_template_specs(desc)
-- This may end up happening twice but that's OK as the function is (usually) idempotent.
-- FIXME: Not idempotent if a preprocessed template returns wikicode.
desc = self:convert_spec_to_string(desc)
if not desc then
return nil
end
-- Populate the substitution arguments.
local args = {}
args.umbrella_msg = "This is an umbrella category. It contains no dictionary entries, but only other, language-specific categories, which in turn contain relevant terms in a given language."
args.umbrella_meta_msg = "This is an umbrella metacategory, covering a general area such as \"lemmas\", \"names\" or \"terms by etymology\". It contains no dictionary entries, but holds only umbrella (\"by language\") categories covering specific subtopics, which in turn contain language-specific categories holding terms in a given language for that same topic."
add_obj_args(args, self._lang, "lang")
add_obj_args(args, self._sc, "sc")
return parse(desc, true):expand(args)
end
function Category:substitute_template_specs_in_args(args)
if not args then
return args
end
local pinfo = {}
for k, v in pairs(args) do
pinfo[self:substitute_template_specs(k)] = self:substitute_template_specs(v)
end
return pinfo
end
function Category:make_new(info)
info.originating_label = self._info.label
info.originating_module = self._data.module
info.called_from_inside = true
return Category.new(info)
end
function Category:getBreadcrumbName()
local ret
if self._lang or self._info.raw then
ret = self._data.breadcrumb
else
ret = self._data.umbrella and self._data.umbrella.breadcrumb
end
if not ret then
ret = self._info.label
end
if type(ret) ~= "table" then
ret = {name = ret}
end
local name = self:substitute_template_specs(ret.name)
local nocap = ret.nocap
if self._sc then
name = name .. " in " .. self._sc:getDisplayForm()
end
return name, nocap
end
local function expand_toc_template_if(template)
local template_obj = new_title(template, 10)
if template_obj.exists then
return mw.getCurrentFrame():expandTemplate{title = template_obj.text, args = {}}
end
return nil
end
-- Return the textual expansion of the first existing template among the given templates, first performing
-- substitutions on the template name such as replacing {{{langcode}}} with the current language's code (if any).
-- If no templates exist after expansion, or if nil is passed in, return nil. If a single string is passed in,
-- treat it like a one-element list consisting of that string.
function Category:get_template_text(templates)
if templates == nil then
return nil
elseif type(templates) ~= "table" then
templates = {templates}
end
for _, template in ipairs(templates) do
if template == false then
return false
end
template = self:substitute_template_specs(template)
return expand_toc_template_if(template)
end
return nil
end
function Category:getTOC(toc_type)
-- Type "none" means everything fits on a single page; in that case, display nothing.
if toc_type == "none" then
return nil
end
local templates, fallback_templates
-- If TOC type is "full" (more than 2500 entries), do the following, in order:
-- 1. look up and expand the `toc_template_full` templates (normal or umbrella, depending on whether there is
-- a current language);
-- 2. look up and expand the `toc_template` templates (normal or umbrella, as above);
-- 3. do the default behavior, which is as follows:
-- 3a. look up a language-specific "full" template according to the current language (using English if there
-- is no current language);
-- 3b. look up a script-specific "full" template according to the first script of current language (using English
-- if there is no current language);
-- 3c. look up a language-specific "normal" template according to the current language (using English if there
-- is no current language);
-- 3d. look up a script-specific "normal" template according to the first script of the current language (using
-- English if there is no current language);
-- 3e. display nothing.
--
-- If TOC type is "normal" (between 200 and 2500 entries), do the following, in order:
-- 1. look up and expand the `toc_template` templates (normal or umbrella, depending on whether there is
-- a current language);
-- 2. do the default behavior, which is as follows:
-- 2a. look up a language-specific "normal" template according to the current language (using English if there
-- is no current language);
-- 2b. look up a script-specific "normal" template according to the first script of the current language (using
-- English if there is no current language);
-- 2c. display nothing.
local data_source
if self._lang or self._info.raw then
data_source = self._data
else
data_source = self._data.umbrella
end
if data_source then
if toc_type == "full" then
templates = data_source.toc_template_full
fallback_templates = data_source.toc_template
else
templates = data_source.toc_template
end
end
local text = self:get_template_text(templates)
if text then
return text
elseif text == false then
return nil
end
text = self:get_template_text(fallback_templates)
if text then
return text
elseif text == false then
return nil
end
local default_toc_templates_to_check = {}
local lang, sc = self:getCatfixInfo()
local langcode = lang and lang:getCode() or "en"
local sccode = sc and sc:getCode() or lang and lang:getScriptCodes()[1] or "Latn"
-- FIXME: What is toctemplateprefix used for?
local tocname = (self._data.toctemplateprefix or "") .. "categoryTOC"
if toc_type == "full" then
insert(default_toc_templates_to_check, ("%s-%s/full"):format(langcode, tocname))
insert(default_toc_templates_to_check, ("%s-%s/full"):format(sccode, tocname))
end
insert(default_toc_templates_to_check, ("%s-%s"):format(langcode, tocname))
insert(default_toc_templates_to_check, ("%s-%s"):format(sccode, tocname))
for _, toc_template in ipairs(default_toc_templates_to_check) do
local toc_template_text = expand_toc_template_if(toc_template)
if toc_template_text then
return toc_template_text
end
end
return nil
end
function Category:getInfo()
return self._info
end
function Category:getDataModule()
return self._data.module
end
function Category:canBeEmpty()
if self._lang or self._info.raw then
return self._data.can_be_empty
end
return self._data.umbrella and self._data.umbrella.can_be_empty
end
function Category:isHidden()
if self._lang or self._info.raw then
return self._data.hidden
end
return self._data.umbrella and self._data.umbrella.hidden
end
function Category:getCategoryName()
if self._info.raw then
return self._info.canonical_name or self._info.label
elseif self._lang then
local ret = self._lang:getCanonicalName() .. " " .. self._info.label
if self._sc then
ret = ret .. " in " .. self._sc:getDisplayForm()
end
return ucfirst(ret)
end
local ret = ucfirst(self._info.label)
if not (self._data.no_by_language or self._data.umbrella and self._data.umbrella.no_by_language) then
ret = ret .. " by language"
end
return ret
end
function Category:getTopright()
if self._lang or self._info.raw then
return self:substitute_template_specs(self._data.topright)
end
return self._data.umbrella and self:substitute_template_specs(self._data.umbrella.topright)
end
function Category:display_title(displaytitle, lang)
if type(displaytitle) == "string" then
displaytitle = self:substitute_template_specs(displaytitle)
else
displaytitle = displaytitle(self:getCategoryName(), lang)
end
mw.getCurrentFrame():callParserFunction("DISPLAYTITLE", "Category:" .. displaytitle)
end
function Category:get_labels_categorizing()
local m_labels_utilities = require(labels_utilities_module)
local pos_cat_labels, sense_cat_labels, use_tlb
pos_cat_labels = m_labels_utilities.find_labels_for_category(self._info.label, "pos", self._lang)
local sense_label = self._info.label:match("^(.*) terms$")
if sense_label then
use_tlb = true
else
sense_label = self._info.label:match("^terms with (.*) senses$")
end
if not sense_label then
return nil
end
sense_cat_labels = m_labels_utilities.find_labels_for_category(sense_label, "sense", self._lang)
if use_tlb then
return m_labels_utilities.format_labels_categorizing(pos_cat_labels, sense_cat_labels, self._lang)
end
local all_labels = pos_cat_labels
for k, v in pairs(sense_cat_labels) do
all_labels[k] = v
end
return m_labels_utilities.format_labels_categorizing(all_labels, nil, self._lang)
end
-- FIXME: this is clunky.
local function remove_lang_params(desc)
-- Simply remove a language name/code/category from the beginning of the string, but replace the language name
-- in the middle of the string with either "specific languages" or "specific-language" depending on whether the
-- language name appears to be an attributive qualifier of another noun or to stand by itself. This may be wrong,
-- in which case the category in question should supply its own umbrella description.
desc = desc:gsub("^{{{langname}}} ", "")
:gsub("{{{langname}}} %(", "specific languages (")
:gsub("{{{langname}}}([.,])", "specific languages%1")
:gsub("{{{langname}}} ", "specific-language ")
:gsub("{{{langdisp}}}", "specific languages")
:gsub("{{{langlink}}}", "specific languages")
return desc
end
function Category:getDescription(isChild)
-- Allows different text in the list of a category's children
local isChild = isChild == "child"
if self._lang or self._info.raw then
if not isChild and self._data.displaytitle then
self:display_title(self._data.displaytitle, self._lang)
end
if self._sc then
return self:getCategoryName() .. "."
end
local desc = self:substitute_template_specs(self._data.description)
if not desc then
return nil
elseif isChild then
return desc
end
return sparse_concat({
self:substitute_template_specs(self._data.preceding),
desc,
self:substitute_template_specs(self._data.additional),
self:substitute_template_specs(self:get_labels_categorizing()),
}, "\n\n")
end
local umbrella = self._data.umbrella
if not isChild and umbrella and umbrella.displaytitle then
self:display_title(umbrella.displaytitle)
end
local desc = self:substitute_template_specs(umbrella and umbrella.description)
local has_umbrella_desc = not not desc
if not desc then
desc = self:convert_spec_to_string(self._data.description)
if desc then
desc = remove_lang_params(desc)
desc = lcfirst(desc)
desc = desc:gsub("%.$", "")
desc = "Categories with " .. desc .. "."
else
desc = "Categories with " .. self._info.label .. " in various specific languages."
end
desc = self:substitute_template_specs(desc)
end
if isChild then
return desc
end
return sparse_concat({
self:substitute_template_specs(umbrella and umbrella.preceding or not has_umbrella_desc and self._data.preceding),
desc,
self:substitute_template_specs(umbrella and umbrella.additional or not has_umbrella_desc and self._data.additional),
self:substitute_template_specs("{{{umbrella_msg}}}"),
self:substitute_template_specs(self:get_labels_categorizing()),
}, "\n\n")
end
function Category:new_sortkey(sortkey)
local sortkey_type = type(sortkey)
if sortkey_type == "string" then
sortkey = uupper(sortkey)
elseif sortkey_type == "table" then
function sortkey:makeSortKey()
local sort_func = self.sort_func
if sort_func ~= nil then
return sort_func(self.sort_base)
end
local lang = self.lang
if lang == nil then
return self.sort_base
end
lang = get_lang(lang, nil, true)
if lang == nil then
return self.sort_base
end
local sc = self.sc
if sc ~= nil then
sc = get_script(sc)
end
return lang:makeSortKey(self.sort_base, sc)
end
end
return sortkey
end
function Category:inherit_spec(spec, parent_spec)
if spec == false then
return nil
end
return self:substitute_template_specs(spec or parent_spec)
end
function Category:canonicalize_parents_children(cats, is_children)
if not cats then
return nil
elseif type(cats) == "table" then
if cats.name or cats.module then
cats = {cats}
elseif #cats == 0 then
return nil
end
else
cats = {cats}
end
local ret = {}
for _, cat in ipairs(cats) do
if type(cat) ~= "table" or not cat.name and not cat.module then
cat = {name = cat}
end
insert(ret, cat)
end
local is_umbrella = not self._lang and not self._info.raw
local table_type = is_children and "extra_children" or "parents"
for i, cat in ipairs(ret) do
local raw
if self._info.raw or is_umbrella then
raw = not cat.is_label
else
raw = cat.raw
end
local lang = self:inherit_spec(cat.lang, not raw and self._info.code or nil)
local sc = self:inherit_spec(cat.sc, not raw and self._info.sc or nil)
-- Get the sortkey.
local sortkey = cat.sort
if type(sortkey) == "table" then
sortkey.sort_base = self:substitute_template_specs(sortkey.sort_base) or
error("Missing .sort_base in '" .. table_type .. "' .sort table for '" ..
self._info.label .. "' category entry in module '" .. (self._data.module or "unknown") .. "'")
if sortkey.sort_func then
-- Not allowed to give a lang and/or script if sort_func is given.
local bad_spec = sortkey.lang and "lang" or sortkey.sc and "sc" or nil
if bad_spec then
error("Cannot specify both ." .. bad_spec .. " and .sort_func in '" .. table_type ..
"' .sort table for '" .. self._info.label .. "' category entry in module '" ..
(self._data.module or "unknown") .. "'")
end
else
sortkey.lang = self:inherit_spec(sortkey.lang, lang)
sortkey.sc = self:inherit_spec(sortkey.sc, sc)
end
else
sortkey = self:substitute_template_specs(sortkey)
end
local name
if cat.module then
-- A reference to a category using another category tree module.
if not cat.args then
error("Missing .args in '" .. table_type .. "' table with module=\"" .. cat.module .. "\" for '" ..
self._info.label .. "' category entry in module '" .. (self._data.module or "unknown") .. "'")
end
name = require("Module:category tree/" .. cat.module).new(self:substitute_template_specs_in_args(cat.args))
else
name = cat.name
if not name then
error("Missing .name in " .. (is_umbrella and "umbrella " or "") .. "'" .. table_type .. "' table for '" ..
self._info.label .. "' category entry in module '" .. (self._data.module or "unknown") .. "'")
elseif type(name) == "string" then -- otherwise, assume it's a category object and use it directly
name = self:substitute_template_specs(name)
if name:find("^Category:") then
-- It's a non-poscatboiler category name.
sortkey = sortkey or is_children and name:gsub("^Category:", "") or self:getCategoryName()
else
-- It's a label.
sortkey = sortkey or is_children and name or self._info.label
name = self:make_new{
label = name, code = lang, sc = sc,
raw = raw, args = self:substitute_template_specs_in_args(cat.args)
}
end
end
end
sortkey = sortkey or is_children and " " or self._info.label
ret[i] = {
name = name,
description = is_children and self:substitute_template_specs(cat.description) or nil,
sort = self:new_sortkey(sortkey)
}
end
return ret
end
function Category:getParents()
local is_umbrella, ret = not self._lang and not self._info.raw
if self._sc then
local parent1 = self:make_new{code = self._info.code, label = "terms in " .. self._sc:getCanonicalName() .. " script"}
local parent2 = self:make_new{code = self._info.code, label = self._info.label, raw = self._info.raw, args = self._info.args}
ret = {
{name = parent1, sort = self._sc:getCanonicalName()},
{name = parent2, sort = self._sc:getCanonicalName()},
}
else
local parents
if is_umbrella then
parents = self._data.umbrella and self._data.umbrella.parents or self._data.umbrella_parents
else
parents = self._data.parents
end
ret = self:canonicalize_parents_children(parents)
if not ret then
return nil
end
end
local self_cat = self:getCategoryName()
for _, parent in ipairs(ret) do
local parent_cat = parent.name.getCategoryName and parent.name:getCategoryName()
if self_cat == parent_cat then
error(("Internal error: Infinite loop would occur, as parent category '%s' is the same as the child category"):format(self_cat))
end
end
return ret
end
function Category:getChildren()
local is_umbrella = not self._lang and not self._info.raw
local children = self._data.children
local ret = {}
if not is_umbrella and children then
for _, child in ipairs(children) do
child = mw.clone(child)
if type(child) ~= "table" then
child = {name = child}
end
if not child.sort then
child.sort = child.name
end
-- FIXME, is preserving the script correct?
child.name = self:make_new{code = self._info.code, label = child.name, raw = child.raw, sc = self._info.sc}
insert(ret, child)
end
end
local extra_children
if is_umbrella then
extra_children = self._data.umbrella and self._data.umbrella.extra_children
else
extra_children = self._data.extra_children
end
extra_children = self:canonicalize_parents_children(extra_children, "children")
if extra_children then
for _, child in ipairs(extra_children) do
insert(ret, child)
end
end
return #ret > 0 and ret or nil
end
function Category:getUmbrella()
local umbrella = self._data.umbrella
if umbrella == false or self._info.raw or not self._lang or self._sc then
return nil
end
-- If `umbrella` is a string, use that; otherwise, use the label.
return self:make_new({label = type(umbrella) == "string" and umbrella or self._info.label})
end
function Category:getAppendix()
-- FIXME, this should be customizable.
local lang, label = self._lang, self._info.label
if self._info.raw or not (lang and label) then
return nil
end
local appendix = make_title(100, lang:getCanonicalName() .. " " .. label)
return appendix.exists and appendix.fullText or nil
end
function Category:getCatfixInfo()
if self._lang or self._sc or self._info.raw then
local langcode, sccode, lang, sc = self._data.catfix, self._data.catfix_sc
if langcode then
langcode = self:substitute_template_specs(langcode)
lang = get_lang(langcode)
elseif langcode == nil then -- not false
lang = self._lang
end
if sccode then
sccode = self:substitute_template_specs(sccode)
sc = get_script(sccode)
elseif sccode == nil then -- not false
sc = self._sc
end
return lang, sc
elseif not self._data.umbrella then
return
end
-- umbrella
local langcode, sccode, lang, sc = self._data.umbrella.catfix, self._data.umbrella.catfix_sc
if langcode then
langcode = self:substitute_template_specs(langcode)
lang = get_lang(langcode)
end
if sccode then
sccode = self:substitute_template_specs(sccode)
sc = get_script(sccode)
end
return lang, sc
end
function Category:getTOCTemplateName()
-- This should only be invoked if getTOC() returns true, meaning to do the default algorithm, but getTOC()
-- implements its own default algorithm.
error("Internal error: This should never get called")
end
local export = {}
function export.main(info)
local self = setmetatable({_info = info}, Category)
self:initCommon()
return self._data and self or nil
end
export.new = Category.new
return export