Modul:Belege
Die Dokumentation für dieses Modul kann unter Modul:Belege/Doku erstellt werden
-- Modul:Belege
-- Kompaktes Utility-Modul für Beleg-Vorlagen.
-- Alle öffentlichen Funktionen sind direkt via #invoke nutzbar.
local p = {}
-----------------------------------------------------
-- Basis-Helpers
-----------------------------------------------------
local function getArg(frameOrValue, index, name)
-- Wenn es ein frame-Objekt ist:
if type(frameOrValue) == "table" and frameOrValue.args then
local args = frameOrValue.args
if name and args[name] ~= nil then
return tostring(args[name])
end
if index and args[index] ~= nil then
return tostring(args[index])
end
return ""
end
-- Direkter Aufruf aus einem Modul mit String
if frameOrValue == nil then
return ""
end
return tostring(frameOrValue)
end
local function trim(s)
if not s then return "" end
return mw.text.trim(tostring(s))
end
local function splitAuthors(str)
local t = {}
str = trim(str)
if str == "" then
return t
end
for part in mw.text.gsplit(str, ";", true) do
table.insert(t, trim(part))
end
return t
end
-----------------------------------------------------
-- 1. esc
-----------------------------------------------------
function p.esc(frame)
local s = getArg(frame, 1)
return mw.text.nowiki(s)
end
-----------------------------------------------------
-- 2. Autorenformatierung
-----------------------------------------------------
function p.formatAuthors(frame)
local str = getArg(frame, 1, "autor")
str = trim(str)
if str == "" then
return ""
end
local authors = splitAuthors(str)
local out = {}
for _, a in ipairs(authors) do
local first, last = a:match("^(.-)%s+(.-)$")
if first and last and first ~= "" and last ~= "" then
table.insert(out, last .. ", " .. first)
else
table.insert(out, a)
end
end
return table.concat(out, "; ")
end
-----------------------------------------------------
-- 3. Datumsverarbeitung (intern)
-----------------------------------------------------
local function parseDateInternal(s)
s = trim(s)
if s == "" then
return nil
end
-- reines Jahr
if s:match("^%d%d%d%d$") then
return {
year = s,
month = nil,
day = nil,
display = s
}
end
-- Jahr + Monat
local y, m = s:match("^(%d%d%d%d)%-(%d%d)$")
if y and m then
return {
year = y,
month = m,
day = nil,
display = y .. "-" .. m
}
end
-- vollständiges Datum
local y2, m2, d2 = s:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)$")
if y2 and m2 and d2 then
local ok, formatted = pcall(function()
return mw.language.getContentLanguage():formatDate("j. M Y", s)
end)
return {
year = y2,
month = m2,
day = d2,
display = ok and formatted or (y2 .. "-" .. m2 .. "-" .. d2)
}
end
-- Fallback: gib es einfach so aus
return {
year = nil,
month = nil,
day = nil,
display = s
}
end
-----------------------------------------------------
-- 3a. formatYearOrDate
-----------------------------------------------------
function p.formatYearOrDate(frame)
local dateStr = getArg(frame, 1, "datum")
local d = parseDateInternal(dateStr)
if not d then
return ""
end
return "(" .. (d.display or dateStr) .. ")"
end
-----------------------------------------------------
-- 3b. formatAccess (abgerufen am …)
-----------------------------------------------------
function p.formatAccess(frame)
local dateStr = getArg(frame, 1, "zugriff")
local d = parseDateInternal(dateStr)
if not d then
return ""
end
return "(abgerufen am " .. (d.display or dateStr) .. ")"
end
-----------------------------------------------------
-- 3c. formatDateStandalone (nur Datum ohne Klammern)
-----------------------------------------------------
function p.formatDateStandalone(frame)
-- Erwartet |datum= oder 1. Parameter
local dateStr = getArg(frame, 1, "datum")
if not dateStr or dateStr == "" then
return ""
end
local d = parseDateInternal(dateStr)
if not d then
-- Fallback: gib den Rohwert aus
return dateStr
end
-- "display" ist bereits lokalisiert (z. B. "1. Nov. 2024")
return d.display or dateStr
end
-----------------------------------------------------
-- 4. Seitenangabe
-----------------------------------------------------
function p.formatPages(frame)
local s = getArg(frame, 1, "seiten")
s = trim(s)
if s == "" then
return ""
end
-- Falls noch kein "S. " vorne steht, ergänzen
if not s:match("^[Ss]%.%s*") then
s = "S. " .. s
end
-- Am Ende genau EIN Punkt: falls keiner da, hinhängen
if not s:match("%.%s*$") then
s = s .. "."
end
return s
end
-----------------------------------------------------
-- 5. URL-Bereinigung
-----------------------------------------------------
function p.cleanUrl(frame)
-- URL besorgen: erst |url=, dann 1. Parameter
local raw = ""
if type(frame) == "table" and frame.args then
raw = frame.args.url or frame.args[1] or ""
-- falls über eine Vorlage aufgerufen und die eigentlichen
-- Parameter im Parent-Frame stecken
if (raw == "" or raw == nil) and frame.getParent then
local parent = frame:getParent()
if parent and parent.args then
raw = parent.args.url or parent.args[1] or ""
end
end
else
raw = tostring(frame or "")
end
raw = trim(raw)
if raw == "" then
return ""
end
-- Hat die URL überhaupt ein "?"
local qm = raw:find("?", 1, true)
if not qm then
-- keine Query -> unverändert
return raw
end
local base = raw:sub(1, qm - 1)
local query = raw:sub(qm + 1)
-- Nur "?" am Ende -> dann kann das Fragezeichen weg
if query == "" then
return base
end
local kept = {}
-- Query in &-Segmente zerlegen
for part in string.gmatch(query, "[^&]+") do
local name, value = part:match("^([^=]+)=(.*)$")
if not name then
-- kein "=", z.B. "?foo" -> ganzer Part als Name
name = part
end
local lname = string.lower(trim(name))
-- Tracking-Parameter RAUS
if not lname:match("^utm_")
and lname ~= "fbclid"
and lname ~= "gclid" then
-- kompletten Part behalten (nicht neu zusammensetzen)
table.insert(kept, part)
end
end
if #kept == 0 then
-- alles war Tracking -> nur Basis
return base
end
return base .. "?" .. table.concat(kept, "&")
end
-----------------------------------------------------
-- Q-ID-Formatter (dw:/wikidata:)
-----------------------------------------------------
local function formatQIDInternal(qidRaw)
qidRaw = trim(qidRaw or "")
if qidRaw == "" then
return ""
end
-- Erwartet Präfix + ID, z. B. "dw:Q123" oder "wikidata:Q183"
local prefix, id = qidRaw:match("^([^:]+):(.+)$")
if not prefix or not id then
return string.format(
'<span class="cite-error">FEHLER: Ungültige Q-ID „%s“</span>',
mw.text.nowiki(qidRaw)
)
end
prefix = mw.ustring.lower(trim(prefix))
id = trim(id)
if prefix == "dw" then
-- Eigenes DataWiki
return string.format("[[dw:%s|dw:%s]]", id, id)
elseif prefix == "wikidata" then
-- Wikidata – nur Q-ID als Linktext
return string.format("[[wikidata:%s|%s]]", id, id)
else
return string.format(
'<span class="cite-error">FEHLER: Unbekannter Q-ID-Präfix „%s“</span>',
mw.text.nowiki(prefix)
)
end
end
function p.formatQID(frame)
-- |qid= oder 1. Parameter
local qid = getArg(frame, 1, "qid")
return formatQIDInternal(qid)
end
-----------------------------------------------------
-- 6. Medienplattformen – Whitelist, Badges, Canonicalizer
-----------------------------------------------------
-- Mapping von eingegebener Plattform (wie im TemplateData) auf kanonische Keys
local PLATFORM_CANON = {
["youtube"] = "youtube",
["vimeo"] = "vimeo",
["ard"] = "ard",
["ard mediathek"] = "ard",
["zdf"] = "zdf",
["zdf mediathek"] = "zdf",
["arte"] = "arte",
["arte mediathek"] = "arte",
["spotify"] = "spotify",
["soundcloud"] = "soundcloud",
["twitch"] = "twitch",
}
local function normalizePlatform(raw)
raw = trim(raw or ""):lower()
if raw == "" then
return nil
end
return PLATFORM_CANON[raw]
end
-- Zulässige Plattformen (canonical lowercase als Schlüssel)
local MEDIA_PLATFORMS = {
youtube = true,
vimeo = true,
ard = true,
zdf = true,
arte = true,
spotify = true,
soundcloud = true,
twitch = true,
}
-- Mapping zur Anzeige (Name → Label)
local PLATFORM_LABEL = {
youtube = "YouTube",
vimeo = "Vimeo",
ard = "ARD Mediathek",
zdf = "ZDF Mediathek",
arte = "Arte",
spotify = "Spotify",
soundcloud = "SoundCloud",
twitch = "Twitch",
}
-- Icons pro Plattform (FontAwesome)
local PLATFORM_ICONS = {
youtube = "fa-brands fa-youtube",
vimeo = "fa-brands fa-vimeo",
ard = "fa-solid fa-tv",
zdf = "fa-solid fa-tv",
arte = "fa-solid fa-tv",
spotify = "fa-brands fa-spotify",
soundcloud = "fa-brands fa-soundcloud",
twitch = "fa-brands fa-twitch",
}
-----------------------------------------------------
-- Hilfsfunktion: Fehlerausgabe bei ungültiger Plattform
-----------------------------------------------------
local function platformError(platform)
platform = mw.text.nowiki(platform or "")
return string.format(
'<span class="cite-error">FEHLER: Ungültige Medienplattform „%s“</span>',
platform
)
end
-----------------------------------------------------
-- Badges für Plattformen
-----------------------------------------------------
function p.platformBadge(frame)
local rawPlatform = getArg(frame, 1, "plattform") or ""
local platform = normalizePlatform(rawPlatform)
if not platform or not MEDIA_PLATFORMS[platform] then
return platformError(rawPlatform)
end
local icon = PLATFORM_ICONS[platform] or "fa-solid fa-video"
local label = PLATFORM_LABEL[platform] or platform
return string.format(
'<span class="cite-platform"><i class="%s"></i> %s</span>',
icon,
mw.text.nowiki(label)
)
end
-----------------------------------------------------
-- CANONICALIZER – für jede Plattform eigene Logik
-----------------------------------------------------
-- YouTube
local function canonicalYouTube(url, id, start)
-- ID hat Vorrang
if id and id ~= "" then
local base = "https://www.youtube.com/watch?v=" .. id
if start and start ~= "" then
return base .. "&t=" .. start
end
return base
end
-- URL normalisieren
url = trim(url or "")
if url == "" then
return ""
end
-- youtu.be/xyz
local sid = url:match("youtu%.be/([^%?&]+)")
if sid then
id = sid
end
-- watch?v=xyz
local wid = url:match("v=([^%&]+)")
if wid then
id = wid
end
if not id then
return url -- Fallback
end
return canonicalYouTube("", id, start)
end
-- Vimeo
local function canonicalVimeo(url, id)
if id and id ~= "" then
return "https://vimeo.com/" .. id
end
url = trim(url or "")
if url == "" then
return ""
end
local vid = url:match("vimeo%.com/(%d+)")
if vid then
return "https://vimeo.com/" .. vid
end
return url
end
-- ARD / ZDF / Arte – einfache Normalizer
-- (Wir verzichten bewusst auf API-Queries; nur Tracking entfernen)
local function canonicalSimpleClean(url)
if not url or url == "" then
return ""
end
-- vorhandene cleanUrl-Logik wiederverwenden
return p.cleanUrl(url)
end
-- Spotify
local function canonicalSpotify(url)
url = trim(url or "")
if url == "" then return "" end
-- Show / Episode IDs extrahieren
local base, id = url:match("(https://open%.spotify%.com/[%w%/]+)/([^%?]+)")
if base and id then
return base .. "/" .. id
end
return p.cleanUrl(url)
end
-- SoundCloud
local function canonicalSoundCloud(url)
return p.cleanUrl(url or "")
end
-- Twitch
local function canonicalTwitch(url, id, channel)
url = trim(url or "")
if channel and channel ~= "" then
return "https://www.twitch.tv/" .. channel
end
-- Clips-URL?
local slug = url:match("clips%.twitch%.tv/([^%?]+)")
if slug then
return "https://clips.twitch.tv/" .. slug
end
return p.cleanUrl(url)
end
-----------------------------------------------------
-- Dispatcher für canonicalMediaUrl
-----------------------------------------------------
local CANONICAL = {
youtube = function(a) return canonicalYouTube(a.url, a.id, a.start) end,
vimeo = function(a) return canonicalVimeo(a.url, a.id) end,
ard = function(a) return canonicalSimpleClean(a.url) end,
zdf = function(a) return canonicalSimpleClean(a.url) end,
arte = function(a) return canonicalSimpleClean(a.url) end,
spotify = function(a) return canonicalSpotify(a.url) end,
soundcloud = function(a) return canonicalSoundCloud(a.url) end,
twitch = function(a) return canonicalTwitch(a.url, a.id, a.channel) end,
}
-----------------------------------------------------
-- Öffentliche Funktion: canonicalMediaUrl
-----------------------------------------------------
function p.canonicalMediaUrl(frame)
local rawPlatform = getArg(frame, 1, "plattform")
local url = getArg(frame, 2, "url")
local id = getArg(frame, 3, "id") -- YouTube/Vimeo
local start = getArg(frame, 4, "zeit") -- Timestamp
local channel = getArg(frame, 5, "kanal") -- Twitch
local platform = normalizePlatform(rawPlatform)
if not platform or platform == "" then
-- keine Plattform -> keine URL
return ""
end
if not MEDIA_PLATFORMS[platform] then
return platformError(rawPlatform)
end
local fn = CANONICAL[platform]
if not fn then
return platformError(rawPlatform)
end
return fn({
url = url,
id = id,
start = start,
channel = channel,
})
end
-----------------------------------------------------
-- 7. Badges
-----------------------------------------------------
function p.badge(frame)
local t = getArg(frame, 1, "typ")
t = trim(t):lower()
if t == "pdf" then
return '<span class="beleg-badge beleg-badge-pdf" title="PDF">' ..
'<i class="fa-solid fa-file-pdf" aria-hidden="true"></i>' ..
'</span>'
elseif t == "warn" then
return '<span class="beleg-badge beleg-badge-warn" title="Hinweis">' ..
'<i class="fas fa-circle-exclamation" aria-hidden="true"></i>' ..
'</span>'
elseif t == "archive" then
return '<span class="beleg-badge beleg-badge-archive" title="Archiv">' ..
'<i class="fa-solid fa-box-archive" aria-hidden="true"></i>' ..
'</span>'
elseif t == "video" then
return '<span class="beleg-badge beleg-badge-video" title="Video">' ..
'<i class="fa-solid fa-play" aria-hidden="true"></i>' ..
'</span>'
end
return ""
end
-----------------------------------------------------
-- 8. Sprach-Badge
-----------------------------------------------------
function p.languageBadge(frame)
local lang = getArg(frame, 1, "sprache")
lang = trim(lang)
if lang == "" or lang == "de" then
return ""
end
return "[" .. lang .. "]"
end
-----------------------------------------------------
-- 9. Archiv-/Offline-Hinweise
-----------------------------------------------------
-- kleines Icon-Helferchen (nur Wikitext/HTML)
local function icon(class, title)
class = class or ""
title = title or ""
return string.format(
'<i class="%s" title="%s"></i>',
class,
title
)
end
-- (B) Archivhinweis: (🗄 Archivversion vom 12. Nov 2025)
function p.archiveInfo(frame)
local archivurl = getArg(frame, 1, "archivurl")
local archivdatum = getArg(frame, 2, "archivdatum")
archivurl = trim(archivurl)
archivdatum = trim(archivdatum)
if archivurl == "" then
return ""
end
local d = parseDateInternal(archivdatum)
local when = d and (d.display or d.year) or ""
local iconHtml = icon("fas fa-archive", "Archivversion")
local label = "Archivversion"
if when ~= "" then
label = label .. " vom " .. when
end
-- Wikitext-Link, Parser kümmert sich um den Rest
local link = string.format(
"[%s %s %s]",
archivurl,
iconHtml,
label
)
return "(<span class=\"cite-archive\">" .. link .. "</span>)"
end
-- (C) Offline-Hinweis: (⚠ Original nicht mehr verfügbar – Beleg prüfen)
-- offline= (leer|ja|DATUM)
-- - leer -> wird i.d.R. gar nicht aufgerufen
-- - "ja" -> generischer Hinweis
-- - DATUM -> präzisierter Hinweis ("seit … nicht mehr verfügbar")
function p.offlineInfo(frame)
local v = trim(getArg(frame, 1, "offline"))
local mode = trim(getArg(frame, 2, "mode")) -- "soft" = mit Archiv
local disp -- formatiertes Datum, falls angegeben
if v ~= "" and v ~= "ja" and v ~= "true" then
local d = parseDateInternal(v)
disp = d and (d.display or d.year) or v
end
-- SOFT: Archiv vorhanden -> nur nüchterne Info, kein Alarm
if mode == "soft" then
if disp then
return "Original seit " .. disp .. " nicht mehr verfügbar."
else
return "Original nicht mehr verfügbar."
end
end
-- HART: kein Archiv -> Warnhinweis mit Icon + „Beleg prüfen“
local iconHtml = icon("fas fa-exclamation-circle", "Hinweis")
local base = "Original"
if disp then
base = base .. " seit " .. disp
end
local text = base .. " nicht mehr verfügbar – Beleg prüfen."
return iconHtml .. " " .. text
end
-----------------------------------------------------
-- Data-Badge für BelegData
-----------------------------------------------------
function p.dataBadge(frame)
-- Optional: eigener Text, z. B. |label= oder 1. Parameter
local label = getArg(frame, 1, "label")
or getArg(frame, 2, "text")
or "Datensatz"
label = trim(label or "")
if label == "" then
label = "Datensatz"
end
-- Einheitliches Icon für alle Datenquellen
local iconHtml = icon("fas fa-chart-bar", "Datensatz / Datenquelle")
return string.format(
'<span class="cite-platform">%s %s</span>',
iconHtml,
mw.text.nowiki(label)
)
end
-----------------------------------------------------
-- 10. ISBN-Formatierung
-----------------------------------------------------
local function normalizeIsbn(raw)
raw = trim(raw or "")
if raw == "" then
return ""
end
-- Führende "ISBN", "ISBN-10", "ISBN-13" etc. entfernen
-- inkl. Doppelpunkt, Bindestrich und Leerzeichen danach
raw = raw:gsub("^[Ii][Ss][Bb][Nn][^0-9Xx]*", "")
-- Nur Ziffern, X/x, Leerzeichen und Bindestriche behalten
raw = raw:gsub("[^0-9Xx%s%-]", "")
-- X normalisieren
raw = raw:gsub("[Xx]", "X")
-- Beliebige Kombinationen aus Leerzeichen/Bindestrichen → EIN Bindestrich
raw = raw:gsub("[%s%-]+", "-")
-- Leading/trailing "-" entfernen
raw = raw:gsub("^%-+", ""):gsub("%-+$", "")
return raw
end
function p.formatIsbn(frame)
local s = getArg(frame, 1, "isbn")
s = normalizeIsbn(s)
if s == "" then
return ""
end
-- Prüfen, ob die "echten" Zeichen 10 oder 13 Stellen haben
local digits = s:gsub("%-", "")
local len = #digits
if len ~= 10 and len ~= 13 then
-- ungültige Länge → trotzdem bereinigte Version zurückgeben,
-- aber ohne weitere Magie
return s
end
-- Optional könnten wir hier noch Checksummen prüfen
return s
end
-----------------------------------------------------
-- 11. ISSN-Formatierung
-----------------------------------------------------
local function normalizeIssn(raw)
raw = trim(raw or "")
if raw == "" then
return ""
end
-- Führende "ISSN" etc. entfernen
raw = raw:gsub("^[Ii][Ss][Ss][Nn][^0-9Xx]*", "")
-- Nur Ziffern, X/x, Leerzeichen und Bindestriche behalten
raw = raw:gsub("[^0-9Xx%s%-]", "")
-- X normalisieren
raw = raw:gsub("[Xx]", "X")
-- Alle Leerzeichen und Bindestriche entfernen
raw = raw:gsub("[%s%-]+", "")
-- Jetzt sollten idealerweise 8 Zeichen übrig sein
if #raw ~= 8 then
-- Länge falsch → trotzdem bereinigte Variante zurückgeben
return raw
end
-- Standardformat ####-###X
return raw:sub(1, 4) .. "-" .. raw:sub(5)
end
function p.formatIssn(frame)
local s = getArg(frame, 1, "issn")
s = normalizeIssn(s)
if s == "" then
return ""
end
-- Später: Prüfsumme rechnen und ggf. Wartungskategorie anhängen
return s
end
-----------------------------------------------------
-- DEBUG: zeigt, was bei cleanUrl eigentlich ankommt
-----------------------------------------------------
function p.debugCleanUrl(frame)
local raw = ""
if type(frame) == "table" and frame.args then
raw = frame.args[1] or frame.args.url or ""
if (raw == "" or raw == nil) and frame.getParent then
local parent = frame:getParent()
if parent and parent.args then
raw = parent.args[1] or parent.args.url or ""
end
end
else
raw = tostring(frame or "")
end
raw = tostring(raw)
local out = {}
table.insert(out, "RAW: " .. mw.text.nowiki(raw))
local qm = raw:find("?", 1, true)
if not qm then
table.insert(out, "KEIN FRAGEZEICHEN GEFUNDEN")
return table.concat(out, "\n")
end
local base = raw:sub(1, qm - 1)
local query = raw:sub(qm + 1)
table.insert(out, "BASE: " .. mw.text.nowiki(base))
table.insert(out, "QUERY: " .. mw.text.nowiki(query))
local i = 0
for part in string.gmatch(query, "[^&]+") do
i = i + 1
table.insert(out, "PART " .. i .. ": " .. mw.text.nowiki(part))
end
return table.concat(out, "\n")
end
return p