« Module:Data/Inventaire » : différence entre les versions
Page créée avec « --[[---------------------------------------------------------------------- Module:Data/Inventaire À copier sur le wiki dans la page « Module:Data/Inventaire ». RÔLE ---- Afficher les listes de marchandises d'UN VENDEUR, quel qu'il soit, depuis une page de données JSON — sans que personne n'ait à recopier ces données ailleurs, et sans qu'aucune mise en forme wiki ait à être analysée. Un vendeur, une page de données : Minerva... » |
Aucun résumé des modifications |
||
| Ligne 303 : | Ligne 303 : | ||
if contenu == '' then | if contenu == '' then | ||
contenu = "''Liste non encore détaillée.''" | contenu = "''Liste non encore détaillée.''" | ||
end | |||
-- PRÉPROCESSER AVANT DE PASSER À expandTemplate, ET NON APRÈS. | |||
-- | |||
-- frame:expandTemplate développe le MODÈLE, mais laisse les valeurs | |||
-- d'arguments telles quelles : elles ne repassent pas par le préprocesseur. | |||
-- Les icônes de devise, écrites {{Lingot76}}, ressortaient donc en toutes | |||
-- lettres dans les boîtes — « (1 500 {{Lingot76}}) » au lieu du lingot. | |||
-- | |||
-- Le défaut ne se voyait que sur ce chemin : |brut=oui préprocessait déjà, | |||
-- et affichait correctement. C'est ce qui le rendait discret — la même | |||
-- liste était juste ou fausse selon un paramètre d'affichage. | |||
-- | |||
-- On préprocesse donc UNE fois, ici, pour les deux chemins. | |||
contenu = frame:preprocess( contenu ) | |||
-- ANCRE STABLE, INDÉPENDANTE DE LA MISE EN FORME. | |||
-- | |||
-- Les boîtes écrites à la main offraient « #Liste_21 », et des liens | |||
-- extérieurs y pointent — la carte joint à chaque passage de Minerva un lien | |||
-- « 📖 Liste N » construit ainsi. Rendre les listes autrement supprimait ces | |||
-- ancres sans prévenir : les liens continuaient d'exister, ils ne menaient | |||
-- simplement plus nulle part, ce qui ne se voit qu'en cliquant. | |||
-- | |||
-- L'ancre est donc posée EXPLICITEMENT, et ne dépend plus de ce que le | |||
-- modèle de boîte fabrique de son titre : refondre l'affichage ne peut plus | |||
-- casser un lien entrant. | |||
-- DEUX ancres : le titre complet, tel qu'une section l'aurait produit, et la | |||
-- forme courte « Liste 21 » — celle que les liens extérieurs utilisent, et | |||
-- qui ne bouge pas quand le nombre de plans change. Un identifiant HTML ne | |||
-- doit pas être présent deux fois : on n'écrit la seconde que si elle diffère. | |||
local titre = titreListe( liste, #puces ) | |||
local court = tonumber( liste.cle ) and ( 'Liste ' .. liste.cle ) or liste.cle | |||
local ancre = '<span id="' .. mw.uri.anchorEncode( titre ) .. '"></span>' | |||
if mw.uri.anchorEncode( court ) ~= mw.uri.anchorEncode( titre ) then | |||
ancre = ancre .. '<span id="' .. mw.uri.anchorEncode( court ) .. '"></span>' | |||
end | end | ||
if brut then | if brut then | ||
return | return ancre .. contenu | ||
end | end | ||
-- On réutilise le modèle de boîte déroulante déjà en place sur le wiki : | -- On réutilise le modèle de boîte déroulante déjà en place sur le wiki : | ||
-- l'habillage reste celui du wiki, seul le CONTENU vient des données. | -- l'habillage reste celui du wiki, seul le CONTENU vient des données. | ||
return frame:expandTemplate{ | -- L'ancre est posée AVANT la boîte, donc atteignable même repliée. | ||
return ancre .. frame:expandTemplate{ | |||
title = 'Boîte déroulante', | title = 'Boîte déroulante', | ||
args = { | args = { | ||
titre = | titre = titre, | ||
contenu = contenu, | contenu = contenu, | ||
}, | }, | ||
Version du 10 août 2026 à 20:42
La documentation pour ce module peut être créée à Module:Data/Inventaire/doc
--[[----------------------------------------------------------------------
Module:Data/Inventaire
À copier sur le wiki dans la page « Module:Data/Inventaire ».
RÔLE
----
Afficher les listes de marchandises d'UN VENDEUR, quel qu'il soit, depuis
une page de données JSON — sans que personne n'ait à recopier ces données
ailleurs, et sans qu'aucune mise en forme wiki ait à être analysée.
Un vendeur, une page de données :
Minerva → Module:Data/Inventaire/Minerva.json
Vendeur Bot Peco → Module:Data/Inventaire/Vendeur Bot Peco.json
Cette page est lue par ce module — pour l'affichage wiki — ET par
l'extension PHP — pour le flux de la carte, l'API et Discord. Une seule
saisie alimente les quatre surfaces.
POURQUOI DES DONNÉES ET NON DU WIKITEXTE ANALYSÉ
-----------------------------------------------
L'inventaire a d'abord vécu en boîtes déroulantes rédigées à la main, que
l'extension relisait à l'expression régulière. Ça marche jusqu'au jour où
quelqu'un réorganise la page : la mise en forme change, l'analyse ne
reconnaît plus rien, et la carte annonce « pas encore publié » devant une
liste complète. Une page de données ne se casse pas en la remettant en
forme, parce qu'elle n'a pas de forme — et MediaWiki REFUSE de
l'enregistrer si le JSON est invalide.
UN NOMBRE QUELCONQUE DE LISTES, NOMMÉES COMME ON VEUT
----------------------------------------------------
Minerva a vingt-quatre listes numérotées qui tournent. Un autre marchand
peut n'en avoir qu'une, ou trois nommées par thème. Les clés de liste sont
donc des CHAÎNES libres (« 21 », « Armes », « Ateliers »), et l'ordre
d'affichage est celui du fichier — un tableau JSON, pas un objet, parce
qu'un objet JSON n'a pas d'ordre défini.
mw.loadJsonData() est délibéré plutôt qu'un require() : il déclare la
dépendance de page auprès du parseur, donc modifier la page de données
invalide automatiquement les pages qui l'affichent. Sans ça, une correction
de prix resterait invisible jusqu'à la purge suivante.
UTILISATION
-----------
{{#invoke:Data/Inventaire|toutes}} toutes les listes du vendeur
{{#invoke:Data/Inventaire|liste|21}} une liste
{{#invoke:Data/Inventaire|liste|Armes}} une liste nommée
{{#invoke:Data/Inventaire|liste|21|brut=oui}} sans la boîte déroulante
{{#invoke:Data/Inventaire|nombre|21}} nombre d'articles
{{#invoke:Data/Inventaire|cles}} clés disponibles, séparées par des virgules
Paramètres communs :
|vendeur= nom du vendeur (défaut : le titre de la page courante)
|donnees= page de données explicite, court-circuite |vendeur=
|brut=oui rend les puces sans boîte déroulante
FORMAT DE LA PAGE DE DONNÉES (schéma « modus.inventaire/2 »)
-----------------------------------------------------------
{
"schema": "modus.inventaire/2",
"vendeur": "Minerva",
"devise_defaut": "lingot",
"listes": [
{
"cle": "21",
"titre": "Liste 21", // optionnel, sinon généré
"mensuelle": false, // optionnel, annoté dans le titre
"articles": [
{ "plan": "Plan : Minigun de Gauss", "cout": 563 },
{ "plan": "Plan : Casseur de têtes" },
"Plan : Poulailler" // forme abrégée
]
}
]
}
Le schéma « modus.inventaire/1 » (« listes » en objet indexé par numéro)
reste lu tel quel : les pages déjà écrites continuent de fonctionner, et
rien n'oblige à les convertir toutes le même jour.
--]]------------------------------------------------------------------------
local p = {}
local SCHEMAS_CONNUS = {
['modus.inventaire/1'] = 1,
['modus.inventaire/2'] = 2,
}
--- Préfixe des pages de données. Un vendeur = une sous-page.
local PREFIXE_DONNEES = 'Module:Data/Inventaire/'
-- Devises connues. Doit rester aligné avec includes/Data/Currency.php ;
-- une clé absente ici affiche le nombre seul plutôt qu'une mauvaise devise.
local DEVISES = {
lingot = { singulier = 'lingot', pluriel = 'lingots', modele = 'Lingot76' },
}
-- ========================================================================
-- Chargement et normalisation
-- ========================================================================
local function erreur( message )
return mw.html.create( 'strong' )
:addClass( 'error' )
:wikitext( 'Module:Data/Inventaire — ' .. message )
:done()
end
--- Normalise une entrée de liste, quelle que soit sa provenance.
local function normaliserListe( cle, brut )
if type( brut ) ~= 'table' then
return nil
end
return {
cle = tostring( cle ),
titre = brut.titre,
mensuelle = brut.mensuelle and true or false,
articles = type( brut.articles ) == 'table' and brut.articles or {},
}
end
--- Charge la page de données et rend TOUJOURS la même structure, quel que
--- soit le schéma d'origine : une séquence ordonnée de listes.
---
--- La normalisation vit ici, en un seul endroit, et non dans chaque point
--- d'entrée : sans ça, chaque fonction devrait connaître les deux schémas et
--- l'une d'elles finirait par en oublier un.
local function charger( pageDonnees )
local ok, donnees = pcall( mw.loadJsonData, pageDonnees )
if not ok or type( donnees ) ~= 'table' then
return nil, 'page de données introuvable ou JSON invalide : ' .. pageDonnees
end
local version = SCHEMAS_CONNUS[donnees.schema or '']
if not version then
return nil, 'schéma inattendu sur ' .. pageDonnees .. ' (« '
.. tostring( donnees.schema ) .. " »)."
end
if type( donnees.listes ) ~= 'table' then
return nil, 'champ « listes » absent de ' .. pageDonnees .. '.'
end
local listes = {}
if version >= 2 then
-- Tableau : l'ordre du fichier EST l'ordre d'affichage. C'est tout
-- l'intérêt du passage au tableau — un vendeur dont les listes
-- s'appellent « Armes », « Armures », « Ateliers » n'a aucun ordre
-- naturel à retrouver, seulement celui que le rédacteur a voulu.
for i, entree in ipairs( donnees.listes ) do
local liste = normaliserListe(
( type( entree ) == 'table' and entree.cle ) or i, entree )
if liste then
table.insert( listes, liste )
end
end
else
-- Objet indexé : les clés d'un objet JSON n'ont pas d'ordre, on trie
-- donc numériquement — ce qui n'a de sens que parce que le schéma 1
-- ne connaissait que des listes numérotées.
local cles = {}
for cle in pairs( donnees.listes ) do
table.insert( cles, tostring( cle ) )
end
table.sort( cles, function ( a, b )
local na, nb = tonumber( a ), tonumber( b )
if na and nb then
return na < nb
end
return a < b
end )
for _, cle in ipairs( cles ) do
local liste = normaliserListe( cle, donnees.listes[cle] )
if liste then
table.insert( listes, liste )
end
end
end
return {
vendeur = donnees.vendeur,
deviseDefaut = donnees.devise_defaut,
listes = listes,
}, nil
end
--- Retrouve une liste par sa clé. La comparaison est faite sur la forme
--- textuelle : « 21 » saisi dans le wikitexte doit trouver la clé 21 écrite
--- en nombre dans le JSON, et inversement.
local function trouver( donnees, cle )
cle = mw.text.trim( tostring( cle ) )
for _, liste in ipairs( donnees.listes ) do
if liste.cle == cle then
return liste
end
end
-- Second passage, insensible à la casse : « armes » doit trouver
-- « Armes ». Un rédacteur ne devrait pas avoir à deviner la casse d'une
-- clé qu'il ne voit pas.
local bas = mw.ustring.lower( cle )
for _, liste in ipairs( donnees.listes ) do
if mw.ustring.lower( liste.cle ) == bas then
return liste
end
end
return nil
end
-- ========================================================================
-- Mise en forme
-- ========================================================================
-- « 1 234 » avec des espaces insécables fines : sur mobile, une espace
-- ordinaire coupe le nombre en fin de ligne.
--
-- ATTENTION AU PIÈGE, il a déjà mordu une fois : string.reverse travaille sur
-- des OCTETS, pas sur des caractères. Insérer l'espace fine insécable (U+202F,
-- soit les trois octets E2 80 AF) AVANT le second reverse ressortait « AF 80
-- E2 » — donc de l'UTF-8 invalide sur tout nombre de trois chiffres ou plus,
-- c'est-à-dire sur pratiquement chaque prix. On groupe donc avec un marqueur
-- ASCII, qui survit à l'inversion, et on ne pose le vrai séparateur qu'à la
-- toute fin, quand plus aucune opération sur octets ne le suivra.
local function formaterNombre( n )
local s = tostring( math.floor( n ) )
local r = s:reverse():gsub( '(%d%d%d)', '%1,' ):reverse()
r = r:gsub( '^,', '' ) -- 1 000 → « ,1 000 » sans ça
return ( r:gsub( ',', '\226\128\175' ) )
end
local function formaterCout( cout, devise )
if cout == nil then
return nil
end
local d = DEVISES[devise or '']
local nombre = formaterNombre( cout )
if not d then
return nombre
end
local libelle = ( math.abs( cout ) == 1 ) and d.singulier or d.pluriel
-- Le modèle d'icône est utilisé s'il existe sur le wiki ; sinon on écrit
-- le mot. On ne teste pas son existence ici — un modèle manquant produit
-- un lien rouge visible, ce qui est le bon signal pour un rédacteur.
if d.modele then
return nombre .. ' {{' .. d.modele .. '}}'
end
return nombre .. ' ' .. libelle
end
local function ligneArticle( article, deviseDefaut )
if type( article ) == 'string' then
article = { plan = article }
end
if type( article ) ~= 'table' or not article.plan then
return nil
end
local nom = article.nom
if not nom or nom == '' then
nom = mw.ustring.gsub( article.plan, '^[Pp]lan%s*:%s*', '' )
end
local ligne = '[[' .. article.plan .. '|' .. nom .. ']]'
local cout = formaterCout( article.cout, article.devise or deviseDefaut )
if cout then
ligne = ligne .. ' (' .. cout .. ')'
end
return ligne
end
--- Titre d'une boîte. Une clé purement numérique donne « Liste 21 » ; une
--- clé nommée est reprise telle quelle, parce qu'inventer « Liste Armes »
--- serait plus laid que ce que le rédacteur a écrit.
local function titreListe( liste, nb )
if liste.titre and liste.titre ~= '' then
return liste.titre
end
local base = tonumber( liste.cle ) and ( 'Liste ' .. liste.cle ) or liste.cle
local details = {}
if liste.mensuelle then
table.insert( details, 'mensuelle' )
end
if nb > 0 then
table.insert( details, nb .. ' plan' .. ( nb > 1 and 's' or '' ) )
end
if #details == 0 then
return base
end
return base .. ' (' .. table.concat( details, ', ' ) .. ')'
end
local function rendreListe( frame, liste, deviseDefaut, brut )
local puces = {}
for _, article in ipairs( liste.articles ) do
local ligne = ligneArticle( article, deviseDefaut )
if ligne then
table.insert( puces, '* ' .. ligne )
end
end
local contenu = table.concat( puces, '\n' )
if contenu == '' then
contenu = "''Liste non encore détaillée.''"
end
-- PRÉPROCESSER AVANT DE PASSER À expandTemplate, ET NON APRÈS.
--
-- frame:expandTemplate développe le MODÈLE, mais laisse les valeurs
-- d'arguments telles quelles : elles ne repassent pas par le préprocesseur.
-- Les icônes de devise, écrites {{Lingot76}}, ressortaient donc en toutes
-- lettres dans les boîtes — « (1 500 {{Lingot76}}) » au lieu du lingot.
--
-- Le défaut ne se voyait que sur ce chemin : |brut=oui préprocessait déjà,
-- et affichait correctement. C'est ce qui le rendait discret — la même
-- liste était juste ou fausse selon un paramètre d'affichage.
--
-- On préprocesse donc UNE fois, ici, pour les deux chemins.
contenu = frame:preprocess( contenu )
-- ANCRE STABLE, INDÉPENDANTE DE LA MISE EN FORME.
--
-- Les boîtes écrites à la main offraient « #Liste_21 », et des liens
-- extérieurs y pointent — la carte joint à chaque passage de Minerva un lien
-- « 📖 Liste N » construit ainsi. Rendre les listes autrement supprimait ces
-- ancres sans prévenir : les liens continuaient d'exister, ils ne menaient
-- simplement plus nulle part, ce qui ne se voit qu'en cliquant.
--
-- L'ancre est donc posée EXPLICITEMENT, et ne dépend plus de ce que le
-- modèle de boîte fabrique de son titre : refondre l'affichage ne peut plus
-- casser un lien entrant.
-- DEUX ancres : le titre complet, tel qu'une section l'aurait produit, et la
-- forme courte « Liste 21 » — celle que les liens extérieurs utilisent, et
-- qui ne bouge pas quand le nombre de plans change. Un identifiant HTML ne
-- doit pas être présent deux fois : on n'écrit la seconde que si elle diffère.
local titre = titreListe( liste, #puces )
local court = tonumber( liste.cle ) and ( 'Liste ' .. liste.cle ) or liste.cle
local ancre = '<span id="' .. mw.uri.anchorEncode( titre ) .. '"></span>'
if mw.uri.anchorEncode( court ) ~= mw.uri.anchorEncode( titre ) then
ancre = ancre .. '<span id="' .. mw.uri.anchorEncode( court ) .. '"></span>'
end
if brut then
return ancre .. contenu
end
-- On réutilise le modèle de boîte déroulante déjà en place sur le wiki :
-- l'habillage reste celui du wiki, seul le CONTENU vient des données.
-- L'ancre est posée AVANT la boîte, donc atteignable même repliée.
return ancre .. frame:expandTemplate{
title = 'Boîte déroulante',
args = {
titre = titre,
contenu = contenu,
},
}
end
-- ========================================================================
-- Points d'entrée
-- ========================================================================
local function options( frame )
local args = frame.args
local parent = frame:getParent()
local function arg( nom )
if args[nom] ~= nil and args[nom] ~= '' then
return args[nom]
end
if parent and parent.args[nom] ~= nil and parent.args[nom] ~= '' then
return parent.args[nom]
end
return nil
end
-- VENDEUR PAR DÉFAUT : le titre de la page courante. Sur l'article
-- « Minerva », {{#invoke:Data/Inventaire|toutes}} suffit donc, sans
-- paramètre — et le jour où un autre marchand a sa page, le même appel y
-- fonctionne sans rien changer. C'est ce qui rend le module réutilisable
-- sans configuration.
local vendeur = arg( 'vendeur' ) or mw.title.getCurrentTitle().baseText
return {
vendeur = vendeur,
pageDonnees = arg( 'donnees' ) or ( PREFIXE_DONNEES .. vendeur .. '.json' ),
brut = ( arg( 'brut' ) or '' ):lower() == 'oui',
cle = arg( 'liste' ) or args[1] or ( parent and parent.args[1] ),
}
end
--- Une liste donnée.
function p.liste( frame )
local o = options( frame )
if not o.cle or mw.text.trim( tostring( o.cle ) ) == '' then
return erreur( 'clé de liste manquante (ex. « |liste|21 »).' )
end
local donnees, err = charger( o.pageDonnees )
if err then
return erreur( err )
end
local liste = trouver( donnees, o.cle )
if not liste then
return "''La liste « " .. mw.text.nowiki( tostring( o.cle ) )
.. " » n'est pas encore documentée pour " .. mw.text.nowiki( o.vendeur ) .. ".''"
end
return rendreListe( frame, liste, donnees.deviseDefaut, o.brut )
end
--- Toutes les listes du vendeur, dans l'ordre de la page de données.
function p.toutes( frame )
local o = options( frame )
local donnees, err = charger( o.pageDonnees )
if err then
return erreur( err )
end
local morceaux = {}
for _, liste in ipairs( donnees.listes ) do
table.insert( morceaux, rendreListe( frame, liste, donnees.deviseDefaut, o.brut ) )
end
if #morceaux == 0 then
return "''Aucune liste documentée pour " .. mw.text.nowiki( o.vendeur ) .. ".''"
end
return table.concat( morceaux, '\n' )
end
--- Nombre d'articles d'une liste — pratique dans un titre.
--
-- Renvoyer « 0 » quand la page de données est introuvable serait un mensonge
-- silencieux : le titre afficherait « Liste 21 (0 plan) » et personne ne
-- saurait que c'est la SOURCE qui manque, pas les plans. p.liste et p.toutes
-- crient dans la même situation ; celle-ci criait dans le vide.
--
-- « 0 » reste la réponse juste pour une liste réellement vide ou non encore
-- documentée : c'est un fait sur les données, pas une panne.
function p.nombre( frame )
local o = options( frame )
if not o.cle or mw.text.trim( tostring( o.cle ) ) == '' then
return erreur( 'clé de liste manquante.' )
end
local donnees, err = charger( o.pageDonnees )
if err then
return erreur( err )
end
local liste = trouver( donnees, o.cle )
if not liste then
return '0'
end
return tostring( #liste.articles )
end
--- Clés disponibles, séparées par des virgules.
---
--- Sert à écrire une page qui ne présume pas du nombre de listes : un modèle
--- peut boucler dessus plutôt que d'appeler vingt-quatre fois « liste » en
--- dur, et un vendeur à liste unique n'oblige alors à rien réécrire.
function p.cles( frame )
local o = options( frame )
local donnees, err = charger( o.pageDonnees )
if err then
return erreur( err )
end
local cles = {}
for _, liste in ipairs( donnees.listes ) do
table.insert( cles, liste.cle )
end
return table.concat( cles, ',' )
end
return p