« Module:Roadmap » : différence entre les versions
De Nefald
Autres actions
Aucun résumé des modifications |
Aucun résumé des modifications |
||
| (6 versions intermédiaires par le même utilisateur non affichées) | |||
| Ligne 2 : | Ligne 2 : | ||
-- Module:Roadmap | -- Module:Roadmap | ||
-- Génère une roadmap visuelle pour wiki.nefald.fr | -- Génère une roadmap visuelle pour wiki.nefald.fr | ||
-- Intégré au design system Citizen | |||
-- ============================================================ | -- ============================================================ | ||
| Ligne 14 : | Ligne 15 : | ||
done = { | done = { | ||
label = "Terminé", | label = "Terminé", | ||
icon = "✓ | icon = "✓", | ||
order = 1, | order = 1, | ||
}, | }, | ||
inprogress = { | inprogress = { | ||
label = "En cours", | label = "En cours", | ||
icon = "◉ | icon = "◉", | ||
order = 2, | order = 2, | ||
}, | }, | ||
planned = { | planned = { | ||
label = "Planifié", | label = "Planifié", | ||
icon = "○ | icon = "○", | ||
order = 3, | order = 3, | ||
}, | }, | ||
idea = { | idea = { | ||
label = "Idée", | label = "Idée", | ||
icon = "✦ | icon = "✦", | ||
order = 4, | order = 4, | ||
}, | }, | ||
cancelled = { | cancelled = { | ||
label = "Annulé", | label = "Annulé", | ||
icon = "✕ | icon = "✕", | ||
order = 5, | order = 5, | ||
}, | }, | ||
}, | }, | ||
done_statuts = { done = true }, | done_statuts = { done = true }, | ||
| Ligne 81 : | Ligne 68 : | ||
end | end | ||
return nil | return nil | ||
end | |||
local function sanitizeClass(s) | |||
if not s then return "" end | |||
s = s:lower() | |||
s = s:gsub("[^%w%-]", "") | |||
return s | |||
end | end | ||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
-- PARSEUR | -- PARSEUR (séparateur ;; au lieu de |) | ||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
| Ligne 99 : | Ligne 93 : | ||
titre = trim(titre or "Section"), | titre = trim(titre or "Section"), | ||
}) | }) | ||
elseif line:match("^item%s* | elseif line:match("^item%s*;;") then | ||
local parts = {} | local parts = {} | ||
for part in line:gmatch("( | for part in (line .. ";;"):gmatch("(.-);;") do | ||
table.insert(parts, trim(part)) | table.insert(parts, trim(part)) | ||
end | end | ||
-- parts[1] = "item", parts[2] = statut, parts[3] = titre, etc. | |||
local statut = trim(parts[2] or "planned"):lower() | local statut = trim(parts[2] or "planned"):lower() | ||
local titre = trim(parts[3] or "") | local titre = trim(parts[3] or "") | ||
| Ligne 166 : | Ligne 161 : | ||
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 | '<span class="roadmap-badge roadmap-badge-%s" title="%s">%s</span>', | ||
statut, escapeHtml(s.label), s.icon | |||
) | ) | ||
end | end | ||
| Ligne 175 : | Ligne 170 : | ||
local parts = {} | local parts = {} | ||
for _, tag in ipairs(tags) do | for _, tag in ipairs(tags) do | ||
table.insert(parts, string.format( | table.insert(parts, string.format( | ||
'<span class="roadmap-tag | '<span class="roadmap-tag roadmap-tag-%s">%s</span>', | ||
sanitizeClass(tag), escapeHtml(tag) | |||
escapeHtml(tag) | |||
)) | )) | ||
end | end | ||
return '<div class="roadmap-tags">' .. table.concat(parts) .. '</div>' | return '<div class="roadmap-tags">' .. table.concat(parts) .. '</div>' | ||
| Ligne 202 : | Ligne 189 : | ||
local descHtml = "" | local descHtml = "" | ||
if item.desc and item.desc ~= "" then | if item.desc and item.desc ~= "" then | ||
descHtml = string.format('<div class="roadmap-item-desc">%s</div>', escapeHtml(item.desc)) | descHtml = string.format( | ||
'<div class="roadmap-item-desc">%s</div>', | |||
escapeHtml(item.desc) | |||
) | |||
end | end | ||
local dateHtml = "" | local dateHtml = "" | ||
if item.date and item.date ~= "" then | if item.date and item.date ~= "" then | ||
dateHtml = string.format('<div class="roadmap-date">%s</div>', escapeHtml(item.date)) | dateHtml = string.format( | ||
'<div class="roadmap-date">%s</div>', | |||
escapeHtml(item.date) | |||
) | |||
end | end | ||
return string.format( | return string.format( | ||
| Ligne 231 : | Ligne 224 : | ||
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> — %d / %d fonctionnalités</div>' | .. '<div class="roadmap-progress-label">' | ||
.. '<div class="roadmap-progress-bar"><div class="roadmap-progress-fill" style="width:%d%%"></div></div>' | .. '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>', | .. '</div>', | ||
pct, done, total, pct | pct, done, total, pct | ||
| Ligne 245 : | Ligne 242 : | ||
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 roadmap-%s">' | ||
.. '%s %s</span>', | |||
.. ' %s</span>', | key, htmlBadge(key), escapeHtml(s.label) | ||
)) | )) | ||
end | end | ||
| Ligne 256 : | Ligne 252 : | ||
local function htmlStats(elements) | local function htmlStats(elements) | ||
local counts = { | local counts = { done = 0, inprogress = 0, planned = 0, idea = 0, cancelled = 0 } | ||
for _, el in ipairs(elements) do | for _, el in ipairs(elements) do | ||
if el.type == "item" and counts[el.statut] then | if el.type == "item" and counts[el.statut] then | ||
| Ligne 263 : | Ligne 258 : | ||
end | end | ||
end | end | ||
local | local order = { | ||
{ key = "done", label = "Terminé" }, | |||
for _, | { key = "inprogress", label = "En cours" }, | ||
if counts[ | { 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( | table.insert(parts, string.format( | ||
'< | '<div class="roadmap-stat roadmap-stat-%s">' | ||
.. '<span class="roadmap-stat-count | .. '<span class="roadmap-stat-count">%d</span>' | ||
.. '<span class="roadmap-stat-label">%s</span>' | .. '<span class="roadmap-stat-label">%s</span>' | ||
.. '</ | .. '</div>', | ||
s. | s.key, counts[s.key], s.label | ||
)) | )) | ||
end | end | ||
end | end | ||
table.insert(parts, '</div>') | |||
return table.concat(parts, "\n") | |||
end | end | ||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
-- | -- RENDU 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 rawContenu = trim(args["contenu"] or args[1] or "") | ||
-- | |||
local titre = getParam(args, "titre", "title") or "Roadmap" | |||
local subtitle = getParam(args, "subtitle") | local subtitle = getParam(args, "subtitle") | ||
-- | |||
local elements = parseContenu(rawContenu) | local elements = parseContenu(rawContenu) | ||
local pct, done, total = calcProgression(elements) | local pct, done, total = calcProgression(elements) | ||
local | -- | ||
table.insert( | local out = {} | ||
-- | table.insert(out, '<div class="roadmap-container">') | ||
table.insert( | -- En-tête | ||
table.insert( | table.insert(out, '<div class="roadmap-header">') | ||
table.insert(out, string.format('<div class="roadmap-title">%s</div>', escapeHtml(titre))) | |||
if subtitle then | if subtitle then | ||
table.insert( | table.insert(out, string.format('<div class="roadmap-subtitle">%s</div>', escapeHtml(subtitle))) | ||
end | end | ||
-- | table.insert(out, htmlProgressBar(pct, done, total)) | ||
local | table.insert(out, htmlStats(elements)) | ||
table.insert(out, htmlLegende()) | |||
table.insert(out, '</div>') | |||
-- Corps | |||
local sectionOpen = false | |||
for _, el in ipairs(elements) do | for _, el in ipairs(elements) do | ||
if el.type == "section" then | if el.type == "section" then | ||
if | if sectionOpen then | ||
table.insert( | table.insert(out, '</div>') | ||
end | end | ||
table.insert( | table.insert(out, htmlSection(el)) | ||
sectionOpen = true | |||
elseif el.type == "item" then | elseif el.type == "item" then | ||
table.insert( | table.insert(out, htmlItem(el)) | ||
end | end | ||
end | end | ||
if sectionOpen then | |||
if | table.insert(out, '</div>') | ||
table.insert( | |||
end | end | ||
table.insert( | table.insert(out, '</div>') | ||
return table.concat( | return table.concat(out, "\n") | ||
end | end | ||
| Ligne 345 : | Ligne 338 : | ||
local item = { | local item = { | ||
statut = statut, | statut = statut, | ||
titre = titre, | titre = titre, | ||
desc = getParam(args, "desc"), | desc = getParam(args, "desc"), | ||
date = getParam(args, "date"), | date = getParam(args, "date"), | ||
lien = getParam(args, "lien"), | lien = getParam(args, "lien"), | ||
tags = {}, | tags = {}, | ||
} | } | ||
local tagStr = getParam(args, "tag", "tags") | local tagStr = getParam(args, "tag", "tags") | ||
Dernière version du 20 février 2026 à 23:07
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("<", "<")
s = s:gsub(">", ">")
s = s:gsub('"', """)
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
local function sanitizeClass(s)
if not s then return "" end
s = s:lower()
s = s:gsub("[^%w%-]", "")
return s
end
-- ------------------------------------------------------------
-- PARSEUR (séparateur ;; au lieu de |)
-- ------------------------------------------------------------
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
-- parts[1] = "item", parts[2] = statut, parts[3] = titre, etc.
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
table.insert(parts, string.format(
'<span class="roadmap-tag roadmap-tag-%s">%s</span>',
sanitizeClass(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\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 = trim(args["contenu"] or args[1] or "")
--
local titre = getParam(args, "titre", "title") or "Roadmap"
local subtitle = getParam(args, "subtitle")
--
local elements = parseContenu(rawContenu)
local pct, done, total = calcProgression(elements)
--
local out = {}
table.insert(out, '<div class="roadmap-container">')
-- En-tête
table.insert(out, '<div class="roadmap-header">')
table.insert(out, string.format('<div class="roadmap-title">%s</div>', escapeHtml(titre)))
if subtitle then
table.insert(out, string.format('<div class="roadmap-subtitle">%s</div>', escapeHtml(subtitle)))
end
table.insert(out, htmlProgressBar(pct, done, total))
table.insert(out, htmlStats(elements))
table.insert(out, htmlLegende())
table.insert(out, '</div>')
-- Corps
local sectionOpen = false
for _, el in ipairs(elements) do
if el.type == "section" then
if sectionOpen then
table.insert(out, '</div>')
end
table.insert(out, htmlSection(el))
sectionOpen = true
elseif el.type == "item" then
table.insert(out, htmlItem(el))
end
end
if sectionOpen then
table.insert(out, '</div>')
end
table.insert(out, '</div>')
return table.concat(out, "\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