Modul:ProjectGraph: Unterschied zwischen den Versionen
Zur Navigation springen
Zur Suche springen
Markus (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
Markus (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
||
| (7 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) | |||
| Zeile 1: | Zeile 1: | ||
local p = {} | local p = {} | ||
local function mermaidId(title) | local function mermaidId(title) | ||
local base = mw.ustring.gsub(title, "[^%w]", "_") | local base = mw.ustring.gsub(title, "[^%w]", "_") | ||
if mw.ustring.match(base, "^%d") then | if mw.ustring.match(base, "^%d") then base = "T_" .. base end | ||
if #base > 40 then | if #base > 40 then | ||
local h = mw.hash.hashValue("md5", title):sub(1, 10) | local h = mw.hash.hashValue("md5", title):sub(1, 10) | ||
| Zeile 13: | Zeile 9: | ||
end | end | ||
return base | return base | ||
end | |||
local function stripNs(title) | |||
return mw.ustring.gsub(title or "", "^[^:]+:", "") | |||
end | end | ||
local function getPrintouts(row) | local function getPrintouts(row) | ||
if row.printouts then return row.printouts end | if row.printouts then return row.printouts end | ||
return row | return row | ||
| Zeile 26: | Zeile 23: | ||
if not v then return {} end | if not v then return {} end | ||
if type(v) ~= "table" then return { v } end | if type(v) ~= "table" then return { v } end | ||
return v | return v | ||
end | end | ||
local function | local function valueToString(v) | ||
if type( | if type(v) == "table" then | ||
return | if v.fulltext then return v.fulltext end | ||
if v.value then return tostring(v.value) end | |||
if v[1] then return valueToString(v[1]) end | |||
end | end | ||
if type( | if type(v) == "string" then return v end | ||
return | if type(v) == "number" then return tostring(v) end | ||
return nil | |||
end | |||
local function canonicalTitle(s) | |||
if not s then return nil end | |||
s = mw.text.trim(tostring(s)) | |||
-- remove surrounding [[...]] | |||
s = mw.ustring.gsub(s, "^%[%[", "") | |||
s = mw.ustring.gsub(s, "%]%]$", "") | |||
s = mw.ustring.gsub(s, "%]%].*$", "") -- if extra garbage after ]] | |||
s = mw.ustring.gsub(s, "%[%[", "") | |||
s = mw.ustring.gsub(s, "%]%]", "") | |||
-- if "Title|Label" -> keep only Title | |||
local pipePos = mw.ustring.find(s, "|", 1, true) | |||
if pipePos then | |||
s = mw.ustring.sub(s, 1, pipePos - 1) | |||
end | |||
s = mw.text.trim(s) | |||
if s == "" then return nil end | |||
return s | |||
end | |||
local function rowTitle(row) | |||
local t = row.fulltext or row.title | |||
if not t and row[1] then | |||
if type(row[1]) == "string" then t = row[1] | |||
elseif type(row[1]) == "table" and row[1].fulltext then t = row[1].fulltext end | |||
end | end | ||
return | return canonicalTitle(t) | ||
end | end | ||
local function | local function norm(s) | ||
if not s then return "todo" end | if not s then return "todo" end | ||
s = mw.ustring.lower(mw.text.trim(tostring(s))) | s = mw.ustring.lower(mw.text.trim(tostring(s))) | ||
| Zeile 47: | Zeile 75: | ||
end | end | ||
local function firstText(po, key) | |||
local v = po[key] | |||
if not v then return nil end | |||
if type(v) == "table" and v[1] ~= nil then | |||
return valueToString(v[1]) | |||
end | |||
return valueToString(v) | |||
end | |||
function p.mermaid(frame) | function p.mermaid(frame) | ||
local args = frame.args | local args = frame.args | ||
| Zeile 53: | Zeile 89: | ||
local project = args.project or parentArgs.project | local project = args.project or parentArgs.project | ||
if not project or project == "" then | if not project or project == "" then | ||
return "Fehler: project= | return "Fehler: project=... fehlt." | ||
end | end | ||
| Zeile 60: | Zeile 96: | ||
"?Abhängig von", | "?Abhängig von", | ||
"?Status", | "?Status", | ||
"limit=2000" | |||
"limit= | |||
} | } | ||
local res = mw.smw.ask(q) or {} | local res = mw.smw.ask(q) or {} | ||
local nodes = {} | |||
local edges = {} | |||
local nodes = { | |||
local edges = {} | |||
for _, row in ipairs(res) do | for _, row in ipairs(res) do | ||
local title = row | local title = rowTitle(row) | ||
if title then | if title then | ||
local po = getPrintouts(row) | local po = getPrintouts(row) | ||
local | local st = norm(firstText(po, "Status")) | ||
nodes[title] = nodes[title] or { id = mermaidId(title), status = st } | |||
local deps = po["Abhängig von"] | local deps = po["Abhängig von"] | ||
for _, dv in ipairs(asList(deps)) do | for _, dv in ipairs(asList(deps)) do | ||
local depTitle = | local depTitle = canonicalTitle(valueToString(dv)) | ||
if depTitle and depTitle ~= "" then | if depTitle and depTitle ~= "" then | ||
nodes[depTitle] = nodes[depTitle] or { id = mermaidId(depTitle), status = "todo" } | |||
edges[nodes[depTitle].id .. "-->" .. nodes[title].id] = true | |||
end | end | ||
end | end | ||
| Zeile 111: | Zeile 122: | ||
end | end | ||
local lines = {} | |||
table.insert(lines, "flowchart LR") | |||
table.insert( | |||
-- | -- WICHTIG: keine führenden Leerzeichen am Zeilenanfang, | ||
-- sonst wird es von MediaWiki als "preformatted" behandelt. | |||
for t, n in pairs(nodes) do | for t, n in pairs(nodes) do | ||
local label = t | local label = stripNs(t) | ||
label = mw.ustring.gsub(label, '"', '\\"') | label = mw.ustring.gsub(label, '"', '\\"') | ||
table.insert( | table.insert(lines, string.format('%s["%s"]', n.id, label)) | ||
end | end | ||
for k, _ in pairs(edges) do | for k, _ in pairs(edges) do | ||
local fromId, toId = k:match("^(.-)%-%-%>(.-)$") | local fromId, toId = k:match("^(.-)%-%-%>(.-)$") | ||
if fromId and toId then | if fromId and toId then | ||
table.insert( | table.insert(lines, string.format("%s --> %s", fromId, toId)) | ||
end | end | ||
end | end | ||
table.insert(lines, "classDef todo fill:#fff,stroke:#333;") | |||
table.insert( | table.insert(lines, "classDef doing fill:#fff,stroke:#333,stroke-width:2px;") | ||
table.insert(lines, "classDef done fill:#fff,stroke:#0a0,stroke-width:2px;") | |||
table.insert( | table.insert(lines, "classDef onhold fill:#fff,stroke:#999,stroke-dasharray: 5 5;") | ||
table.insert( | table.insert(lines, "classDef blocked fill:#fff,stroke:#f00,stroke-width:2px;") | ||
table.insert( | |||
table.insert( | |||
for _, n in pairs(nodes) do | for _, n in pairs(nodes) do | ||
table.insert(lines, string.format("class %s %s;", n.id, norm(n.status))) | |||
table.insert( | |||
end | end | ||
table. | local mermaidText = table.concat(lines, "\n") | ||
return | return frame:callParserFunction("mermaid", mermaidText) | ||
end | end | ||
Aktuelle Version vom 25. Februar 2026, 05:18 Uhr
Die Dokumentation für dieses Modul kann unter Modul:ProjectGraph/Doku erstellt werden
local p = {}
local function mermaidId(title)
local base = mw.ustring.gsub(title, "[^%w]", "_")
if mw.ustring.match(base, "^%d") then base = "T_" .. base end
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 stripNs(title)
return mw.ustring.gsub(title or "", "^[^:]+:", "")
end
local function getPrintouts(row)
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
return v
end
local function valueToString(v)
if type(v) == "table" then
if v.fulltext then return v.fulltext end
if v.value then return tostring(v.value) end
if v[1] then return valueToString(v[1]) end
end
if type(v) == "string" then return v end
if type(v) == "number" then return tostring(v) end
return nil
end
local function canonicalTitle(s)
if not s then return nil end
s = mw.text.trim(tostring(s))
-- remove surrounding [[...]]
s = mw.ustring.gsub(s, "^%[%[", "")
s = mw.ustring.gsub(s, "%]%]$", "")
s = mw.ustring.gsub(s, "%]%].*$", "") -- if extra garbage after ]]
s = mw.ustring.gsub(s, "%[%[", "")
s = mw.ustring.gsub(s, "%]%]", "")
-- if "Title|Label" -> keep only Title
local pipePos = mw.ustring.find(s, "|", 1, true)
if pipePos then
s = mw.ustring.sub(s, 1, pipePos - 1)
end
s = mw.text.trim(s)
if s == "" then return nil end
return s
end
local function rowTitle(row)
local t = row.fulltext or row.title
if not t and row[1] then
if type(row[1]) == "string" then t = row[1]
elseif type(row[1]) == "table" and row[1].fulltext then t = row[1].fulltext end
end
return canonicalTitle(t)
end
local function norm(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
local function firstText(po, key)
local v = po[key]
if not v then return nil end
if type(v) == "table" and v[1] ~= nil then
return valueToString(v[1])
end
return valueToString(v)
end
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=... fehlt."
end
local q = {
string.format("[[Gehört zu Projekt::%s]]", project),
"?Abhängig von",
"?Status",
"limit=2000"
}
local res = mw.smw.ask(q) or {}
local nodes = {}
local edges = {}
for _, row in ipairs(res) do
local title = rowTitle(row)
if title then
local po = getPrintouts(row)
local st = norm(firstText(po, "Status"))
nodes[title] = nodes[title] or { id = mermaidId(title), status = st }
local deps = po["Abhängig von"]
for _, dv in ipairs(asList(deps)) do
local depTitle = canonicalTitle(valueToString(dv))
if depTitle and depTitle ~= "" then
nodes[depTitle] = nodes[depTitle] or { id = mermaidId(depTitle), status = "todo" }
edges[nodes[depTitle].id .. "-->" .. nodes[title].id] = true
end
end
end
end
local lines = {}
table.insert(lines, "flowchart LR")
-- WICHTIG: keine führenden Leerzeichen am Zeilenanfang,
-- sonst wird es von MediaWiki als "preformatted" behandelt.
for t, n in pairs(nodes) do
local label = stripNs(t)
label = mw.ustring.gsub(label, '"', '\\"')
table.insert(lines, string.format('%s["%s"]', n.id, label))
end
for k, _ in pairs(edges) do
local fromId, toId = k:match("^(.-)%-%-%>(.-)$")
if fromId and toId then
table.insert(lines, string.format("%s --> %s", fromId, toId))
end
end
table.insert(lines, "classDef todo fill:#fff,stroke:#333;")
table.insert(lines, "classDef doing fill:#fff,stroke:#333,stroke-width:2px;")
table.insert(lines, "classDef done fill:#fff,stroke:#0a0,stroke-width:2px;")
table.insert(lines, "classDef onhold fill:#fff,stroke:#999,stroke-dasharray: 5 5;")
table.insert(lines, "classDef blocked fill:#fff,stroke:#f00,stroke-width:2px;")
for _, n in pairs(nodes) do
table.insert(lines, string.format("class %s %s;", n.id, norm(n.status)))
end
local mermaidText = table.concat(lines, "\n")
return frame:callParserFunction("mermaid", mermaidText)
end