Zum Inhalt springen

Modul:Kategoriestufen

aus AufEinWort, der freien Forschung
Dies ist eine alte Version dieser Seite, zuletzt bearbeitet am 1. Oktober 2026 um 17:33 Uhr durch Nikolaos (Diskussion | Beiträge) (AZ: Die Seite wurde neu angelegt: 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…). Sie kann sich erheblich von der aktuellen Version unterscheiden.
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)

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