« Module:Roadmap » : différence entre les versions
De Nefald
Autres actions
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 = {... » |
Aucun résumé des modifications |
||
| Ligne 7 : | Ligne 7 : | ||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
-- CONFIGURATION | -- CONFIGURATION | ||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
local CONFIG = { | 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 }, | |||
} | } | ||
| Ligne 68 : | Ligne 60 : | ||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
local function escapeHtml(s) | 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 | end | ||
local function trim(s) | local function trim(s) | ||
if not s then return "" end | |||
return s:match("^%s*(.-)%s*$") | |||
end | end | ||
local function getParam(args, ...) | 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 | end | ||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
-- PARSEUR | -- PARSEUR - adapté au nouveau format @section | ||
- | |||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
local function parseContenu(raw) | 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 | end | ||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
-- CALCUL | -- CALCUL PROGRESSION | ||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
local function calcProgression(elements) | 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 | end | ||
| Ligne 205 : | Ligne 164 : | ||
local function htmlBadge(statut) | 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 | end | ||
local function htmlTags(tags) | 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 | end | ||
local function htmlItem(item) | 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 | end | ||
local function htmlSection(section) | local function htmlSection(section) | ||
return string.format( | |||
'<div class="roadmap-section"><div class="roadmap-section-title">%s</div>', | |||
escapeHtml(section.titre) | |||
) | |||
end | end | ||
local function htmlProgressBar(pct, done, total) | 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 | end | ||
local function htmlLegende() | 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 | end | ||
local function htmlStats(elements) | 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 | end | ||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
-- | -- RENDER PRINCIPAL | ||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
function p.render(frame) | 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 | end | ||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
-- | -- ITEM INLINE | ||
-- ------------------------------------------------------------ | -- ------------------------------------------------------------ | ||
function p.item(frame) | 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 | 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("&", "&")
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
-- ------------------------------------------------------------
-- 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