Aller au contenu

Poképédia subit actuellement quelques soucis techniques !
L'équipe est au courant des problèmes et fait le nécessaire pour les arranger le plus rapidement possible. Merci de votre compréhension !

Module:Apprentissage/bac à sable

De Poképédia

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

--[[----------------------------------------------------------------------------
  Module:Apprentissage — proposition v2 (2026-08-17)

  Rétrocompatible : tout appel existant rend le même tableau, aux trois
  changements visuels du LOT 1 près (classes responsive, tri, compteur).
  Toute nouveauté passe par un paramètre nommé optionnel, éteint par défaut.

  LOT 0 — correctifs invisibles
    · les variables ne fuient plus dans le global (v1 : infos_capacite_split,
      jeu, i, infos_capacite)
    · « jeux » est copié avant d'être amputé : generation_to_jeux n'est plus muté
    · l'icône Capacité Plus porte enfin son propre alt (v1 : « Maîtrise »)
    · classes colonne-9 / colonne-10 au lieu de colonne-8 répété
    · guillemets et max-width uniformisés
    · une génération inconnue rend un message clair, au lieu de planter

  LOT 1 — visible, sans toucher aux pages
    · les classes qu'attend MediaWiki:Gadget-tableauxFlexibles.css sont enfin
      posées : tableau-apprentissage, apprentissage-usuel[-générationN],
      apprentissage-capsule[-générationN], apprentissage-donneur,
      apprentissage-niveau-Nniveaux. Sans elles, TOUT le CSS mobile du module
      est mort — c'est le cas aujourd'hui sur les 5 410 pages.
    · data-sort-value sur type, catégorie, puissance, précision, PP, coût,
      numéro de CT/DT : ces colonnes n'étaient pas triables (images sans texte)
    · un compteur « 57 capacités » À CÔTÉ du bouton — surtout pas dedans :
      MediaWiki:Common.js échange textContent et title à chaque clic, un
      compteur placé dans le bouton disparaîtrait au premier clic
    · le bouton et son compteur retrouvent une taille lisible : mesuré sur le
      rendu réel, le « font-size:80% » de la v1 donnait 11,2 px dans une cellule
      qui en fait 14, et une cible cliquable de 69 × 14 px — sous le minimum de
      24 × 24 px du critère WCAG 2.2 « Target Size (Minimum) »

  LOT 2 — nouveautés, sur paramètre
    · notes=          notes numérotées en pied de tableau, appelées par note(n)
    · colonnes-jeux=  une colonne ✔/— par jeu, pour les méthodes autres que niveau
    · légende=        un <caption> (visible et gras : cf. MediaWiki:Common.css)
    · couleur-type=   colore la cellule de type (classe td.<type> du gadget couleurs)
    · p.transfert     la méthode « par transfert », qui n'existait pas
    · colonnes-générations= sur p.transfert : une colonne par génération
      d'origine, comme Bulbapedia. 63 % de ses 3 293 tableaux de transfert en
      portent au moins deux, jusqu'à sept ; une colonne de texte ne peut pas
      porter cela. « depuis= » force la première colonne, « pokémon= » la racine
      des liens d'en-tête.
    · p.obscur        Colosseum / XD, sans catégoriser les capacités Ombre

  NON FAIT, et pourquoi : aria-expanded sur le bouton. MediaWiki:Common.js ne
  met pas l'attribut à jour au clic ; il deviendrait faux après le premier clic.
  À poser en même temps qu'un correctif du gadget, pas avant.

  Banc d'essai : tools/comparer_module_apprentissage.py (console Scribunto,
  aucune écriture). Cas de test : wiki/Tests_Module_Apprentissage.wiki
--------------------------------------------------------------------------]]--

local p = {}

local ressources = {
	["donneesCapacite"] = require("Module:Ressources/donneesCapacite")
}

local generation_actuelle = 9

local generation_to_jeux = {
	{"RB", "J"},                  -- gen 1
	{"OA", "C"},                  -- gen 2
	{"RS", "E", "RFVF"},          -- gen 3
	{"DP", "Pt", "HGSS"},         -- gen 4
	{"NB", "N2B2"},               -- gen 5
	{"XY", "ROSA"},               -- gen 6
	{"SL", "USUL", "LGPE"},       -- gen 7
	{{"EB", "DEPS"}, "LPA"},      -- gen 8, EB et DEPS dans la même colonne
	{"EV", "LPZA"},               -- gen 9
	{"VV"}                        -- gen 10
}

--------------------------------------------------------------------------------
-- Petits outils
--------------------------------------------------------------------------------

local function non_vide( valeur )
	if valeur == nil then return false end
	valeur = mw.text.trim( valeur )
	return valeur ~= "" and valeur ~= "-" and valeur ~= "—"
end

--- Copie de surface. Sans elle, amputer « jeux » ampute generation_to_jeux.
local function copier( tableau )
	local copie = {}
	for i, valeur in ipairs( tableau ) do copie[i] = valeur end
	return copie
end

--- Le nombre de colonnes communes à toutes les méthodes, concours compris.
local function nb_communes( generation )
	if generation == 3 or generation == 6 then return 9
	elseif generation == 4 then return 8
	else return 6
	end
end

local function lire_generation( frame )
	local generation = frame.args['génération']
	if generation == nil then return generation_actuelle end
	return tonumber( generation )
end

local function lire_type( frame )
	local poke_type = frame.args['type']
	if poke_type == nil then return "inconnu" end
	return mw.text.trim( string.lower( string.gsub( poke_type, "É", "é" ) ) )
end

