Modul:ProjectGraph: Unterschied zwischen den Versionen

Aus Kyffhäuser KI
Zur Navigation springen Zur Suche springen
Die Seite wurde neu angelegt: „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…“
 
Keine Bearbeitungszusammenfassung
 
(9 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
local p = {}
local p = {}


-- Mermaid IDs: müssen stabil, "sicher" und eindeutig sein.
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
    base = "T_" .. base
  end
  -- Kollisionen/Längen vermeiden:
   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)
  -- 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
   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
  -- Manche Werte sind Tabellen mit fulltext; manche sind schon Listen.
   return v
   return v
end
end


local function pageTitleFromValue(val)
local function valueToString(v)
   if type(val) == "table" and val.fulltext then
   if type(v) == "table" then
     return val.fulltext
    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(val) == "string" then
   if type(v) == "string" then return v end
     return val
  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 nil
   return canonicalTitle(t)
end
end


local function normalizeStatus(s)
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


-- invoke: {{#invoke:ProjectGraph|mermaid|project=Project:Foo}}
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=Project:... fehlt."
     return "Fehler: project=... fehlt."
   end
   end


Zeile 60: Zeile 96:
     "?Abhängig von",
     "?Abhängig von",
     "?Status",
     "?Status",
    "mainlabel=-",
     "limit=2000"
     "limit=1000"
   }
   }


   local res = mw.smw.ask(q) or {}
   local res = mw.smw.ask(q) or {}
 
   local nodes = {}
  -- Sammeln
   local edges = {}
   local nodes = {}  -- title -> { id=..., status=... }
   local edges = {}   -- "fromId-->toId" -> true (Dedup)


   for _, row in ipairs(res) do
   for _, row in ipairs(res) do
     local title = row.fulltext or row[1]
     local title = rowTitle(row)
     if title then
     if title then
       local po = getPrintouts(row)
       local po = getPrintouts(row)
       local statusVal = nil
       local st = norm(firstText(po, "Status"))


       -- Status kann als Liste kommen:
       nodes[title] = nodes[title] or { id = mermaidId(title), status = st }
      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"]
       local deps = po["Abhängig von"]
       for _, dv in ipairs(asList(deps)) do
       for _, dv in ipairs(asList(deps)) do
         local depTitle = pageTitleFromValue(dv)
         local depTitle = canonicalTitle(valueToString(dv))
         if depTitle and depTitle ~= "" then
         if depTitle and depTitle ~= "" then
           if not nodes[depTitle] then
           nodes[depTitle] = nodes[depTitle] or { id = mermaidId(depTitle), status = "todo" }
            nodes[depTitle] = { id = mermaidId(depTitle), status = "todo" }
           edges[nodes[depTitle].id .. "-->" .. nodes[title].id] = true
           end
          local fromId = nodes[depTitle].id
          local toId = nodes[title].id
          edges[fromId .. "-->" .. toId] = true
         end
         end
       end
       end
Zeile 111: Zeile 122:
   end
   end


  -- Mermaid bauen
  local lines = {}
  local out = {}
   table.insert(lines, "flowchart LR")
   table.insert(out, "```mermaid")
  table.insert(out, "flowchart LR")


   -- Node-Definitions (mit Label ohne Namespace hübscher machen)
   -- 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, "^[^:]+:", "") -- Namespace entfernen (Task:)
    -- Mermaid Label escapen (einfach)
     label = mw.ustring.gsub(label, '"', '\\"')
     label = mw.ustring.gsub(label, '"', '\\"')
     table.insert(out, string.format(' %s["%s"]', n.id, label))
     table.insert(lines, string.format('%s["%s"]', n.id, label))
   end
   end


  -- Kanten
   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(out, string.format(" %s --> %s", fromId, toId))
       table.insert(lines, string.format("%s --> %s", fromId, toId))
     end
     end
   end
   end


  -- Status-Styles (optional; greift nur, wenn dein Mermaid-Renderer classDefs unterstützt)
   table.insert(lines, "classDef todo fill:#fff,stroke:#333;")
   table.insert(out, "")
   table.insert(lines, "classDef doing fill:#fff,stroke:#333,stroke-width:2px;")
  table.insert(out, "  classDef todo fill:#fff,stroke:#333;")
   table.insert(lines, "classDef done fill:#fff,stroke:#0a0,stroke-width:2px;")
   table.insert(out, " classDef doing fill:#fff,stroke:#333,stroke-width:2px;")
   table.insert(lines, "classDef onhold fill:#fff,stroke:#999,stroke-dasharray: 5 5;")
   table.insert(out, " classDef blocked fill:#fff,stroke:#f00,stroke-width:2px;")
   table.insert(lines, "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
   for _, n in pairs(nodes) do
    local st = normalizeStatus(n.status)
     table.insert(lines, string.format("class %s %s;", n.id, norm(n.status)))
     table.insert(out, string.format(" class %s %s;", n.id, st))
   end
   end


   table.insert(out, "```")
   local mermaidText = table.concat(lines, "\n")
   return table.concat(out, "\n")
   return frame:callParserFunction("mermaid", mermaidText)
end
  end
 
return p

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