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 à 19:59 par 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 = {... »)
(diff) ← Version précédente | Version actuelle (diff) | Version suivante → (diff)

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 CENTRALE
-- Modifie ici les statuts, couleurs, icônes et tags
-- ------------------------------------------------------------

local CONFIG = {

  statuts = {
    done = {
      label   = "Terminé",
      icon    = "✓",
      color   = "#27ae60",
      order   = 1,   -- ordre d'affichage si tri activé
    },
    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 disponibles : nom = couleur de fond, couleur texte
  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" },
  },

  -- 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 },
}

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

-- Échappe le HTML pour éviter les injections
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

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

-- Récupère un paramètre nommé ou positionnel avec fallback
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 DE SECTIONS ET ITEMS
-- 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 elements = {}

  for line in (raw .. "\n"):gmatch("([^\n]*)\n") do
    line = trim(line)

    -- Ignorer les lignes vides et commentaires
    if line == "" or line:match("^%-%-") then
      -- skip

    -- Détecter une section : == Titre ==
    elseif line:match("^==.+==$") then
      local titre = line:match("^==%s*(.-)%s*==$")
      table.insert(elements, {
        type  = "section",
        titre = titre or "Section",
      })

    -- Détecter un item : item | statut | Titre | ...
    elseif line:match("^item%s*|") or line:match("^item%s*:") then
      -- Séparer par "|"
      local parts = {}
      for part in line:gmatch("([^|]+)") do
        table.insert(parts, trim(part))
      end
      -- parts[1] = "item", parts[2] = statut, parts[3] = titre, parts[4..n] = params

      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,
      }

      -- Parser les paramètres nommés dans parts[4..n]
      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
            -- Support multi-tags séparés par virgule
            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 DE LA 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
  local pct = math.floor((done / total) * 100)
  return pct, 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
      -- Tag inconnu : style neutre
      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)

  -- Lien sur le titre si défini
  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> &mdash; %d / %d fonctionnalités terminées</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 = {}
  -- Ordre d'affichage fixé
  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)
  -- Compte par statut
  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

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

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[1] or args["contenu"] or "")
  local showStats  = getParam(args, "stats") ~= "non"
  local showLegende = getParam(args, "legende") ~= "non"

  -- Parsing du contenu
  local elements = parseContenu(rawContenu)

  -- Calcul automatique de la progression
  local pct, done, total = calcProgression(elements)

  -- Construction du HTML
  local html = {}

  -- Conteneur principal
  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 globales
  if showStats and #elements > 0 then
    table.insert(html, htmlStats(elements))
  end

  -- Barre de progression
  if total > 0 then
    table.insert(html, htmlProgressBar(pct, done, total))
  end

  -- Contenu : sections et items
  local inSection = false
  for _, el in ipairs(elements) do
    if el.type == "section" then
      if inSection then
        table.insert(html, '</div>') -- ferme section précédente
      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>') -- 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

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

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.