--- Les jeux de la génération, ou ceux demandés. « sans » retire un jeu absent
--- de la méthode (LPA n'a ni capsule ni reproduction).
local function lire_jeux( frame, generation, sans )
	local jeux = frame.args['jeux']
	if jeux == nil then
		jeux = copier( generation_to_jeux[generation] )
		if sans ~= nil then
			for i = #jeux, 1, -1 do
				if jeux[i] == sans then table.remove( jeux, i ) end
			end
		end
	else
		jeux = mw.text.split( jeux, "/" )
		for i, jeu in ipairs( jeux ) do jeux[i] = mw.text.trim( jeu ) end
	end
	return jeux
end

local function oui( valeur )
	return valeur == "oui" or valeur == "1"
end

--------------------------------------------------------------------------------
-- LOT 1 — le tri
--------------------------------------------------------------------------------

--- Les valeurs BRUTES de la capacité, pour trier ce que donneesCapacite ne rend
--- qu'en images. Renvoie nil si la capacité est inconnue (ou génération future).
local function donnees_brutes( generation, capacite )
	local ok, data = pcall( mw.loadData,
		'Module:Data/Données Capacités Génération ' .. generation )
	if not ok or type( data ) ~= "table" then return nil end
	return data[capacite]
end

--- « ?? » (inconnu) et « — » (sans objet) doivent tomber au bas du tri, pas
--- se mêler aux nombres.
local function tri_nombre( valeur )
	if valeur == nil then return -2 end
	if valeur == 0 then return -1 end
	return valeur
end

local function attribut_tri( valeur )
	if valeur == nil then return "" end
	-- Pas de mw.text.nowiki ici : il échapperait des caractères qui n'ont aucun
	-- sens wiki dans un attribut. Seul le guillemet doit être neutralisé.
	return ' data-sort-value="'
		.. string.gsub( tostring( valeur ), '"', '&quot;' ) .. '"'
end

--------------------------------------------------------------------------------
-- LOT 2 — les notes de pied de tableau
--------------------------------------------------------------------------------

--- Découpe le paramètre « notes » : une note par ligne, numérotées dans l'ordre.
local function lire_notes( frame )
	local notes = frame.args['notes']
	if notes == nil or mw.text.trim( notes ) == "" then return {} end
	local lignes = mw.text.split( mw.text.trim( notes ), "\n" )
	local propres = {}
	for _, ligne in ipairs( lignes ) do
		if mw.text.trim( ligne ) ~= "" then
			table.insert( propres, mw.text.trim( ligne ) )
		end
	end
	return propres
end

--- Le nom de la capacité, débarrassé de son éventuel appel de note.
--- Les parenthèses autour de string.gsub sont indispensables : gsub renvoie
--- DEUX valeurs, et la seconde serait prise par mw.text.trim pour son charset —
--- le nom garderait alors ses espaces et la page tomberait en catégorie de
--- maintenance. C'est ce que le banc d'essai a attrapé.
local function nettoyer_nom( champ )
	return mw.text.trim( ( string.gsub( champ, "note%(%d+%)", "" ) ) )
end

--- Remplace les appels « note(2) » d'un champ par un renvoi cliquable.
local function appliquer_notes( texte, ancre )
	if texte == nil then return texte end
	return ( string.gsub( texte, "note%((%d+)%)", function( numero )
		return '<sup>[[#' .. ancre .. '-' .. numero .. '|' .. numero .. ']]</sup>'
	end ) )
end

local function pied_de_notes( notes, ancre, colspan )
	if #notes == 0 then return "" end
	local morceaux = { '<tfoot><tr><td class="colonne-toutes" colspan="' .. colspan
		.. '" style="text-align:left; font-size:90%">' }
	for numero, texte in ipairs( notes ) do
		table.insert( morceaux, '<span id="' .. ancre .. '-' .. numero .. '">'
			.. numero .. '.</span> ' .. texte .. '<br />' )
	end
	table.insert( morceaux, '</td></tr></tfoot>' )
	return table.concat( morceaux, "" )
end

--------------------------------------------------------------------------------
-- Le squelette commun : classes, en-têtes, cellules, bouton
--------------------------------------------------------------------------------

--- LOT 1 — les classes que MediaWiki:Gadget-tableauxFlexibles.css attend.
--- « famille » vaut "usuel" ou "capsule" ; « suffixes » ajoute par exemple
--- "apprentissage-donneur" ou "apprentissage-niveau-2niveaux".
local function classes_table( famille, generation, poke_type, suffixes )
	local classes = { "tableaustandard", poke_type, "centre", "sortable",
		"tableau-overflow", "tableau-apprentissage",
		"apprentissage-" .. famille,
		"apprentissage-" .. famille .. "-génération" .. generation }
	for _, suffixe in ipairs( suffixes or {} ) do
		table.insert( classes, suffixe )
	end
	return table.concat( classes, " " )
end

local function ouvrir_table( famille, generation, poke_type, suffixes, legende )
	local html = '<div class="center"><table class="'
		.. classes_table( famille, generation, poke_type, suffixes )
		.. '" style="max-width: 100%;">'
	if legende ~= nil and mw.text.trim( legende ) ~= "" then
		html = html .. '<caption>' .. mw.text.trim( legende ) .. '</caption>'
	end
	return html .. '<thead>'
end

--- Les six (ou huit, ou neuf) en-têtes communs. « depart » vaut 1, sauf pour
--- capsule et disque où la colonne 1 est le numéro.
--- Les libellés d'en-tête, repris À LA LETTRE de la v1 : ils diffèrent d'une
--- méthode à l'autre (capsule ne lie rien, reproduction abrège, donneur écrit
--- « [[Précision|Précision]] »). Les reproduire exactement est ce qui rend la
--- comparaison v1/v2 concluante.
local TITRES = {
	niveau = { '[[Capacité|Nom]]', '[[Type]]', '[[Catégorie de capacité|Catégorie]]',
		'[[Puissance (statistique)|Puissance]]', '[[Précision]]', '[[PP]]' },
	capsule = { 'Capacité', 'Type', 'Catégorie', 'Puissance', 'Précision', 'PP' },
	reproduction = { '[[Capacité]]', '[[Type]]', '[[Catégorie de capacité|Cat.]]',
		'[[Puissance (statistique)|Puis.]]', '[[Précision|Préc.]]', '[[PP]]' },
	usuel = { '[[Capacité]]', '[[Type]]', '[[Catégorie de capacité|Catégorie]]',
		'[[Puissance (statistique)|Puissance]]', '[[Précision|Précision]]', '[[PP]]' },
}

local function entetes_communes( generation, depart, titres )
	local html = {}
	for i, titre in ipairs( titres ) do
		table.insert( html, '<th class="colonne-' .. ( depart + i - 1 )
			.. '" scope="col">' .. titre .. '</th>' )
	end
	if generation == 3 or generation == 4 or generation == 6 then
		table.insert( html, '<th class="colonne-' .. ( depart + 6 )
			.. '" scope="col">[[Concours Pokémon|Type Concours]]</th>' )
		table.insert( html, '<th class="colonne-' .. ( depart + 7 )
			.. '" scope="col">[[Charme (concours)|Charme]]</th>' )
		if generation == 3 or generation == 6 then
			table.insert( html, '<th class="colonne-' .. ( depart + 8 )
				.. '" scope="col">[[Blocage (concours)|Blocage]]</th>' )
		end
	end
	return table.concat( html, "" )
end

--- Les cellules communes d'une ligne, avec leur valeur de tri.
--- « suffixe_nom » colle un <sup> après le nom (capsule : le jeu du numéro).
local function cellules_communes( donnees, brutes, generation, depart, options )
	options = options or {}
	local html = {}
	local nom = donnees[1] .. ( options.suffixe_nom or "" )
	table.insert( html, '<td class="colonne-' .. depart .. '">' .. nom .. '</td>' )

	local classe_type = 'colonne-' .. ( depart + 1 )
	if options.couleur_type and brutes ~= nil and brutes[1] ~= nil then
		classe_type = classe_type .. ' '
			.. string.lower( ( string.gsub( brutes[1], "É", "é" ) ) )
	end
	local tris = {
		brutes and brutes[1] or nil,
		brutes and brutes[2] or nil,
		brutes and tri_nombre( brutes[3] ) or nil,
		brutes and tri_nombre( brutes[4] ) or nil,
		brutes and tri_nombre( brutes[5] ) or nil,
	}
	table.insert( html, '<td class="' .. classe_type .. '"' .. attribut_tri( tris[1] )
		.. '>' .. donnees[2] .. '</td>' )
	for i = 3, 6 do
		table.insert( html, '<td class="colonne-' .. ( depart + i - 1 ) .. '"'
			.. attribut_tri( tris[i - 1] ) .. '>' .. donnees[i] .. '</td>' )
	end

	if generation == 3 or generation == 4 or generation == 6 then
		table.insert( html, '<td class="colonne-' .. ( depart + 6 ) .. '"'
			.. attribut_tri( brutes and brutes[6] or nil ) .. '>' .. donnees[7] .. '</td>' )
		table.insert( html, '<td class="colonne-' .. ( depart + 7 ) .. '"'
			.. attribut_tri( brutes and tri_nombre( brutes[7] ) or nil ) .. '>'
			.. donnees[8] .. '</td>' )
		if generation == 3 or generation == 6 then
			table.insert( html, '<td class="colonne-' .. ( depart + 8 ) .. '"'
				.. attribut_tri( brutes and tri_nombre( brutes[8] ) or nil ) .. '>'
				.. donnees[9] .. '</td>' )
		end
	end
	return table.concat( html, "" )
