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 à 20:35 par Hiob (discussion | contributions) (Annulation des modifications 5246 de Hiob (discussion))

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

-- ------------------------------------------------------------
-- 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 vide ou commentaire
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" title="%s">%s</span>',
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
table.insert(parts, string.format(
'<span class="roadmap-tag" data-tag="%s">%s</span>',
escapeHtml(tag), 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 : <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="width:16px;height:16px;min-width:16px;font-size:0.7em">'
.. '%s</span> %s</span>',
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">'
.. '<span class="roadmap-stat-count">%d</span>'
.. '<span class="roadmap-stat-label">%s</span>'
.. '</span>',
counts[key], escapeHtml(s.label)
))
end
end
return '<div class="roadmap-stats">' .. table.concat(parts, "") .. '</div>'
end

-- ------------------------------------------------------------
-- RENDU 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><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></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 (usage standalone)
-- ------------------------------------------------------------

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.