Modul:ProjectGraph
Die Dokumentation für dieses Modul kann unter Modul:ProjectGraph/Doku erstellt werden
local p = {}
-- Mermaid IDs: müssen stabil, "sicher" und eindeutig sein.
local function mermaidId(title)
local base = mw.ustring.gsub(title, "[^%w]", "_")
if mw.ustring.match(base, "^%d") then
base = "T_" .. base
end
-- Kollisionen/Längen vermeiden:
if #base > 40 then
local h = mw.hash.hashValue("md5", title):sub(1, 10)
base = base:sub(1, 30) .. "_" .. h
end
return base
end
local function getPrintouts(row)
-- SMW liefert je nach Version/Config unterschiedliche Strukturen.
-- Häufig: row.printouts["PropertyName"] = { ... }
-- Oder ältere: row["PropertyName"]
if row.printouts then return row.printouts end
return row
end
local function asList(v)
if not v then return {} end
if type(v) ~= "table" then return { v } end
-- Manche Werte sind Tabellen mit fulltext; manche sind schon Listen.
return v
end
local function pageTitleFromValue(val)
if type(val) == "table" and val.fulltext then
return val.fulltext
end
if type(val) == "string" then
return val
end
return nil
end
local function normalizeStatus(s)
if not s then return "todo" end
s = mw.ustring.lower(mw.text.trim(tostring(s)))
if s == "" then return "todo" end
return s
end
-- invoke: {{#invoke:ProjectGraph|mermaid|project=Project:Foo}}
function p.mermaid(frame)
local args = frame.args
local parentArgs = frame:getParent() and frame:getParent().args or {}
local project = args.project or parentArgs.project
if not project or project == "" then
return "Fehler: project=Project:... fehlt."
end
local q = {
string.format("[[Gehört zu Projekt::%s]]", project),
"?Abhängig von",
"?Status",
"mainlabel=-",
"limit=1000"
}
local res = mw.smw.ask(q) or {}
-- Sammeln
local nodes = {} -- title -> { id=..., status=... }
local edges = {} -- "fromId-->toId" -> true (Dedup)
for _, row in ipairs(res) do
local title = row.fulltext or row[1]
if title then
local po = getPrintouts(row)
local statusVal = nil
-- Status kann als Liste kommen:
local st = po["Status"]
if type(st) == "table" then
-- kann { "todo" } oder { { ... } } sein
if st[1] ~= nil then
statusVal = st[1]
end
else
statusVal = st
end
local status = normalizeStatus(statusVal)
if not nodes[title] then
nodes[title] = { id = mermaidId(title), status = status }
else
nodes[title].status = nodes[title].status or status
end
-- Dependencies: Task X ist abhängig von D => Kante D --> X
local deps = po["Abhängig von"]
for _, dv in ipairs(asList(deps)) do
local depTitle = pageTitleFromValue(dv)
if depTitle and depTitle ~= "" then
if not nodes[depTitle] then
nodes[depTitle] = { id = mermaidId(depTitle), status = "todo" }
end
local fromId = nodes[depTitle].id
local toId = nodes[title].id
edges[fromId .. "-->" .. toId] = true
end
end
end
end
-- Mermaid bauen
local out = {}
table.insert(out, "{{#mermaid:flowchart LR")
-- Node-Definitions (mit Label ohne Namespace hübscher machen)
for t, n in pairs(nodes) do
local label = t
label = mw.ustring.gsub(label, "^[^:]+:", "") -- Namespace entfernen (Task:)
-- Mermaid Label escapen (einfach)
label = mw.ustring.gsub(label, '"', '\\"')
table.insert(out, string.format(' %s["%s"]', n.id, label))
end
-- Kanten
for k, _ in pairs(edges) do
local fromId, toId = k:match("^(.-)%-%-%>(.-)$")
if fromId and toId then
table.insert(out, string.format(" %s --> %s", fromId, toId))
end
end
-- Status-Styles (optional; greift nur, wenn dein Mermaid-Renderer classDefs unterstützt)
table.insert(out, "")
table.insert(out, " classDef todo fill:#fff,stroke:#333;")
table.insert(out, " classDef doing fill:#fff,stroke:#333,stroke-width:2px;")
table.insert(out, " classDef blocked fill:#fff,stroke:#f00,stroke-width:2px;")
table.insert(out, " classDef done fill:#fff,stroke:#0a0,stroke-width:2px;")
table.insert(out, " classDef onhold fill:#fff,stroke:#999,stroke-dasharray: 5 5;")
for _, n in pairs(nodes) do
local st = normalizeStatus(n.status)
table.insert(out, string.format(" class %s %s;", n.id, st))
end
table.insert(out, "}}")
return table.concat(out, "\n")
end
return p