Basculer le menu
Changer de menu des préférences
Basculer le menu personnel
Non connecté(e)
Votre adresse IP sera visible au public si vous faites des modifications.

« Module:Roadmap » : différence entre les versions

De Nefald
Hiob (discussion | contributions)
Page créée avec « -- ============================================================ -- Module:Roadmap -- Génère une roadmap visuelle pour wiki.nefald.fr -- ============================================================ local p = {} -- ------------------------------------------------------------ -- CONFIGURATION CENTRALE -- Modifie ici les statuts, couleurs, icônes et tags -- ------------------------------------------------------------ local CONFIG = { statuts = { done = {... »
 
Hiob (discussion | contributions)
Aucun résumé des modifications
Ligne 7 : Ligne 7 :


-- ------------------------------------------------------------
-- ------------------------------------------------------------
-- CONFIGURATION CENTRALE
-- CONFIGURATION
-- Modifie ici les statuts, couleurs, icônes et tags
-- ------------------------------------------------------------
-- ------------------------------------------------------------


local CONFIG = {
local CONFIG = {
 
statuts = {
  statuts = {
done = {
    done = {
label = "Terminé",
      label   = "Terminé",
icon = "✓",
      icon   = "✓",
color = "#27ae60",
      color   = "#27ae60",
order = 1,
      order   = 1,   -- ordre d'affichage si tri activé
},
    },
inprogress = {
    inprogress = {
label = "En cours",
      label   = "En cours",
icon = "◉",
      icon   = "◉",
color = "#f39c12",
      color   = "#f39c12",
order = 2,
      order   = 2,
},
    },
planned = {
    planned = {
label = "Planifié",
      label   = "Planifié",
icon = "○",
      icon   = "○",
color = "#3498db",
      color   = "#3498db",
order = 3,
      order   = 3,
},
    },
idea = {
    idea = {
label = "Idée",
      label   = "Idée",
icon = "✦",
      icon   = "✦",
color = "#9b59b6",
      color   = "#9b59b6",
order = 4,
      order   = 4,
},
    },
cancelled = {
    cancelled = {
label = "Annulé",
      label   = "Annulé",
icon = "✕",
      icon   = "✕",
color = "#e74c3c",
      color   = "#e74c3c",
order = 5,
      order   = 5,
},
    },
},
  },
tags = {
 
gameplay  = { bg = "#d5f5e3", fg = "#1e8449" },
  -- Tags disponibles : nom = couleur de fond, couleur texte
technique = { bg = "#d6eaf8", fg = "#1a5276" },
  tags = {
contenu  = { bg = "#fdebd0", fg = "#784212" },
    gameplay  = { bg = "#d5f5e3", fg = "#1e8449" },
interface = { bg = "#f9ebea", fg = "#922b21" },
    technique = { bg = "#d6eaf8", fg = "#1a5276" },
event    = { bg = "#f5eef8", fg = "#6c3483" },
    contenu  = { bg = "#fdebd0", fg = "#784212" },
wiki      = { bg = "#eafaf1", fg = "#1d6a39" },
    interface = { bg = "#f9ebea", fg = "#922b21" },
discord  = { bg = "#eee8ff", fg = "#4527a0" },
    event    = { bg = "#f5eef8", fg = "#6c3483" },
},
    wiki      = { bg = "#eafaf1", fg = "#1d6a39" },
done_statuts   = { done = true },
    discord  = { bg = "#eee8ff", fg = "#4527a0" },
ignored_statuts = { cancelled = true },
  },
 
  -- Statuts comptabilisés comme "terminés" pour la progression
  done_statuts = { done = true },
 
  -- Statuts ignorés dans le calcul de progression
  ignored_statuts = { cancelled = true },
}
}


Ligne 68 : Ligne 60 :
-- ------------------------------------------------------------
-- ------------------------------------------------------------


-- Échappe le HTML pour éviter les injections
local function escapeHtml(s)
local function escapeHtml(s)
  if not s then return "" end
if not s then return "" end
  s = tostring(s)
s = tostring(s)
  s = s:gsub("&", "&")
s = s:gsub("&", "&")
  s = s:gsub("<", "&lt;")
s = s:gsub("<", "&lt;")
  s = s:gsub(">", "&gt;")
s = s:gsub(">", "&gt;")
  s = s:gsub('"', "&quot;")
s = s:gsub('"', "&quot;")
  return s
return s
end
end


-- Trim des espaces
local function trim(s)
local function trim(s)
  if not s then return "" end
if not s then return "" end
  return s:match("^%s*(.-)%s*$")
return s:match("^%s*(.-)%s*$")
end
end


-- Récupère un paramètre nommé ou positionnel avec fallback
local function getParam(args, ...)
local function getParam(args, ...)
  for _, key in ipairs({...}) do
for _, key in ipairs({...}) do
    local v = trim(args[key] or "")
local v = trim(args[key] or "")
    if v ~= "" then return v end
if v ~= "" then return v end
  end
end
  return nil
return nil
end
end


-- ------------------------------------------------------------
-- ------------------------------------------------------------
-- PARSEUR DE SECTIONS ET ITEMS
-- PARSEUR - adapté au nouveau format @section
-- Lit le contenu brut du template et extrait les données
-- ------------------------------------------------------------
-- ------------------------------------------------------------
--[[
  Format attendu dans le wikitext :
 
  == Titre de section ==
  item | statut | Titre | desc=Description | tag=gameplay | date=Jan 2025
  item | done  | Truc  | tag=wiki
 
  Chaque ligne "item" devient un item de roadmap.
  Les lignes "==" deviennent des sections.
]]


local function parseContenu(raw)
local function parseContenu(raw)
  local elements = {}
local elements = {}
 
for line in (raw .. "\n"):gmatch("([^\n]*)\n") do
  for line in (raw .. "\n"):gmatch("([^\n]*)\n") do
line = trim(line)
    line = trim(line)
if line == "" or line:match("^%-%-") then
 
-- skip
    -- Ignorer les lignes vides et commentaires
elseif line:match("^@section%s+") then
    if line == "" or line:match("^%-%-") then
local titre = line:match("^@section%s+(.+)$")
      -- skip
table.insert(elements, {
 
type  = "section",
    -- Détecter une section : == Titre ==
titre = trim(titre or "Section"),
    elseif line:match("^==.+==$") then
})
      local titre = line:match("^==%s*(.-)%s*==$")
elseif line:match("^item%s*|") then
      table.insert(elements, {
local parts = {}
        type  = "section",
for part in line:gmatch("([^|]+)") do
        titre = titre or "Section",
table.insert(parts, trim(part))
      })
end
 
local statut = trim(parts[2] or "planned"):lower()
    -- Détecter un item : item | statut | Titre | ...
local titre  = trim(parts[3] or "")
    elseif line:match("^item%s*|") or line:match("^item%s*:") then
local item = {
      -- Séparer par "|"
type  = "item",
      local parts = {}
statut = CONFIG.statuts[statut] and statut or "planned",
      for part in line:gmatch("([^|]+)") do
titre  = titre,
        table.insert(parts, trim(part))
desc  = nil,
      end
tags  = {},
      -- parts[1] = "item", parts[2] = statut, parts[3] = titre, parts[4..n] = params
date  = nil,
 
lien  = nil,
      local statut = trim(parts[2] or "planned"):lower()
}
      local titre  = trim(parts[3] or "")
for i = 4, #parts do
      local item   = {
local key, val = parts[i]:match("^(%w+)%s*=%s*(.+)$")
        type  = "item",
if key and val then
        statut = CONFIG.statuts[statut] and statut or "planned",
key = key:lower()
        titre  = titre,
if key == "desc" then
        desc  = nil,
item.desc = val
        tags  = {},
elseif key == "tag" or key == "tags" then
        date  = nil,
for tag in val:gmatch("([^,]+)") do
        lien  = nil,
table.insert(item.tags, trim(tag):lower())
      }
end
 
elseif key == "date" then
      -- Parser les paramètres nommés dans parts[4..n]
item.date = val
      for i = 4, #parts do
elseif key == "lien" then
        local key, val = parts[i]:match("^(%w+)%s*=%s*(.+)$")
item.lien = val
        if key and val then
end
          key = key:lower()
end
          if key == "desc" then
end
            item.desc = val
table.insert(elements, item)
          elseif key == "tag" or key == "tags" then
end
            -- Support multi-tags séparés par virgule
end
            for tag in val:gmatch("([^,]+)") do
return elements
              table.insert(item.tags, trim(tag):lower())
            end
          elseif key == "date" then
            item.date = val
          elseif key == "lien" then
            item.lien = val
          end
        end
      end
 
      table.insert(elements, item)
    end
  end
 
  return elements
end
end


-- ------------------------------------------------------------
-- ------------------------------------------------------------
-- CALCUL DE LA PROGRESSION
-- CALCUL PROGRESSION
-- ------------------------------------------------------------
-- ------------------------------------------------------------


local function calcProgression(elements)
local function calcProgression(elements)
  local total = 0
local total = 0
  local done  = 0
local done  = 0
 
for _, el in ipairs(elements) do
  for _, el in ipairs(elements) do
if el.type == "item" then
    if el.type == "item" then
if not CONFIG.ignored_statuts[el.statut] then
      if not CONFIG.ignored_statuts[el.statut] then
total = total + 1
        total = total + 1
if CONFIG.done_statuts[el.statut] then
        if CONFIG.done_statuts[el.statut] then
done = done + 1
          done = done + 1
end
        end
end
      end
end
    end
end
  end
if total == 0 then return 0, 0, 0 end
 
return math.floor((done / total) * 100), done, total
  if total == 0 then return 0, 0, 0 end
  local pct = math.floor((done / total) * 100)
  return pct, done, total
end
end


Ligne 205 : Ligne 164 :


local function htmlBadge(statut)
local function htmlBadge(statut)
  local s = CONFIG.statuts[statut] or CONFIG.statuts.planned
local s = CONFIG.statuts[statut] or CONFIG.statuts.planned
  return string.format(
return string.format(
    '<span class="roadmap-badge" style="background:%s" title="%s">%s</span>',
'<span class="roadmap-badge" style="background:%s" title="%s">%s</span>',
    s.color,
s.color, escapeHtml(s.label), s.icon
    escapeHtml(s.label),
)
    s.icon
  )
end
end


local function htmlTags(tags)
local function htmlTags(tags)
  if not tags or #tags == 0 then return "" end
if not tags or #tags == 0 then return "" end
  local parts = {}
local parts = {}
  for _, tag in ipairs(tags) do
for _, tag in ipairs(tags) do
    local cfg = CONFIG.tags[tag]
local cfg = CONFIG.tags[tag]
    if cfg then
if cfg then
      table.insert(parts, string.format(
table.insert(parts, string.format(
        '<span class="roadmap-tag" style="background:%s;color:%s">%s</span>',
'<span class="roadmap-tag" style="background:%s;color:%s">%s</span>',
        cfg.bg, cfg.fg, escapeHtml(tag)
cfg.bg, cfg.fg, escapeHtml(tag)
      ))
))
    else
else
      -- Tag inconnu : style neutre
table.insert(parts, string.format(
      table.insert(parts, string.format(
'<span class="roadmap-tag">%s</span>',
        '<span class="roadmap-tag">%s</span>',
escapeHtml(tag)
        escapeHtml(tag)
))
      ))
end
    end
end
  end
return '<div class="roadmap-tags">' .. table.concat(parts) .. '</div>'
  return '<div class="roadmap-tags">' .. table.concat(parts) .. '</div>'
end
end


local function htmlItem(item)
local function htmlItem(item)
  local statut = item.statut or "planned"
local statut = item.statut or "planned"
  local titreTxt = escapeHtml(item.titre)
local titreTxt = escapeHtml(item.titre)
 
local titreHtml
  -- Lien sur le titre si défini
if item.lien and item.lien ~= "" then
  local titreHtml
titreHtml = string.format('<a href="%s">%s</a>', escapeHtml(item.lien), titreTxt)
  if item.lien and item.lien ~= "" then
else
    titreHtml = string.format(
titreHtml = titreTxt
      '<a href="%s">%s</a>',
end
      escapeHtml(item.lien), titreTxt
local descHtml = ""
    )
if item.desc and item.desc ~= "" then
  else
descHtml = string.format('<div class="roadmap-item-desc">%s</div>', escapeHtml(item.desc))
    titreHtml = titreTxt
end
  end
local dateHtml = ""
 
if item.date and item.date ~= "" then
  local descHtml = ""
dateHtml = string.format('<div class="roadmap-date">%s</div>', escapeHtml(item.date))
  if item.desc and item.desc ~= "" then
end
    descHtml = string.format(
return string.format(
      '<div class="roadmap-item-desc">%s</div>',
'<div class="roadmap-item roadmap-%s">'
      escapeHtml(item.desc)
.. '%s'
    )
.. '<div class="roadmap-item-content">'
  end
.. '<div class="roadmap-item-title">%s</div>'
 
.. '%s%s'
  local dateHtml = ""
.. '</div>'
  if item.date and item.date ~= "" then
.. '%s'
    dateHtml = string.format(
.. '</div>',
      '<div class="roadmap-date">%s</div>',
statut, htmlBadge(statut), titreHtml, descHtml, htmlTags(item.tags), dateHtml
      escapeHtml(item.date)
)
    )
  end
 
  return string.format(
    '<div class="roadmap-item roadmap-%s">%s<div class="roadmap-item-content"><div class="roadmap-item-title">%s</div>%s%s</div>%s</div>',
    statut,
    htmlBadge(statut),
    titreHtml,
    descHtml,
    htmlTags(item.tags),
    dateHtml
  )
end
end


local function htmlSection(section)
local function htmlSection(section)
  return string.format(
return string.format(
    '<div class="roadmap-section"><div class="roadmap-section-title">%s</div>',
'<div class="roadmap-section"><div class="roadmap-section-title">%s</div>',
    escapeHtml(section.titre)
escapeHtml(section.titre)
  )
)
end
end


local function htmlProgressBar(pct, done, total)
local function htmlProgressBar(pct, done, total)
  return string.format(
return string.format(
    '<div class="roadmap-progress-wrap">' ..
'<div class="roadmap-progress-wrap">'
    '<div class="roadmap-progress-label">Progression : <strong>%d%%</strong> &mdash; %d / %d fonctionnalités terminées</div>' ..
.. '<div class="roadmap-progress-label">Progression : <strong>%d%%</strong> %d / %d fonctionnalités</div>'
    '<div class="roadmap-progress-bar"><div class="roadmap-progress-fill" style="width:%d%%"></div></div>' ..
.. '<div class="roadmap-progress-bar"><div class="roadmap-progress-fill" style="width:%d%%"></div></div>'
    '</div>',
.. '</div>',
    pct, done, total, pct
pct, done, total, pct
  )
)
end
end


local function htmlLegende()
local function htmlLegende()
  local parts = {}
local parts = {}
  -- Ordre d'affichage fixé
local ordre = { "done", "inprogress", "planned", "idea", "cancelled" }
  local ordre = { "done", "inprogress", "planned", "idea", "cancelled" }
for _, key in ipairs(ordre) do
  for _, key in ipairs(ordre) do
local s = CONFIG.statuts[key]
    local s = CONFIG.statuts[key]
if s then
    if s then
table.insert(parts, string.format(
      table.insert(parts, string.format(
'<span class="roadmap-legend-item">'
        '<span class="roadmap-legend-item">' ..
.. '<span class="roadmap-badge" style="background:%s;width:18px;height:18px;font-size:0.75em">%s</span>'
        '<span class="roadmap-badge" style="background:%s;width:18px;height:18px;font-size:0.75em">%s</span>' ..
.. ' %s</span>',
        ' %s</span>',
s.color, s.icon, escapeHtml(s.label)
        s.color, s.icon, escapeHtml(s.label)
))
      ))
end
    end
end
  end
return '<div class="roadmap-legend">' .. table.concat(parts, "") .. '</div>'
  return '<div class="roadmap-legend">' .. table.concat(parts, "") .. '</div>'
end
end


local function htmlStats(elements)
local function htmlStats(elements)
  -- Compte par statut
local counts = {}
  local counts = {}
for key in pairs(CONFIG.statuts) do counts[key] = 0 end
  for key in pairs(CONFIG.statuts) do counts[key] = 0 end
for _, el in ipairs(elements) do
 
if el.type == "item" and counts[el.statut] then
  for _, el in ipairs(elements) do
counts[el.statut] = counts[el.statut] + 1
    if el.type == "item" and counts[el.statut] then
end
      counts[el.statut] = counts[el.statut] + 1
end
    end
local parts = {}
  end
local ordre = { "done", "inprogress", "planned", "idea", "cancelled" }
 
for _, key in ipairs(ordre) do
  local parts = {}
if counts[key] and counts[key] > 0 then
  local ordre = { "done", "inprogress", "planned", "idea", "cancelled" }
local s = CONFIG.statuts[key]
  for _, key in ipairs(ordre) do
table.insert(parts, string.format(
    if counts[key] and counts[key] > 0 then
'<span class="roadmap-stat" style="border-color:%s">'
      local s = CONFIG.statuts[key]
.. '<span class="roadmap-stat-count" style="color:%s">%d</span>'
      table.insert(parts, string.format(
.. '<span class="roadmap-stat-label">%s</span>'
        '<span class="roadmap-stat" style="border-color:%s">' ..
.. '</span>',
        '<span class="roadmap-stat-count" style="color:%s">%d</span>' ..
s.color, s.color, counts[key], escapeHtml(s.label)
        '<span class="roadmap-stat-label">%s</span>' ..
))
        '</span>',
end
        s.color, s.color, counts[key], escapeHtml(s.label)
end
      ))
return '<div class="roadmap-stats">' .. table.concat(parts, "") .. '</div>'
    end
  end
 
  return '<div class="roadmap-stats">' .. table.concat(parts, "") .. '</div>'
end
end


-- ------------------------------------------------------------
-- ------------------------------------------------------------
-- POINT D'ENTRÉE PRINCIPAL : render
-- RENDER PRINCIPAL
-- ------------------------------------------------------------
-- ------------------------------------------------------------


function p.render(frame)
function p.render(frame)
  local args   = frame:getParent().args
local args = frame:getParent().args
  local titre   = getParam(args, "titre")   or "Roadmap"
local titre = getParam(args, "titre") or "Roadmap"
  local subtitle = getParam(args, "subtitle")
local subtitle = getParam(args, "subtitle")
  local rawContenu = trim(args[1] or args["contenu"] or "")
local rawContenu = trim(args["contenu"] or args[1] or "")
  local showStats = getParam(args, "stats") ~= "non"
local showStats = getParam(args, "stats") ~= "non"
  local showLegende = getParam(args, "legende") ~= "non"
local showLegende = getParam(args, "legende") ~= "non"
 
local elements = parseContenu(rawContenu)
  -- Parsing du contenu
local pct, done, total = calcProgression(elements)
  local elements = parseContenu(rawContenu)
local html = {}
 
table.insert(html, '<div class="roadmap-container">')
  -- Calcul automatique de la progression
-- Header
  local pct, done, total = calcProgression(elements)
table.insert(html, '<div class="roadmap-header">')
 
table.insert(html, string.format('<div class="roadmap-header-title">%s</div>', escapeHtml(titre)))
  -- Construction du HTML
if subtitle then
  local html = {}
table.insert(html, string.format('<div class="roadmap-subtitle">%s</div>', escapeHtml(subtitle)))
 
end
  -- Conteneur principal
table.insert(html, '</div>')
  table.insert(html, '<div class="roadmap-container">')
-- Stats
 
if showStats and #elements > 0 then
  -- Header
table.insert(html, htmlStats(elements))
  table.insert(html, '<div class="roadmap-header">')
end
  table.insert(html, string.format('<div class="roadmap-header-title">%s</div>', escapeHtml(titre)))
-- Progression
  if subtitle then
if total > 0 then
    table.insert(html, string.format('<div class="roadmap-subtitle">%s</div>', escapeHtml(subtitle)))
table.insert(html, htmlProgressBar(pct, done, total))
  end
end
  table.insert(html, '</div>')
-- Contenu
 
local inSection = false
  -- Stats globales
for _, el in ipairs(elements) do
  if showStats and #elements > 0 then
if el.type == "section" then
    table.insert(html, htmlStats(elements))
if inSection then
  end
table.insert(html, '</div>')
 
end
  -- Barre de progression
table.insert(html, htmlSection(el))
  if total > 0 then
inSection = true
    table.insert(html, htmlProgressBar(pct, done, total))
elseif el.type == "item" then
  end
table.insert(html, htmlItem(el))
 
end
  -- Contenu : sections et items
end
  local inSection = false
if inSection then
  for _, el in ipairs(elements) do
table.insert(html, '</div>')
    if el.type == "section" then
end
      if inSection then
-- Légende
        table.insert(html, '</div>') -- ferme section précédente
if showLegende then
      end
table.insert(html, htmlLegende())
      table.insert(html, htmlSection(el))
end
      inSection = true
table.insert(html, '</div>')
    elseif el.type == "item" then
return table.concat(html, "\n")
      table.insert(html, htmlItem(el))
    end
  end
  if inSection then
    table.insert(html, '</div>') -- ferme dernière section
  end
 
  -- Légende
  if showLegende then
    table.insert(html, htmlLegende())
  end
 
  table.insert(html, '</div>') -- ferme roadmap-container
 
  return table.concat(html, "\n")
end
end


-- ------------------------------------------------------------
-- ------------------------------------------------------------
-- POINT D'ENTRÉE SECONDAIRE : item inline
-- ITEM INLINE
-- Usage : {{#invoke:Roadmap|item|done|Titre|desc=...|tag=...}}
-- ------------------------------------------------------------
-- ------------------------------------------------------------


function p.item(frame)
function p.item(frame)
  local args   = frame.args
local args = frame.args
  local statut = trim(args[1] or "planned"):lower()
local statut = trim(args[1] or "planned"):lower()
  local titre = trim(args[2] or "")
local titre = trim(args[2] or "")
 
if not CONFIG.statuts[statut] then statut = "planned" end
  if not CONFIG.statuts[statut] then statut = "planned" end
local item = {
 
statut = statut,
  local item = {
titre = titre,
    statut = statut,
desc = getParam(args, "desc"),
    titre = titre,
date = getParam(args, "date"),
    desc   = getParam(args, "desc"),
lien = getParam(args, "lien"),
    date   = getParam(args, "date"),
tags = {},
    lien   = getParam(args, "lien"),
}
    tags   = {},
local tagStr = getParam(args, "tag", "tags")
  }
if tagStr then
 
for tag in tagStr:gmatch("([^,]+)") do
  local tagStr = getParam(args, "tag", "tags")
table.insert(item.tags, trim(tag):lower())
  if tagStr then
end
    for tag in tagStr:gmatch("([^,]+)") do
end
      table.insert(item.tags, trim(tag):lower())
return htmlItem(item)
    end
  end
 
  return htmlItem(item)
end
end


return p
return p

Version du 20 février 2026 à 20:13

La documentation pour ce module peut être créée à Module:Roadmap/doc

-- ============================================================
-- Module:Roadmap
-- Génère une roadmap visuelle pour wiki.nefald.fr
-- ============================================================

local p = {}

-- ------------------------------------------------------------
-- CONFIGURATION
-- ------------------------------------------------------------

local CONFIG = {
statuts = {
done = {
label = "Terminé",
icon = "✓",
color = "#27ae60",
order = 1,
},
inprogress = {
label = "En cours",
icon = "◉",
color = "#f39c12",
order = 2,
},
planned = {
label = "Planifié",
icon = "○",
color = "#3498db",
order = 3,
},
idea = {
label = "Idée",
icon = "✦",
color = "#9b59b6",
order = 4,
},
cancelled = {
label = "Annulé",
icon = "✕",
color = "#e74c3c",
order = 5,
},
},
tags = {
gameplay  = { bg = "#d5f5e3", fg = "#1e8449" },
technique = { bg = "#d6eaf8", fg = "#1a5276" },
contenu   = { bg = "#fdebd0", fg = "#784212" },
interface = { bg = "#f9ebea", fg = "#922b21" },
event     = { bg = "#f5eef8", fg = "#6c3483" },
wiki      = { bg = "#eafaf1", fg = "#1d6a39" },
discord   = { bg = "#eee8ff", fg = "#4527a0" },
},
done_statuts    = { done = true },
ignored_statuts = { cancelled = true },
}

-- ------------------------------------------------------------
-- UTILITAIRES
-- ------------------------------------------------------------

local function escapeHtml(s)
if not s then return "" end
s = tostring(s)
s = s:gsub("&", "&amp;")
s = s:gsub("<", "&lt;")
s = s:gsub(">", "&gt;")
s = s:gsub('"', "&quot;")
return s
end

local function trim(s)
if not s then return "" end
return s:match("^%s*(.-)%s*$")
end

local function getParam(args, ...)
for _, key in ipairs({...}) do
local v = trim(args[key] or "")
if v ~= "" then return v end
end
return nil
end

-- ------------------------------------------------------------
-- PARSEUR - adapté au nouveau format @section
-- ------------------------------------------------------------

local function parseContenu(raw)
local elements = {}
for line in (raw .. "\n"):gmatch("([^\n]*)\n") do
line = trim(line)
if line == "" or line:match("^%-%-") then
-- skip
elseif line:match("^@section%s+") then
local titre = line:match("^@section%s+(.+)$")
table.insert(elements, {
type  = "section",
titre = trim(titre or "Section"),
})
elseif line:match("^item%s*|") then
local parts = {}
for part in line:gmatch("([^|]+)") do
table.insert(parts, trim(part))
end
local statut = trim(parts[2] or "planned"):lower()
local titre  = trim(parts[3] or "")
local item = {
type   = "item",
statut = CONFIG.statuts[statut] and statut or "planned",
titre  = titre,
desc   = nil,
tags   = {},
date   = nil,
lien   = nil,
}
for i = 4, #parts do
local key, val = parts[i]:match("^(%w+)%s*=%s*(.+)$")
if key and val then
key = key:lower()
if key == "desc" then
item.desc = val
elseif key == "tag" or key == "tags" then
for tag in val:gmatch("([^,]+)") do
table.insert(item.tags, trim(tag):lower())
end
elseif key == "date" then
item.date = val
elseif key == "lien" then
item.lien = val
end
end
end
table.insert(elements, item)
end
end
return elements
end

-- ------------------------------------------------------------
-- CALCUL PROGRESSION
-- ------------------------------------------------------------

local function calcProgression(elements)
local total = 0
local done  = 0
for _, el in ipairs(elements) do
if el.type == "item" then
if not CONFIG.ignored_statuts[el.statut] then
total = total + 1
if CONFIG.done_statuts[el.statut] then
done = done + 1
end
end
end
end
if total == 0 then return 0, 0, 0 end
return math.floor((done / total) * 100), done, total
end

-- ------------------------------------------------------------
-- GÉNÉRATEURS HTML
-- ------------------------------------------------------------

local function htmlBadge(statut)
local s = CONFIG.statuts[statut] or CONFIG.statuts.planned
return string.format(
'<span class="roadmap-badge" style="background:%s" title="%s">%s</span>',
s.color, escapeHtml(s.label), s.icon
)
end

local function htmlTags(tags)
if not tags or #tags == 0 then return "" end
local parts = {}
for _, tag in ipairs(tags) do
local cfg = CONFIG.tags[tag]
if cfg then
table.insert(parts, string.format(
'<span class="roadmap-tag" style="background:%s;color:%s">%s</span>',
cfg.bg, cfg.fg, escapeHtml(tag)
))
else
table.insert(parts, string.format(
'<span class="roadmap-tag">%s</span>',
escapeHtml(tag)
))
end
end
return '<div class="roadmap-tags">' .. table.concat(parts) .. '</div>'
end

local function htmlItem(item)
local statut = item.statut or "planned"
local titreTxt = escapeHtml(item.titre)
local titreHtml
if item.lien and item.lien ~= "" then
titreHtml = string.format('<a href="%s">%s</a>', escapeHtml(item.lien), titreTxt)
else
titreHtml = titreTxt
end
local descHtml = ""
if item.desc and item.desc ~= "" then
descHtml = string.format('<div class="roadmap-item-desc">%s</div>', escapeHtml(item.desc))
end
local dateHtml = ""
if item.date and item.date ~= "" then
dateHtml = string.format('<div class="roadmap-date">%s</div>', escapeHtml(item.date))
end
return string.format(
'<div class="roadmap-item roadmap-%s">'
.. '%s'
.. '<div class="roadmap-item-content">'
.. '<div class="roadmap-item-title">%s</div>'
.. '%s%s'
.. '</div>'
.. '%s'
.. '</div>',
statut, htmlBadge(statut), titreHtml, descHtml, htmlTags(item.tags), dateHtml
)
end

local function htmlSection(section)
return string.format(
'<div class="roadmap-section"><div class="roadmap-section-title">%s</div>',
escapeHtml(section.titre)
)
end

local function htmlProgressBar(pct, done, total)
return string.format(
'<div class="roadmap-progress-wrap">'
.. '<div class="roadmap-progress-label">Progression : <strong>%d%%</strong> — %d / %d fonctionnalités</div>'
.. '<div class="roadmap-progress-bar"><div class="roadmap-progress-fill" style="width:%d%%"></div></div>'
.. '</div>',
pct, done, total, pct
)
end

local function htmlLegende()
local parts = {}
local ordre = { "done", "inprogress", "planned", "idea", "cancelled" }
for _, key in ipairs(ordre) do
local s = CONFIG.statuts[key]
if s then
table.insert(parts, string.format(
'<span class="roadmap-legend-item">'
.. '<span class="roadmap-badge" style="background:%s;width:18px;height:18px;font-size:0.75em">%s</span>'
.. ' %s</span>',
s.color, s.icon, escapeHtml(s.label)
))
end
end
return '<div class="roadmap-legend">' .. table.concat(parts, "") .. '</div>'
end

local function htmlStats(elements)
local counts = {}
for key in pairs(CONFIG.statuts) do counts[key] = 0 end
for _, el in ipairs(elements) do
if el.type == "item" and counts[el.statut] then
counts[el.statut] = counts[el.statut] + 1
end
end
local parts = {}
local ordre = { "done", "inprogress", "planned", "idea", "cancelled" }
for _, key in ipairs(ordre) do
if counts[key] and counts[key] > 0 then
local s = CONFIG.statuts[key]
table.insert(parts, string.format(
'<span class="roadmap-stat" style="border-color:%s">'
.. '<span class="roadmap-stat-count" style="color:%s">%d</span>'
.. '<span class="roadmap-stat-label">%s</span>'
.. '</span>',
s.color, s.color, counts[key], escapeHtml(s.label)
))
end
end
return '<div class="roadmap-stats">' .. table.concat(parts, "") .. '</div>'
end

-- ------------------------------------------------------------
-- RENDER PRINCIPAL
-- ------------------------------------------------------------

function p.render(frame)
local args = frame:getParent().args
local titre = getParam(args, "titre") or "Roadmap"
local subtitle = getParam(args, "subtitle")
local rawContenu = trim(args["contenu"] or args[1] or "")
local showStats = getParam(args, "stats") ~= "non"
local showLegende = getParam(args, "legende") ~= "non"
local elements = parseContenu(rawContenu)
local pct, done, total = calcProgression(elements)
local html = {}
table.insert(html, '<div class="roadmap-container">')
-- Header
table.insert(html, '<div class="roadmap-header">')
table.insert(html, string.format('<div class="roadmap-header-title">%s</div>', escapeHtml(titre)))
if subtitle then
table.insert(html, string.format('<div class="roadmap-subtitle">%s</div>', escapeHtml(subtitle)))
end
table.insert(html, '</div>')
-- Stats
if showStats and #elements > 0 then
table.insert(html, htmlStats(elements))
end
-- Progression
if total > 0 then
table.insert(html, htmlProgressBar(pct, done, total))
end
-- Contenu
local inSection = false
for _, el in ipairs(elements) do
if el.type == "section" then
if inSection then
table.insert(html, '</div>')
end
table.insert(html, htmlSection(el))
inSection = true
elseif el.type == "item" then
table.insert(html, htmlItem(el))
end
end
if inSection then
table.insert(html, '</div>')
end
-- Légende
if showLegende then
table.insert(html, htmlLegende())
end
table.insert(html, '</div>')
return table.concat(html, "\n")
end

-- ------------------------------------------------------------
-- ITEM INLINE
-- ------------------------------------------------------------

function p.item(frame)
local args = frame.args
local statut = trim(args[1] or "planned"):lower()
local titre = trim(args[2] or "")
if not CONFIG.statuts[statut] then statut = "planned" end
local item = {
statut = statut,
titre = titre,
desc = getParam(args, "desc"),
date = getParam(args, "date"),
lien = getParam(args, "lien"),
tags = {},
}
local tagStr = getParam(args, "tag", "tags")
if tagStr then
for tag in tagStr:gmatch("([^,]+)") do
table.insert(item.tags, trim(tag):lower())
end
end
return htmlItem(item)
end

return p
Les témoins (''cookies'') nous aident à fournir nos services. En utilisant nos services, vous acceptez notre utilisation de témoins.