end

--- Le nom que partagent le bouton et le tbody qu'il replie. MediaWiki:Common.js
--- pilote TOUS les tbody de ce nom : deux tableaux de même méthode et de même
--- génération sur une page se replient donc ensemble. Cela arrive sur 21 % des
--- pages (formes régionales : sur Raichu, ListeCapsules8 existe deux fois).
--- « identifiant= » permet de les distinguer ; sans lui, rien ne change.
local function nom_du_groupe( frame, nom_classe, generation )
	local nom = nom_classe .. generation
	local identifiant = frame.args['identifiant']
	if identifiant ~= nil and mw.text.trim( identifiant ) ~= "" then
		-- Un nom de classe CSS ne tolère ni espace ni accent.
		identifiant = mw.ustring.gsub( mw.text.trim( identifiant ), "[^%w]", "" )
		if identifiant ~= "" then nom = nom .. "-" .. identifiant end
	end
	return nom
end

--- La ligne du bouton. Le compteur est un <span> HORS du bouton : Common.js
--- échange textContent et title au clic et l'effacerait.
---
--- LOT 1 — la taille. Mesuré sur le rendu réel : le « font-size:80% » de la v1
--- donnait **11,2 px** dans une cellule qui en fait 14, et la cible cliquable
--- mesurait 69 × 14 px — sous le minimum de 24 × 24 px du critère WCAG 2.2
--- « Target Size (Minimum) ». Un <button> n'hérite pas non plus de la police du
--- tableau : le navigateur lui impose Arial, alors que la page est en sans-serif.
--- D'où : taille pleine, police héritée, et un peu de rembourrage pour la cible.
--- Le compteur reste secondaire, mais lisible (90 %, soit 12,6 px).
local STYLE_BOUTON = "font-size:100%; font-family:inherit; padding:3px 6px; line-height:1.4"
local STYLE_COMPTEUR = "font-size:90%; color:#54595d"

local function ligne_bouton( frame, nom_classe, generation, colspan, total )
	local groupe = nom_du_groupe( frame, nom_classe, generation )
	local bouton
	if oui( frame.args['masquer'] ) then bouton = { "afficher", "[-] Masquer", "[+] Afficher" }
	else bouton = { "masquer", "[+] Afficher", "[-] Masquer" }
	end
	local compteur = ""
	if total ~= nil and total > 0 then
		local mot = " capacités"
		if total == 1 then mot = " capacité" end
		compteur = ' <span class="apprentissage-compteur" style="' .. STYLE_COMPTEUR
			.. '">' .. total .. mot .. '</span>'
	end
	return '<tr><td class="colonne-toutes" colspan="' .. colspan
		.. '"><button type="button" name="' .. groupe
		.. '" value="' .. bouton[1] .. '" style="' .. STYLE_BOUTON .. '" title="'
		.. bouton[2] .. '">' .. bouton[3] .. '</button>' .. compteur
		.. '</td></tr></thead><tbody class="' .. groupe .. '">'
end

local function ligne_vide( colspan, phrase )
	return '</thead><tbody><tr><td class="colonne-toutes" colspan="' .. colspan
		.. '">' .. phrase .. '</td></tr>'
end

--- Le wiki nomme ses catégories de maintenance « Page avec un X incorrect »
--- (nom de capacité, nom de Pokémon, paramètre de distribution). On suit le
--- même schéma. La catégorie reste à créer — une ligne, et elle listera d'elle
--- même les pages fautives.
local function generation_invalide( generation )
	return '<span class="error">Module:Apprentissage : génération «&#160;'
		.. tostring( generation ) .. '&#160;» inconnue.</span>'
		.. '[[Catégorie:Page avec un paramètre d\'apprentissage incorrect]]'
end

--------------------------------------------------------------------------------
-- LOT 2 — les colonnes par jeu, pour les méthodes autres que « niveau »
--------------------------------------------------------------------------------

--- Découpe une ligne et sépare les champs de la méthode des colonnes de jeu.
--- Actif seulement si colonnes-jeux=oui ; sinon la ligne garde sa forme d'origine.
local function colonnes_de_jeux( frame, generation )
	if not oui( frame.args['colonnes-jeux'] ) then return nil end
	local jeux = frame.args['jeux']
	if jeux == nil then jeux = copier( generation_to_jeux[generation] )
	else
		jeux = mw.text.split( jeux, "/" )
		for i, jeu in ipairs( jeux ) do jeux[i] = mw.text.trim( jeu ) end
	end
	return jeux
end

