Modul:ProjectGraph: Unterschied zwischen den Versionen

Aus Kyffhäuser KI
Zur Navigation springen Zur Suche springen
Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
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
  end
    if v.value then return tostring(v.value) end
  if type(val) == "string" then
     if v[1] then return valueToString(v[1]) end
     return val
   end
   end
  if type(v) == "string" then return v end
  if type(v) == "number" then return tostring(v) end
  return nil
end
local function rowTitle(row)
  if row.fulltext then return row.fulltext end
  if row[1] and type(row[1]) == "string" then return row[1] end
  if row[1] and type(row[1]) == "table" and row[1].fulltext then return row[1].fulltext end
   return nil
   return nil
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 51:
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 65:
   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 61: Zeile 73:
     "?Status",
     "?Status",
     "mainlabel=-",
     "mainlabel=-",
     "limit=1000"
     "limit=2000"
   }
   }


   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 = 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 99:
   end
   end


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


   -- Node-Definitions (mit Label ohne Namespace hübscher machen)
   -- Node labels
   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(out, string.format('  %s["%s"]', n.id, label))
   end
   end


   -- Kanten
   -- edges
   for k, _ in pairs(edges) do
   for k, _ in pairs(edges) do
     local fromId, toId = k:match("^(.-)%-%-%>(.-)$")
     local fromId, toId = k:match("^(.-)%-%-%>(.-)$")
Zeile 132: Zeile 118:
   end
   end


   -- Status-Styles (optional; greift nur, wenn dein Mermaid-Renderer classDefs unterstützt)
   -- Styles (optional)
  table.insert(out, "")
   table.insert(out, "  classDef todo fill:#fff,stroke:#333;")
   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 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 done fill:#fff,stroke:#0a0,stroke-width:2px;")
   table.insert(out, "  classDef onhold fill:#fff,stroke:#999,stroke-dasharray: 5 5;")
   table.insert(out, "  classDef onhold fill:#fff,stroke:#999,stroke-dasharray: 5 5;")
  table.insert(out, "  classDef blocked fill:#fff,stroke:#f00,stroke-width:2px;")


   for _, n in pairs(nodes) do
   for _, n in pairs(nodes) do
    local st = normalizeStatus(n.status)
     table.insert(out, string.format("  class %s %s;", n.id, norm(n.status)))
     table.insert(out, string.format("  class %s %s;", n.id, st))
   end
   end



Version vom 25. Februar 2026, 04:40 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 rowTitle(row)
  if row.fulltext then return row.fulltext end
  if row[1] and type(row[1]) == "string" then return row[1] end
  if row[1] and type(row[1]) == "table" and row[1].fulltext then return row[1].fulltext end
  return nil
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",
    "mainlabel=-",
    "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 = 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 out = {}
  table.insert(out, "{{#mermaid:")
  table.insert(out, "flowchart LR")

  -- Node labels
  for t, n in pairs(nodes) do
    local label = stripNs(t)
    label = mw.ustring.gsub(label, '"', '\\"')
    table.insert(out, string.format('  %s["%s"]', n.id, label))
  end

  -- edges
  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

  -- Styles (optional)
  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 done fill:#fff,stroke:#0a0,stroke-width:2px;")
  table.insert(out, "  classDef onhold fill:#fff,stroke:#999,stroke-dasharray: 5 5;")
  table.insert(out, "  classDef blocked fill:#fff,stroke:#f00,stroke-width:2px;")

  for _, n in pairs(nodes) do
    table.insert(out, string.format("  class %s %s;", n.id, norm(n.status)))
  end

  table.insert(out, "}}")
  return table.concat(out, "\n")
end

return p