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.
Version datée du 20 février 2026 à 23:03 par Hiob (discussion | contributions)

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

-- ============================================================
-- Module:Roadmap
-- Génère une roadmap visuelle pour wiki.nefald.fr
-- Intégré au design system Citizen
-- ============================================================

local p = {}

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

local CONFIG = {
statuts = {
done = {
label = "Terminé",
icon  = "✓",
order = 1,
},
inprogress = {
label = "En cours",
icon  = "◉",
order = 2,
},
planned = {
label = "Planifié",
icon  = "○",
order = 3,
},
idea = {
label = "Idée",
icon  = "✦",
order = 4,
},
cancelled = {
label = "Annulé",
icon  = "✕",
order = 5,
},
},
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("&", "&")
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

-- Sanitize un nom de tag pour l'utiliser comme classe CSS
-- Ne garde que lettres, chiffres et tirets
local function sanitizeClass(s)
if not s then return "" end
s = s:lower()
s = s:gsub("[^%w%-]", "")
return s
end

-- ------------------------------------------------------------
-- PARSEUR
-- ------------------------------------------------------------

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 roadmap-badge-%s" title="%s">%s</span>',
statut, 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 classe = sanitizeClass(tag)
table.insert(parts, string.format(
'<span class="roadmap-tag roadmap-tag-%s">%s</span>',
classe, escapeHtml(tag)
))
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\194\160: <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 roadmap-%s">'
.. '%s %s</span>',
key, htmlBadge(key), escapeHtml(s.label)
))
end
end
return '<div class="roadmap-legend">' .. table.concat(parts, "") .. '</div>'
end

local function htmlStats(elements)
local counts = { done = 0, inprogress = 0, planned = 0, idea = 0, cancelled = 0 }
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 order = {
{ key = "done",       label = "Terminé" },
{ key = "inprogress", label = "En cours" },
{ key = "planned",    label = "Planifié" },
{ key = "idea",       label = "Idée" },
{ key = "cancelled",  label = "Annulé" },
}
local parts = { '<div class="roadmap-stats">' }
for _, s in ipairs(order) do
if counts[s.key] > 0 then
table.insert(parts, string.format(
'<div class="roadmap-stat roadmap-stat-%s">'
.. '<span class="roadmap-stat-count">%d</span>'
.. '<span class="roadmap-stat-label">%s</span>'
.. '</div>',
s.key, counts[s.key], s.label
))
end
end
table.insert(parts, '</div>')
return table.concat(parts, "\n")
end

-- ------------------------------------------------------------
-- RENDU PRINCIPAL
-- ------------------------------------------------------------

function p.render(frame)
local args = frame:getParent().args
local rawContenu = args["contenu"] or args[1] or "(vide)"
return '<pre>' .. mw.text.nowiki(tostring(rawContenu)) .. '</pre>'
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.