--- Les colonnes de jeu réutilisent la classe « colonne-niveau » : pour que le
--- CSS du gadget les prenne en charge, la table doit porter le compte, comme
--- le fait déjà p.niveau.
local function suffixes_jeux( colonnes_jeux )
	if colonnes_jeux == nil or #colonnes_jeux == 0 then return {} end
	return { "apprentissage-niveau-" .. #colonnes_jeux .. "niveaux" }
end

local function entetes_jeux( frame, jeux )
	if jeux == nil then return "" end
	local html = {}
	for _, jeu in ipairs( jeux ) do
		local libelle
		if type( jeu ) == "table" then
			local abreviations = {}
			for _, un in ipairs( jeu ) do
				table.insert( abreviations,
					frame:expandTemplate{ title = "Abréviation", args = { un, "" } } )
			end
			libelle = table.concat( abreviations, ", " )
		else
			libelle = frame:expandTemplate{ title = "Abréviation", args = { jeu, "" } }
		end
		-- Volontairement « colonne-niveau », et non une classe neuve : le gadget
		-- CSS style déjà cette classe (.apprentissage-niveau-Nniveaux .colonne-niveau).
		-- Réutiliser ce qui existe évite de demander la moindre ligne de CSS.
		table.insert( html, '<th class="colonne-niveau" scope="col">' .. libelle .. '</th>' )
	end
	return table.concat( html, "" )
end

--- Une cellule par jeu : ✔ quand le champ est rempli, — sinon. Un texte libre
--- (« N.15 », « CT12 ») est affiché tel quel.
local function cellules_jeux( champs, premier, jeux, ancre )
	if jeux == nil then return "" end
	local html = {}
	for i = 1, #jeux do
		local valeur = champs[premier + i - 1]
		if non_vide( valeur ) then
			valeur = mw.text.trim( valeur )
			if valeur == "oui" or valeur == "x" or valeur == "X" then valeur = "✔" end
			table.insert( html, '<td class="colonne-niveau" data-sort-value="1">'
				.. appliquer_notes( valeur, ancre ) .. '</td>' )
		else
			table.insert( html, '<td class="colonne-niveau" data-sort-value="0">—</td>' )
		end
	end
	return table.concat( html, "" )
end

--------------------------------------------------------------------------------
-- LOT 2 — les colonnes par GÉNÉRATION D'ORIGINE, pour le transfert
--------------------------------------------------------------------------------
--
-- Bulbapedia ne met pas « d'où vient l'attaque » dans une colonne de texte : il
-- met **une colonne par génération antérieure**, et coche celles où l'attaque
-- s'apprenait. Mesuré sur notre cache (6 769 pages, le 2026-08-17) :
--
--     3 293 tableaux « by transfer », 43 073 lignes
--     1 colonne : 1 203   ·   2 : 848   ·   3 : 471   ·   4 : 213
--     5 : 212   ·   6 : 209   ·   7 : 137
--
-- Soit **2 090 tableaux (63 %) à deux colonnes ou plus**, jusqu'à sept. Une
-- seule colonne « Provenance » ne peut pas porter cela sans devenir du texte
-- non triable. D'où ces colonnes-ci.
--
-- Sur Poképédia, les 323 tableaux de transfert existants n'écrivent AUCUNE
-- provenance : ils passent une simple liste de noms à p.niveau, qui affiche
-- alors des colonnes de niveau vides. L'information existe chez Bulbapedia et
-- se perd chez nous ; ces colonnes sont l'endroit où la remettre.

local ROMAINS = { "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X" }

--- « 2,3,4 » ou « II, III » -> { [2]=true, [3]=true, [4]=true }.
--- Un « * » collé au chiffre note une génération où l'attaque ne s'obtenait que
--- par événement ou capacité spéciale : c'est le fond rayé de Bulbapedia.
local function ensemble_generations( champ )
	local ensemble = {}
	if champ == nil then return ensemble end
	for _, morceau in ipairs( mw.text.split( champ, "," ) ) do
		morceau = mw.text.trim( morceau )
		local special = false
		if string.sub( morceau, -1 ) == "*" then
			special = true
			morceau = mw.text.trim( string.sub( morceau, 1, -2 ) )
		end
		local numero = tonumber( morceau )
		if numero == nil then
			for i, romain in ipairs( ROMAINS ) do
				if string.upper( morceau ) == romain then numero = i end
			end
		end
		if numero ~= nil then
			ensemble[numero] = special and "special" or true
		end
	end
	return ensemble
end

--- Les générations à afficher : celles qui précèdent la génération du tableau,
--- bornées par « depuis= », et **amputées de celles que personne ne remplit** —
--- même règle que les colonnes de jeu vides de p.niveau. Sans elle, un Pokémon
--- de neuvième génération recevrait huit colonnes toutes vides.
local function colonnes_de_generations( frame, generation, lignes )
	if not oui( frame.args['colonnes-générations'] ) then return nil end
	local depuis = tonumber( frame.args['depuis'] or "" ) or 1
	if depuis < 1 then depuis = 1 end

	local remplies = {}
	for _, ligne in ipairs( lignes ) do
		local champs = mw.text.split( ligne, "/" )
		for numero in pairs( ensemble_generations( champs[2] ) ) do
			remplies[numero] = true
		end
	end

	local liste = {}
	for g = depuis, generation - 1 do
		if remplies[g] or frame.args['depuis'] ~= nil then
			table.insert( liste, g )
		end
	end
	return liste
end

--- Le titre de la sous-page de génération du Pokémon de la page courante, quand
--- elle existe. « rootText » vaut « Bulbizarre » aussi bien depuis la page
--- principale que depuis « Bulbizarre/Génération 2 ». Le test d'existence est
--- une fonction coûteuse : une seule par colonne, jamais par cellule, et sous
--- pcall pour qu'un dépassement de quota n'efface pas le tableau.
local function lien_de_generation( frame, numero )
	local racine = frame.args['pokémon']
	if racine == nil or mw.text.trim( racine ) == "" then
		racine = mw.title.getCurrentTitle().rootText
	end
	local cible = mw.text.trim( racine ) .. "/Génération " .. numero
	local ok, page = pcall( mw.title.new, cible )
	if ok and page ~= nil then
		local vu, existe = pcall( function() return page.exists end )
		if vu and existe then
			return '[[' .. cible .. '|' .. ROMAINS[numero] .. ']]'
		end
	end
	return ROMAINS[numero]
end

--- Les largeurs mobiles du gadget s'arrêtent à trois colonnes étroites
--- (.apprentissage-niveau-1/2/3niveaux). Au-delà, la classe est posée quand
--- même : cinq lignes de CSS suffiraient à l'honorer, et sans elles les
--- cellules se rangent d'elles-mêmes — aucune n'est cassée.
local function suffixes_generations( generations )
	if generations == nil or #generations == 0 then return {} end
	return { "apprentissage-niveau-" .. #generations .. "niveaux" }
end

local function entetes_generations( frame, generations )
	if generations == nil then return "" end
	local html = {}
	for _, numero in ipairs( generations ) do
		table.insert( html, '<th class="colonne-niveau" scope="col" title="Génération '
			.. ROMAINS[numero] .. '">' .. lien_de_generation( frame, numero ) .. '</th>' )
	end
	return table.concat( html, "" )
end

local function cellules_generations( champ, generations )
	if generations == nil then return "" end
	local ensemble = ensemble_generations( champ )
	local html = {}
	for _, numero in ipairs( generations ) do
		local etat = ensemble[numero]
		if etat == "special" then
			table.insert( html, '<td class="colonne-niveau" data-sort-value="2"'
				.. ' title="Uniquement par événement ou capacité spéciale">✦</td>' )
		elseif etat then
			table.insert( html, '<td class="colonne-niveau" data-sort-value="1">✔</td>' )
		else
			table.insert( html, '<td class="colonne-niveau" data-sort-value="0">—</td>' )
		end
	end
	return table.concat( html, "" )
end

--------------------------------------------------------------------------------
-- p.niveau
--------------------------------------------------------------------------------

function p.niveau( frame )
	local infos = frame.args[1]
	if infos == nil then return "Aucune information donnée" else infos = mw.text.trim( infos ) end
	infos = mw.text.split( infos, "\n" )
	local generation = lire_generation( frame )
	if generation == nil or generation_to_jeux[generation] == nil then
		return generation_invalide( frame.args['génération'] )
	end
	local poke_type = lire_type( frame )
	local notes = lire_notes( frame )
	local ancre = "note-niveau-" .. generation

	local generation_to_colspans = {
		{6, 2}, {6, 2}, {9, 3}, {8, 3}, {6, 2}, {9, 2}, {6, 3}, {6, 2}, {6, 2}, {6, 1}
	}
	local colspans = { generation_to_colspans[generation][1],
		generation_to_colspans[generation][2] }
	local nb_jeux_possibles = generation_to_colspans[generation][2]

	-- Les colonnes de jeu sans aucune donnée n'apparaissent pas.
	local presence = {}
	for j = 1, nb_jeux_possibles do presence[j] = false end
	local total = 0
	for _, ligne in ipairs( infos ) do
		local champs = mw.text.split( ligne, "/" )
		if champs[1] ~= nil and mw.text.trim( champs[1] ) ~= "" then total = total + 1 end
		for j = 1, nb_jeux_possibles do
			if non_vide( champs[j + 1] ) then presence[j] = true end
		end
	end
	for j = 1, nb_jeux_possibles do
		if presence[j] == false then colspans[2] = colspans[2] - 1 end
	end
	colspans[3] = colspans[1] + colspans[2]

	local jeux = {}
	for j = 1, nb_jeux_possibles do
		if presence[j] then table.insert( jeux, generation_to_jeux[generation][j] ) end
	end

	-- Le gadget ne définit que 1niveaux, 2niveaux et 3niveaux : quand aucune
	-- colonne de niveau n'est renseignée, il n'y a pas de classe à poser.
	local suffixes = {}
	if colspans[2] > 0 then
		table.insert( suffixes, "apprentissage-niveau-" .. colspans[2] .. "niveaux" )
	end

	local resultat = {}
	table.insert( resultat, ouvrir_table( "usuel", generation, poke_type, suffixes,
		frame.args['légende'] ) )
	table.insert( resultat, '<tr><th class="cellule-entête" colspan="' .. colspans[1]
		.. '" scope="colgroup">[[Capacité]]</th>' )
	if colspans[2] > 0 then
		table.insert( resultat, '<th class="cellule-entête" colspan="' .. colspans[2]
			.. '" scope="colgroup">[[Niveau]]</th>' )
	end
	table.insert( resultat, '</tr><tr>'
		.. entetes_communes( generation, 1, TITRES.niveau ) )
	for _, jeux_colonne in ipairs( jeux ) do
		if type( jeux_colonne ) == "string" then
			table.insert( resultat, '<th class="colonne-niveau" scope="col">'
				.. frame:expandTemplate{ title = "Abréviation", args = { jeux_colonne, "" } }
				.. '</th>' )
		else
			local abreviations = {}
			for _, jeu_colonne in ipairs( jeux_colonne ) do
				table.insert( abreviations,
					frame:expandTemplate{ title = "Abréviation", args = { jeu_colonne, "" } } )
			end
			table.insert( resultat, '<th class="colonne-niveau" scope="col">'
				.. table.concat( abreviations, ", " ) .. '</th>' )
		end
	end
	table.insert( resultat, '</tr>' )

	if #infos == 1 and infos[1] == "Aucune" then
		table.insert( resultat, ligne_vide( colspans[3],
			'Ce Pokémon n\'apprend aucune capacité par montée en niveau lors de cette génération.' ) )
	else
		table.insert( resultat, ligne_bouton( frame, "ListeCapacites", generation,
			colspans[3], total ) )

		for _, ligne in ipairs( infos ) do
			local champs = mw.text.split( ligne, "/" )
			if champs[1] ~= nil and champs[1] ~= "" then
				local nom_capacite = nettoyer_nom( champs[1] )
				local jeux_capacite = {}
				for j = 1, nb_jeux_possibles do
					if non_vide( champs[j + 1] ) then
						table.insert( jeux_capacite, generation_to_jeux[generation][j] )
					end
				end

				local donnees = ressources.donneesCapacite( generation, nom_capacite, jeux_capacite )
				local brutes = donnees_brutes( generation, nom_capacite )
				local renvoi = appliquer_notes(
					string.match( champs[1], "note%(%d+%)" ) or "", ancre )
				table.insert( resultat, '<tr>' .. cellules_communes( donnees, brutes,
					generation, 1, { suffixe_nom = renvoi,
					                 couleur_type = oui( frame.args['couleur-type'] ) } ) )

				for j = 2, nb_jeux_possibles + 1 do
					if presence[j - 1] then
						local contenu, valeur_tri = "—", 101
						if champs[j] ~= nil then
							contenu, valeur_tri = "", nil
							local morceaux = mw.text.split( mw.text.trim( champs[j] ), ", " )
							local est_sup = true
							for k, morceau in ipairs( morceaux ) do
								local maitrise, capacite_plus = false, false
								if mw.ustring.find( morceau, "M" ) == 1 then
									maitrise = true
									morceau = mw.ustring.sub( morceau, 2 )
								elseif mw.ustring.find( morceau, "P" ) == 1 then
									capacite_plus = true
									morceau = mw.ustring.sub( morceau, 2 )
								end

								if morceau == "—" or morceau == "-" or morceau == "" then
									morceau = "—"
									if valeur_tri == nil then valeur_tri = 101 end
									est_sup = false
								elseif string.find( morceau, "Départ" ) or string.find( morceau, "Évolution" ) then
									if valeur_tri == nil then valeur_tri = 0 end
									est_sup = false
								elseif tonumber( morceau ) ~= nil then
									if valeur_tri == nil then valeur_tri = tonumber( morceau ) end
									morceau = "N." .. morceau
									est_sup = false
								else
									if valeur_tri == nil then valeur_tri = 0 end
									est_sup = not est_sup
								end

								if k ~= 1 and not est_sup then contenu = contenu .. "<br/>" end
								if maitrise then
									contenu = contenu ..
										"[[Fichier:Icône Maîtrise LPA.png|x20px|Maîtrise|link=Maîtrise]]"
								end
								if capacite_plus then
									contenu = contenu ..
										"[[Fichier:Icône Capacité Plus LPZA.png|x20px|Capacité Plus|link=Capacité Plus]]"
								end

								if est_sup then
									contenu = contenu .. frame:expandTemplate{ title = 'Sup', args = { morceau } }
								else
									contenu = contenu .. morceau
								end
							end
						end
						table.insert( resultat, '<td class="colonne-niveau" style="white-space:nowrap"'
							.. ' data-sort-value="' .. tostring( valeur_tri or 101 ) .. '">'
							.. appliquer_notes( contenu, ancre ) .. '</td>' )
					end
				end
				table.insert( resultat, '</tr>' )
			end
		end
	end

	table.insert( resultat, '</tbody>' .. pied_de_notes( notes, ancre, colspans[3] )
		.. '</table></div>' )
	return table.concat( resultat, "" )
end

--------------------------------------------------------------------------------
-- p.capsule / p.disque
--------------------------------------------------------------------------------

--- Les capacités qui sont des CS selon la génération. « depend » : cela dépend
--- du jeu, précisé en 3e champ.
local CAPACITES_SECRETES = {
	["Coupe"]       = {true, true, true, true, true, true},
	["Vol"]         = {true, true, true, true, true, true},
	["Surf"]        = {true, true, true, true, true, true},
	["Force"]       = {true, true, true, true, true, true},
	["Flash"]       = {true, true, true, false, false, false},
	["Siphon"]      = {false, true, false, "depend", false, false},
	["Cascade"]     = {false, true, true, true, true, true},
	["Éclate-Roc"]  = {false, false, true, true, false, "depend"},
	["Plongée"]     = {false, false, "depend", false, true, "depend"},
	["Anti-Brume"]  = {false, false, false, "depend", false, false},
	["Escalade"]    = {false, false, false, true, false, false}
}
CAPACITES_SECRETES["Plongée"]["rse"] = true
CAPACITES_SECRETES["Plongée"]["rfvf"] = false
CAPACITES_SECRETES["Siphon"]["dpp"] = false
CAPACITES_SECRETES["Siphon"]["hgss"] = true
CAPACITES_SECRETES["Anti-Brume"]["dpp"] = true
CAPACITES_SECRETES["Anti-Brume"]["hgss"] = false
CAPACITES_SECRETES["Éclate-Roc"]["xy"] = false
CAPACITES_SECRETES["Éclate-Roc"]["rosa"] = true
CAPACITES_SECRETES["Plongée"]["xy"] = false
CAPACITES_SECRETES["Plongée"]["rosa"] = true

local function sigle_capsule( disque, capacite, generation, jeu )
	if disque then return 'DT' end
	local regle = CAPACITES_SECRETES[capacite]
	if regle == nil then return 'CT' end
	if regle[generation] == true then return 'CS' end
	if regle[generation] == "depend" and jeu ~= nil
			and regle[mw.text.trim( string.lower( jeu ) )] == true then
		return 'CS'
	end
	return 'CT'
end

local function capsule_et_disque( disque, frame )
	local infos = frame.args[1]
	if infos == nil then return "Aucune information donnée" else infos = mw.text.trim( infos ) end
	local generation = lire_generation( frame )
	if generation == nil or generation_to_jeux[generation] == nil then
		return generation_invalide( frame.args['génération'] )
	end
	local poke_type = lire_type( frame )
	local jeux = lire_jeux( frame, generation, generation == 8 and "LPA" or nil )
	local notes = lire_notes( frame )
	local colonnes_jeux = colonnes_de_jeux( frame, generation )
	local mot = disque and 'disque' or 'capsule'
	local nom_classe = disque and 'ListeDisques' or 'ListeCapsules'
	local ancre = "note-" .. mot .. "-" .. generation

	local colspan = 1 + nb_communes( generation )
		+ ( colonnes_jeux and #colonnes_jeux or 0 )

	local resultat = {}
	table.insert( resultat, ouvrir_table( "capsule", generation, poke_type,
		suffixes_jeux( colonnes_jeux ), frame.args['légende'] ) )
	table.insert( resultat, '<tr><th class="colonne-1" scope="col">Numéro</th>' )
	table.insert( resultat, entetes_communes( generation, 2, TITRES.capsule ) )
	table.insert( resultat, entetes_jeux( frame, colonnes_jeux ) )
	table.insert( resultat, '</tr>' )

	if infos == "Aucune" then
		table.insert( resultat, ligne_vide( colspan,
			'Ce Pokémon n\'apprend aucune capacité par ' .. mot .. ' lors de cette génération.' ) )
	else
		local lignes = mw.text.split( infos, "\n" )
		local total = 0
		for _, ligne in ipairs( lignes ) do
			local champs = mw.text.split( ligne, "/" )
			if non_vide( champs[1] ) and non_vide( champs[2] ) then total = total + 1 end
		end
		table.insert( resultat, ligne_bouton( frame, nom_classe, generation, colspan, total ) )

		for _, ligne in ipairs( lignes ) do
			local champs = mw.text.split( ligne, "/" )
			if champs[1] ~= nil and champs[1] ~= "" and champs[2] ~= nil and champs[2] ~= "" then
				local numero = mw.text.trim( champs[1] )
				local nom_capacite = nettoyer_nom( champs[2] )
				local sigle = sigle_capsule( disque, nom_capacite, generation, champs[3] )
				local donnees = ressources.donneesCapacite( generation, nom_capacite, jeux )
				local brutes = donnees_brutes( generation, nom_capacite )

				local suffixe = appliquer_notes(
					string.match( champs[2], "note%(%d+%)" ) or "", ancre )
				if champs[3] and not colonnes_jeux then
					suffixe = suffixe .. '<sup>'
						.. frame:expandTemplate{ title = 'Abréviation', args = { champs[3] } }
						.. '</sup>'
				end

				table.insert( resultat, '<tr><td class="colonne-1" data-sort-value="'
					.. ( tonumber( ( string.gsub( numero, "%D", "" ) ) ) or 0 ) .. '">[['
					.. sigle .. numero .. ']]</td>' )
				table.insert( resultat, cellules_communes( donnees, brutes, generation, 2,
					{ suffixe_nom = suffixe,
					  couleur_type = oui( frame.args['couleur-type'] ) } ) )
				table.insert( resultat, cellules_jeux( champs, 3, colonnes_jeux, ancre ) )
				table.insert( resultat, '</tr>' )
			end
		end
	end

	table.insert( resultat, '</tbody>' .. pied_de_notes( notes, ancre, colspan )
		.. '</table></div>' )
	return table.concat( resultat, "" )
end

function p.capsule( frame )
	return capsule_et_disque( false, frame )
end

function p.disque( frame )
	return capsule_et_disque( true, frame )
end

--------------------------------------------------------------------------------
-- p.reproduction
--------------------------------------------------------------------------------

function p.reproduction( frame )
	ressources["infosPokemon"] = require("Module:Ressources/infosPokemon")

	local infos = frame.args[1]
	if infos == nil then return "Aucune information donnée" else infos = mw.text.trim( infos ) end
	local generation = lire_generation( frame )
	if generation == nil or generation_to_jeux[generation] == nil then
		return generation_invalide( frame.args['génération'] )
	end
	local poke_type = lire_type( frame )
	local jeux = lire_jeux( frame, generation, generation == 8 and "LPA" or nil )
	local notes = lire_notes( frame )
	local colonnes_jeux = colonnes_de_jeux( frame, generation )
	local ancre = "note-reproduction-" .. generation

	local methodes = frame.args['méthodes']
	if methodes == nil then methodes = { "[[niveau]]", "[[reproduction]]" }
	else methodes = mw.text.split( methodes, "/" )
	end

	local communes = nb_communes( generation )
	local colspan = communes + #methodes + ( colonnes_jeux and #colonnes_jeux or 0 )

	local resultat = {}
	table.insert( resultat, ouvrir_table( "usuel", generation, poke_type,
		suffixes_jeux( colonnes_jeux ), frame.args['légende'] ) )
	table.insert( resultat, '<tr>' .. entetes_communes( generation, 1, TITRES.reproduction ) )
	for _, methode in ipairs( methodes ) do
		table.insert( resultat, '<th class="colonne-toutes" scope="col">Parents l\'apprenant par '
			.. methode .. '</th>' )
	end
	table.insert( resultat, entetes_jeux( frame, colonnes_jeux ) )
	table.insert( resultat, '</tr>' )

	if infos == "Aucune" then
		table.insert( resultat, ligne_vide( colspan,
			'Ce Pokémon n\'apprend aucune capacité par reproduction lors de cette génération.' ) )
	else
		local lignes = mw.text.split( infos, "\n" )
		local total = 0
		for _, ligne in ipairs( lignes ) do
			local champs = mw.text.split( ligne, "/" )
			if champs[1] ~= nil and mw.text.trim( champs[1] ) ~= "" then total = total + 1 end
		end
		table.insert( resultat, ligne_bouton( frame, "ListeReproduction", generation,
			colspan, total ) )

		for _, ligne in ipairs( lignes ) do
			local champs = mw.text.split( ligne, "/" )
			if champs[1] ~= nil and mw.text.trim( champs[1] ) ~= "" then
				local brut = champs[1]
				local jeu = ""
				if string.match( brut, "jeu%([^)]*%)" ) then
					jeu = frame:expandTemplate{ title = 'Sup',
						args = { string.sub( string.match( brut, "jeu%([^)]*%)" ), 5, -2 ) } }
					brut = string.gsub( brut, "jeu%([^)]*%)", "" )
				end
				if string.match( brut, "sup%([^)]*%)" ) then
					jeu = frame:expandTemplate{ title = 'Sup',
						args = { string.sub( string.match( brut, "sup%([^)]*%)" ), 5, -2 ) } }
					brut = string.gsub( brut, "sup%([^)]*%)", "" )
				end
				local renvoi = appliquer_notes( string.match( brut, "note%(%d+%)" ) or "", ancre )
				brut = nettoyer_nom( brut )

				local donnees = ressources.donneesCapacite( generation, brut, jeux )
				local brutes = donnees_brutes( generation, brut )
				table.insert( resultat, '<tr>' .. cellules_communes( donnees, brutes,
					generation, 1, { suffixe_nom = jeu .. renvoi,
					                 couleur_type = oui( frame.args['couleur-type'] ) } ) )

				for m = 1, #methodes do
					local parents = champs[m + 1]
					local contenu = "—"
					if parents ~= nil and mw.text.trim( parents ) ~= "" then
						local liste = {}
						for _, parent in ipairs( mw.text.split( parents, "," ) ) do
							table.insert( liste, ressources.infosPokemon( parent, "miniature" ) )
						end
						contenu = table.concat( liste, "" )
					end
					table.insert( resultat, '<td class="colonne-toutes">' .. contenu .. '</td>' )
				end
				table.insert( resultat, cellules_jeux( champs, #methodes + 2, colonnes_jeux, ancre ) )
				table.insert( resultat, '</tr>' )
			end
		end
	end

	if oui( frame.args['queulorior'] ) then
		table.insert( resultat, '<tr><td class="colonne-toutes" colspan="' .. colspan
			.. '">Grâce à sa capacité [[Gribouille]], [[Queulorior]] est capable d\'apprendre '
			.. 'toutes ces capacités, qu\'il peut également transmettre par reproduction.</td></tr>' )
	end

	table.insert( resultat, '</tbody>' .. pied_de_notes( notes, ancre, colspan )
		.. '</table></div>' )
	return table.concat( resultat, "" )
end

--------------------------------------------------------------------------------
-- p.donneur
--------------------------------------------------------------------------------

function p.donneur( frame )
	local infos = frame.args[1]
	if infos == nil then return "Aucune information donnée" else infos = mw.text.trim( infos ) end
	local generation = lire_generation( frame )
	if generation == nil or generation_to_jeux[generation] == nil then
		return generation_invalide( frame.args['génération'] )
	end
	local poke_type = lire_type( frame )
	local jeux = lire_jeux( frame, generation )
	local notes = lire_notes( frame )
	local colonnes_jeux = colonnes_de_jeux( frame, generation )
	local ancre = "note-donneur-" .. generation

	local prix_unite = frame.args['prix-unité']
	if prix_unite == nil then prix_unite = "[[Point de Combat|PCo]]" end

	local communes = nb_communes( generation )
	local colspan = communes + 2 + ( colonnes_jeux and #colonnes_jeux or 0 )

	local suffixes = suffixes_jeux( colonnes_jeux )
	table.insert( suffixes, "apprentissage-donneur" )

	local resultat = {}
	table.insert( resultat, ouvrir_table( "usuel", generation, poke_type, suffixes,
		frame.args['légende'] ) )
	table.insert( resultat, '<tr>' .. entetes_communes( generation, 1, TITRES.usuel ) )
	table.insert( resultat, '<th class="colonne-lieu" scope="col">Lieu</th>'
		.. '<th class="colonne-coût" scope="col">Coût</th>' )
	table.insert( resultat, entetes_jeux( frame, colonnes_jeux ) )
	table.insert( resultat, '</tr>' )

	if infos == "Aucune" then
		table.insert( resultat, ligne_vide( colspan,
			'Ce Pokémon n\'apprend aucune capacité par donneur lors de cette génération.' ) )
	else
		local lignes = mw.text.split( infos, "\n" )
		local total = 0
		for _, ligne in ipairs( lignes ) do
			local champs = mw.text.split( ligne, "/" )
			if non_vide( champs[1] ) then total = total + 1 end
		end
		table.insert( resultat, ligne_bouton( frame, "ListeDonneur", generation, colspan, total ) )

		for _, ligne in ipairs( lignes ) do
			local champs = mw.text.split( ligne, "/" )
			if champs[1] ~= nil and champs[1] ~= "" then
				local renvoi = appliquer_notes( string.match( champs[1], "note%(%d+%)" ) or "", ancre )
				local nom_capacite = nettoyer_nom( champs[1] )
				local donnees = ressources.donneesCapacite( generation, nom_capacite, jeux )
				local brutes = donnees_brutes( generation, nom_capacite )

				local lieu = champs[2] or ""
				local cout, unite = champs[3], champs[4]
				if cout == nil then cout, unite = "—", ""
				elseif unite == nil then unite = prix_unite
				end
				-- Un « / » dans le coût (balise, lien) a découpé la ligne : on recolle.
				local k = 5
				while champs[k] do
					unite = unite .. "/" .. champs[k]
					k = k + 1
				end

				local tri_cout = tonumber( ( string.gsub( cout, "[^%d]", "" ) ) )
				table.insert( resultat, '<tr>' .. cellules_communes( donnees, brutes, generation, 1,
					{ couleur_type = oui( frame.args['couleur-type'] ) } ) )
				table.insert( resultat, '<td class="colonne-lieu">'
					.. appliquer_notes( lieu, ancre ) .. '</td>' )
				table.insert( resultat, '<td class="colonne-coût"'
					.. attribut_tri( tri_cout or -1 ) .. '>'
					.. appliquer_notes( cout .. " " .. unite, ancre ) .. '</td>' )
				table.insert( resultat, cellules_jeux( champs, 5, colonnes_jeux, ancre ) )
				table.insert( resultat, '</tr>' )
			end
		end
	end

	table.insert( resultat, '</tbody>' .. pied_de_notes( notes, ancre, colspan )
		.. '</table></div>' )
	return table.concat( resultat, "" )
end

--------------------------------------------------------------------------------
-- p.preevolution / p.evenement / p.transfert : même forme, une colonne finale
--------------------------------------------------------------------------------

--- Fabrique une fonction « colonnes communes + une colonne libre ».
--- « miniature » rend le champ final comme un Pokémon plutôt que du texte.
local function methode_a_colonne_finale( reglages )
	return function( frame )
		if reglages.miniature then
			ressources["infosPokemon"] = require("Module:Ressources/infosPokemon")
		end
		local infos = frame.args[1]
		if infos == nil then return "Aucune information donnée" else infos = mw.text.trim( infos ) end
		local generation = lire_generation( frame )
		if generation == nil or generation_to_jeux[generation] == nil then
			return generation_invalide( frame.args['génération'] )
		end
		local poke_type = lire_type( frame )
		local jeux = lire_jeux( frame, generation )
		local notes = lire_notes( frame )
		local colonnes_jeux = colonnes_de_jeux( frame, generation )
		local ancre = "note-" .. reglages.ancre .. "-" .. generation

		-- Les colonnes par génération d'origine remplacent la colonne de texte.
		-- Elles demandent de connaître toutes les lignes avant d'écrire l'en-tête :
		-- une génération que personne ne remplit ne mérite pas sa colonne.
		local lignes = {}
		if infos ~= "Aucune" then lignes = mw.text.split( infos, "\n" ) end
		local generations = nil
		if reglages.generations then
			generations = colonnes_de_generations( frame, generation, lignes )
			if generations ~= nil and #generations == 0 then generations = nil end
		end

		local colspan = nb_communes( generation )
			+ ( generations and #generations or 1 )
			+ ( colonnes_jeux and #colonnes_jeux or 0 )

		local suffixes = suffixes_jeux( colonnes_jeux )
		for _, suffixe in ipairs( suffixes_generations( generations ) ) do
			table.insert( suffixes, suffixe )
		end

		local resultat = {}
		table.insert( resultat, ouvrir_table( "usuel", generation, poke_type,
			suffixes, frame.args['légende'] ) )
		table.insert( resultat, '<tr>' .. entetes_communes( generation, 1, TITRES.usuel ) )
		if generations ~= nil then
			table.insert( resultat, entetes_generations( frame, generations ) )
		else
			local titre_final = frame.args['colonne'] or reglages.titre
			table.insert( resultat, '<th class="colonne-toutes" scope="col">'
				.. titre_final .. '</th>' )
		end
		table.insert( resultat, entetes_jeux( frame, colonnes_jeux ) )
		table.insert( resultat, '</tr>' )

		if infos == "Aucune" then
			table.insert( resultat, ligne_vide( colspan, reglages.phrase_vide ) )
		else
			local total = 0
			for _, ligne in ipairs( lignes ) do
				local champs = mw.text.split( ligne, "/" )
				if non_vide( champs[1] ) then total = total + 1 end
			end
			table.insert( resultat, ligne_bouton( frame, reglages.classe, generation,
				colspan, total ) )

			for _, ligne in ipairs( lignes ) do
				local champs = mw.text.split( ligne, "/" )
				if champs[1] ~= nil and champs[1] ~= "" then
					local renvoi = appliquer_notes(
						string.match( champs[1], "note%(%d+%)" ) or "", ancre )
					local nom_capacite = nettoyer_nom( champs[1] )
					local donnees = ressources.donneesCapacite( generation, nom_capacite, jeux )
					local brutes = donnees_brutes( generation, nom_capacite )

					local final = champs[2]
					if final == nil or mw.text.trim( final ) == "" then
						final = reglages.defaut
					end
					local style = ""
					if reglages.miniature then
						final = ressources.infosPokemon( final )
						style = ' style="text-align:left"'
					else
						final = appliquer_notes( final, ancre )
					end

					table.insert( resultat, '<tr>' .. cellules_communes( donnees, brutes,
						generation, 1, { suffixe_nom = renvoi,
						                 couleur_type = oui( frame.args['couleur-type'] ) } ) )
					if generations ~= nil then
						table.insert( resultat, cellules_generations( champs[2], generations ) )
					else
						table.insert( resultat, '<td class="colonne-toutes"' .. style .. '>'
							.. final .. '</td>' )
					end
					table.insert( resultat, cellules_jeux( champs, 3, colonnes_jeux, ancre ) )
					table.insert( resultat, '</tr>' )
				end
			end
		end

		table.insert( resultat, '</tbody>' .. pied_de_notes( notes, ancre, colspan )
			.. '</table></div>' )
		return table.concat( resultat, "" )
	end
end

p.preevolution = methode_a_colonne_finale{
	titre = "Pré-évolution", defaut = "Inconnue", miniature = true,
	classe = "ListePreevolution", ancre = "preevolution",
	phrase_vide = "Ce Pokémon n'apprend aucune capacité par pré-évolution lors de cette génération."
}

p.evenement = methode_a_colonne_finale{
	titre = "Événement", defaut = "Inconnu", miniature = false,
	classe = "ListeEvenement", ancre = "evenement",
	phrase_vide = "Ce Pokémon n'apprend aucune capacité par événement lors de cette génération."
}

--- LOT 2 — la méthode « par transfert », qui n'avait pas de fonction : les pages
--- détournaient p.niveau, dont le tableau vide annonce « par montée en niveau ».
--- « colonnes-générations=oui » remplace la colonne « Provenance » par une
--- colonne par génération d'origine, comme le fait Bulbapedia. Sans le
--- paramètre, le tableau garde sa colonne de texte : rien ne change.
p.transfert = methode_a_colonne_finale{
	titre = "Provenance", defaut = "—", miniature = false, generations = true,
	classe = "ListeTransfert", ancre = "transfert",
	phrase_vide = "Ce Pokémon n'apprend aucune capacité par transfert lors de cette génération."
}

--------------------------------------------------------------------------------
-- p.obscur — LOT 2 : Colosseum et XD
--------------------------------------------------------------------------------

--- Les capacités Ombre n'ont pas de fiche dans Module:Data/Données Capacités :
--- les passer à donneesCapacite verse la page en catégorie de maintenance et
--- affiche « ?? ». Ici, une capacité absente des données est rendue en clair,
--- sans catégoriser — c'est un manque connu, pas une faute de saisie.
function p.obscur( frame )
	local infos = frame.args[1]
	if infos == nil then return "Aucune information donnée" else infos = mw.text.trim( infos ) end
	local generation = lire_generation( frame )
	if generation == nil or generation_to_jeux[generation] == nil then
		return generation_invalide( frame.args['génération'] )
	end
	local poke_type = lire_type( frame )
	local jeux = lire_jeux( frame, generation )
	local notes = lire_notes( frame )
	local ancre = "note-obscur-" .. generation
	local colspan = nb_communes( generation ) + 2

	local resultat = {}
	table.insert( resultat, ouvrir_table( "usuel", generation, poke_type, {},
		frame.args['légende'] ) )
	table.insert( resultat, '<tr>' .. entetes_communes( generation, 1, TITRES.usuel ) )
	table.insert( resultat, '<th class="colonne-toutes" scope="col">Jeu</th>'
		.. '<th class="colonne-toutes" scope="col">Obtention</th></tr>' )

	if infos == "Aucune" then
		table.insert( resultat, ligne_vide( colspan,
			'Ce Pokémon n\'apprend aucune capacité dans Pokémon Colosseum ni Pokémon XD.' ) )
	else
		local lignes = mw.text.split( infos, "\n" )
		local total = 0
		for _, ligne in ipairs( lignes ) do
			local champs = mw.text.split( ligne, "/" )
			if non_vide( champs[1] ) then total = total + 1 end
		end
		table.insert( resultat, ligne_bouton( frame, "ListeObscur", generation, colspan, total ) )

		for _, ligne in ipairs( lignes ) do
			local champs = mw.text.split( ligne, "/" )
			if champs[1] ~= nil and champs[1] ~= "" then
				local renvoi = appliquer_notes(
					string.match( champs[1], "note%(%d+%)" ) or "", ancre )
				local nom_capacite = nettoyer_nom( champs[1] )
				local brutes = donnees_brutes( generation, nom_capacite )

				local donnees
				if brutes == nil then
					-- Hors données : on affiche sans catégoriser la page.
					donnees = { '[[' .. nom_capacite .. ']]',
						"[[Fichier:Miniature Type Inconnu.png|class=type|link=Type inconnu|alt=Type inconnu]]",
						"[[Fichier:Miniature Type Inconnu.png|class=type|link=Catégorie de capacité|alt=Catégorie inconnue]]",
						"??", "??", "??", "??", "??", "??" }
				else
					donnees = ressources.donneesCapacite( generation, nom_capacite, jeux )
				end

				table.insert( resultat, '<tr>' .. cellules_communes( donnees, brutes, generation, 1,
					{ suffixe_nom = renvoi,
					  couleur_type = oui( frame.args['couleur-type'] ) } ) )
				table.insert( resultat, '<td class="colonne-toutes">'
					.. appliquer_notes( champs[2] or "—", ancre ) .. '</td>' )
				table.insert( resultat, '<td class="colonne-toutes">'
					.. appliquer_notes( champs[3] or "—", ancre ) .. '</td></tr>' )
			end
		end
	end

	table.insert( resultat, '</tbody>' .. pied_de_notes( notes, ancre, colspan )
		.. '</table></div>' )
	return table.concat( resultat, "" )
end

return p