Modul:Kategoriestufen
Erscheinungsbild
Die Dokumentation für dieses Modul kann unter Modul:Kategoriestufen/Doku erstellt werden
local p = {}
-- Ziele und Auswahl der Obereinordnung: Modul:Kategoriestufen/config.
-- Die bisherigen sprachlichen Kategorien werden weiterhin erzeugt.
local DEFAULT_MAX_NODES = 200
local MAX_DEPTH = 40
local MAX_DISPLAY_NODES = 1000
local function normalize(text)
text = mw.text.trim(text)
text = text:gsub(':+$', ' Seitenordnung')
text = text:gsub(':', ' '):gsub('%s+', ' ')
return mw.text.trim(text)
end
local function languageCategories(title)
local parentheses, categories, seen = {}, {}, {}
local name = title.text:gsub('%b()', function(group)
if title.namespace ~= 0 then
parentheses[#parentheses + 1] = group:sub(2, -2):gsub('[()]', ' ')
end
return ' '
end)
local function addParts(text)
for part in text:gmatch('[^/]+') do
part = normalize(part)
if part ~= '' then
local category = mw.title.makeTitle(14, '•' .. part)
if category and not seen[category.prefixedText] then
seen[category.prefixedText] = true
categories[#categories + 1] = '[[' .. category.prefixedText .. ']]'
end
end
end
end
addParts(name)
for _, content in ipairs(parentheses) do addParts(content) end
return table.concat(categories)
end
local function warning(message)
return '<span class="error">Seitenordnung: '
.. mw.text.nowiki(message) .. '</span>'
end
local function readConfig()
local config = mw.loadData('Modul:Kategoriestufen/config')
if (config.KategorieLinks ~= nil and type(config.KategorieLinks) ~= 'boolean')
or type(config.SprachkategorienEinbeziehen) ~= 'boolean' then
error('Konfiguration: KategorieLinks und SprachkategorienEinbeziehen '
.. 'müssen true oder false sein.', 0)
end
if config.Attributerstellung ~= nil and type(config.Attributerstellung) ~= 'boolean' then
error('Konfiguration: Attributerstellung muss true oder false sein.', 0)
end
if config.Attributerstellung and (type(config.Attributname) ~= 'string'
or mw.text.trim(config.Attributname) == '') then
error('Konfiguration: Attributname muss bei aktivierter Attributerstellung eine nicht leere Zeichenkette sein.', 0)
end
-- Im Kategorienmodus werden die Einstellungen für abgeleitete Ziele ignoriert.
if not config.KategorieLinks and (
type(config.Vorzeichen) ~= 'string'
or type(config.Zielpfad) ~= 'string'
or (config.EnddoppelpunktErsatz ~= nil
and type(config.EnddoppelpunktErsatz) ~= 'string')
) then
error('Konfiguration: Vorzeichen, Zielpfad und EnddoppelpunktErsatz '
.. 'müssen Zeichenketten sein.', 0)
end
local maxNodes = config.MaxKnoten
if maxNodes ~= nil and (type(maxNodes) ~= 'number'
or maxNodes ~= maxNodes or maxNodes == math.huge
or maxNodes < 1 or maxNodes % 1 ~= 0) then
error('Konfiguration: MaxKnoten muss eine positive ganze Zahl sein.', 0)
end
return config
end
-- Liest den bereits gespeicherten SMW-Datenstand, nicht die laufende Vorschau.
local function parents(title)
local property = title.namespace == 14 and 'Subcategory of' or 'Category'
local ok, rows = pcall(mw.smw.ask, {
'[[:' .. title.prefixedText .. ']]',
'?' .. property .. '#-=parents',
mainlabel = '-', limit = 1
})
if not ok or type(rows) ~= 'table' or type(rows[1]) ~= 'table' then
error('Keine verwertbare SMW-Antwort für ' .. title.prefixedText, 0)
end
local values = rows[1].parents
if values == nil or values == '' then return {} end
if type(values) == 'string' then values = { values } end
if type(values) ~= 'table' then
error('Unerwartetes Antwortformat für ' .. title.prefixedText, 0)
end
local result = {}
for _, value in ipairs(values) do
if type(value) ~= 'string' then
error('Ungültiger Kategorienwert für ' .. title.prefixedText, 0)
end
local cat = mw.title.new(mw.text.trim(value), 14)
if not cat or cat.namespace ~= 14 then
error('Ungültiger Kategoriename: ' .. value, 0)
end
result[#result + 1] = cat
end
table.sort(result, function(a, b) return a.prefixedText < b.prefixedText end)
return result
end
local function ancestors(title, config)
local maxNodes = config.MaxKnoten or DEFAULT_MAX_NODES
local queue = { { title = title, depth = 0 } }
local seen = { [title.prefixedText] = true }
local found, graph = {}, {}
local cursor = 1
while cursor <= #queue do
local node = queue[cursor]
cursor = cursor + 1
local edges, edgeSeen = {}, {}
graph[node.title.prefixedText] = edges
for _, cat in ipairs(parents(node.title)) do
local key = cat.prefixedText
local include = config.SprachkategorienEinbeziehen
or cat.text:sub(1, #'•') ~= '•'
if include and not edgeSeen[key] then
edgeSeen[key] = true
edges[#edges + 1] = cat
end
if include and not seen[key] then
if node.depth >= MAX_DEPTH then
error('Hierarchietiefe über ' .. MAX_DEPTH .. ' bei ' .. key
.. '; Obereinordnung wurde nicht gesetzt.', 0)
end
if #queue >= maxNodes then
error('Mehr als ' .. maxNodes .. ' Seiten im Kategoriengraph bei '
.. key .. '; Obereinordnung wurde nicht gesetzt.', 0)
end
seen[key] = true
found[#found + 1] = { title = cat, depth = node.depth + 1 }
queue[#queue + 1] = { title = cat, depth = node.depth + 1 }
end
end
end
return found, graph
end
local function orderTarget(category, config)
-- Originaltitel einschließlich Namensraum und abschließender Doppelpunkte.
-- pageLink stellt durch [[:...]] eine Verlinkung ohne Kategorisierung her.
if config.KategorieLinks then return category, category.text end
local name = category.text
local replacement = config.EnddoppelpunktErsatz
if replacement == nil then replacement = ' Seitenordnung' end
-- Funktion als Ersatz: Prozentzeichen im konfigurierten Text bleiben wörtlich.
name = name:gsub(':+$', function() return replacement end)
local prefix = config.Vorzeichen
if prefix ~= '' and name:sub(1, #prefix) ~= prefix then
name = prefix .. name
end
local fullName = config.Zielpfad .. name
local target = mw.title.new(fullName, 0)
if not target or target.isExternal or (target.fragment and target.fragment ~= '') then
return nil, name, 'Ungültiges lokales Seitenziel: ' .. fullName
end
return target, name
end
local function pageLink(title, label)
return '[[:' .. title.prefixedText .. '|' .. mw.text.nowiki(label or title.text) .. ']]'
end
-- Nur der gesamte Block ist aufklappbar; die Äste sind verschachtelte Listen.
local function renderTree(root, graph, count, heading, config)
local path, rendered = {}, 0
local function renderNode(title, depth, isRoot)
local key = title.prefixedText
local target, name
if isRoot and title.namespace ~= 14 then
target, name = title, title.text
else
target, name = orderTarget(title, config)
end
local label
if target then
label = pageLink(target, name)
else
label = mw.text.nowiki(name)
.. ' <small class="error">(kein gültiges Seitenziel; nicht annotiert)</small>'
end
if path[key] then
return '<li>' .. label .. ' <small>(Kreisverweis)</small></li>'
end
rendered = rendered + 1
local edges = graph[key] or {}
if depth >= MAX_DEPTH and #edges > 0 then
return '<li>' .. label .. ' <small>(weitere Ebenen nicht dargestellt)</small></li>'
end
path[key] = true
local children = {}
for _, parent in ipairs(edges) do
if rendered >= MAX_DISPLAY_NODES then
children[#children + 1] = '<li><small>Anzeigegrenze erreicht; '
.. 'weitere Äste nicht dargestellt.</small></li>'
break
end
children[#children + 1] = renderNode(parent, depth + 1, false)
end
path[key] = nil
local subtree = ''
if #children > 0 then
subtree = '<ul class="seitenordnung-aeste">' .. table.concat(children) .. '</ul>'
end
return '<li>' .. label .. subtree .. '</li>'
end
local tree = renderNode(root, 0, true)
local empty = count == 0 and '<p>Keine gültigen Obereinordnungswerte ermittelt.</p>' or ''
return '<div id="seitenordnung-oberordnung" '
.. 'class="seitenordnung-baum mw-collapsible mw-collapsed" '
.. 'data-expandtext="⎇" data-collapsetext="⌥">'
.. '<div class="seitenordnung-kopfzeile"><strong>' .. heading
.. '</strong> <small>(' .. count .. ')</small> '
.. '<span class="seitenordnung-schalter" title="ein-/ausklappen">'
.. '<span class="mw-collapsible-toggle-placeholder"></span></span></div>'
.. '<div class="mw-collapsible-content"><ul>' .. tree .. '</ul>' .. empty
.. '</div></div>'
end
function p.main(frame)
local title = mw.title.getCurrentTitle()
local categories = languageCategories(title)
if not mw.smw or type(mw.smw.ask) ~= 'function' then
return warning('Semantic Scribunto ist nicht verfügbar.') .. categories
end
local configOK, config = pcall(readConfig)
if not configOK then return warning(tostring(config)) .. categories end
local ok, found, graph = pcall(ancestors, title, config)
if not ok then return warning(tostring(found)) .. categories end
local values, seenTargets = {}, {}
for _, entry in ipairs(found) do
local targetOK, target = pcall(orderTarget, entry.title, config)
if not targetOK then return warning(tostring(target)) .. categories end
if target then
local key = target.prefixedText
if not seenTargets[key] then
seenTargets[key] = true
values[#values + 1] = key
end
end
end
local renderOK, output = pcall(function()
local heading = frame:preprocess('{{int:SMW-cat-order-title}}')
return renderTree(title, graph, #values, heading, config)
end)
if not renderOK then return warning(tostring(output)) .. categories end
if config.Attributerstellung and #values > 0 then
if type(mw.smw.set) ~= 'function' then
return output .. warning('SMW-Attributerstellung ist nicht verfügbar.') .. categories
end
local setOK, result = pcall(mw.smw.set, {
[mw.text.trim(config.Attributname)] = values
})
if not setOK or result ~= true then
local message = type(result) == 'table' and result.error or result
return warning('SMW konnte die Werte nicht übernehmen: '
.. tostring(message)) .. categories
end
end
return output .. categories
end
return p