[ADD] docs: generated regions in README and guides, checked on build
Tables in the documentation must come from the code, not be copied by hand.
scripts/documentation.js fills marked regions of README.md and the guides
from the single sources: settings, shortcuts, CSV columns, constants and
measured contrasts. A check mode reports any region that differs, and a
missing manifest blocks a delivery build. Editing a carrier document reruns
the documentation tests.
Checked: red first; 3454 node, 655 browser and 76 long tests green from the
index alone.
--- FR ---
[ADD] docs : régions engendrées du README et des guides, vérifiées
Les tableaux de la documentation doivent venir du code, non d'une copie à la
main. scripts/documentation.js remplit les régions marquées de README.md et
des guides à partir des sources uniques : réglages, raccourcis, colonnes CSV,
constantes et contrastes relevés. Un mode de vérification signale toute
région qui diffère, et un manifeste absent bloque une construction de
livraison. Modifier un document porteur relance les épreuves de la
documentation.
Vérifié : rouge d'abord ; 3454 node, 655 navigateur et 76 longues vertes
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 13:06:01 -04:00
|
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|
|
|
|
|
|
|
|
|
// Les régions engendrées de la documentation (§ 19.8) et le contrôle de
|
|
|
|
|
// version des captures (§ 19.7). Un document porteur délimite chaque région
|
|
|
|
|
// par deux marqueurs nommés, seuls à se lire comme tels :
|
|
|
|
|
//
|
|
|
|
|
// <!-- engendre:debut <nom> -->
|
|
|
|
|
// <!-- engendre:fin <nom> -->
|
|
|
|
|
//
|
|
|
|
|
// Le nom, en minuscules, chiffres et traits d'union, désigne un producteur
|
|
|
|
|
// de PRODUCTEURS, qui rend le texte de la région depuis sa source unique : la
|
|
|
|
|
// définition des réglages, la table des colonnes du CSV, le registre des
|
|
|
|
|
// commandes et leurs libellés figés, la table des raccourcis, les
|
|
|
|
|
// démonstrations livrées passées au diagnostic, les styles relevés au
|
|
|
|
|
// manifeste des captures, le registre des constantes. Aucune valeur de ces
|
|
|
|
|
// tableaux ne s'écrit ici : elle se lit, ou se compose par la traduction.
|
|
|
|
|
// Seul s'écrit ici ce qui appartient au document : les en-têtes de colonnes,
|
|
|
|
|
// le nom d'un contexte, d'une touche, d'un statut ou de l'état d'une
|
|
|
|
|
// bascule.
|
|
|
|
|
//
|
|
|
|
|
// engendrer remplace le contenu de chaque région — ce qui sépare la fin du
|
|
|
|
|
// marqueur de début du début du marqueur de fin — par deux sauts de ligne,
|
|
|
|
|
// le texte du producteur, deux sauts de ligne ; aucun octet hors de ces
|
|
|
|
|
// bornes ne change. verifier compare chaque région à ce qu'engendrer y
|
|
|
|
|
// écrirait et nomme celle qui diffère : une correction faite à la main dans
|
|
|
|
|
// une région fait échouer la vérification au lieu de se perdre au passage
|
|
|
|
|
// suivant (§ 14.3). Un marqueur illisible, sans fin, sans début ou qui
|
|
|
|
|
// ferme une autre région, une région ouverte dans une autre, un nom en
|
|
|
|
|
// double ou inconnu sont des fautes ; un porteur sans région aussi, et une
|
|
|
|
|
// liste de porteurs vide : un balayage vide ne passe pas (§ 14.2). Seuls les
|
|
|
|
|
// fichiers de PORTEURS se lisent.
|
|
|
|
|
//
|
|
|
|
|
// verifier mesure aussi l'écart entre la version réelle inscrite au
|
|
|
|
|
// manifeste des captures et celle de la construction (ecartDeVersion,
|
|
|
|
|
// documentation/manifeste.js) : affiché et non bloquant au cycle court,
|
|
|
|
|
// bloquant sous --livraison (§ 19.7). Un manifeste absent ne prouve aucune
|
|
|
|
|
// version, et compte pour un écart.
|
|
|
|
|
//
|
|
|
|
|
// Ligne de commande, depuis la racine du projet ou d'ailleurs :
|
|
|
|
|
// node scripts/documentation.js engendrer | verifier [--livraison]
|
|
|
|
|
// [--racine <dossier>] [--porteur <chemin>]…
|
|
|
|
|
// --racine remplace la racine du projet ; chaque --porteur, relatif à la
|
|
|
|
|
// racine, remplace la liste PORTEURS. Code de sortie : 0 sans faute, 1 sur
|
|
|
|
|
// une faute ou, sous --livraison, un écart de version, 2 sur des arguments
|
|
|
|
|
// illisibles.
|
|
|
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
|
|
import { join, resolve } from 'node:path';
|
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
|
import { COMMANDES } from '../src/application/commandes.js';
|
|
|
|
|
import { creerFormat } from '../src/application/format.js';
|
|
|
|
|
import { TABLES_APPLICATION } from '../src/application/libelles.js';
|
|
|
|
|
import { creerTraduction } from '../src/application/traduction.js';
|
|
|
|
|
import { ENTETES_DES_CHAMPS } from '../src/csv/colonnes.js';
|
|
|
|
|
import { CATALOGUE } from '../src/demo/catalogue.js';
|
|
|
|
|
import { LIVREES } from '../src/demo/livrees.js';
|
|
|
|
|
import { CONSTANTES } from '../src/geometrie/constantes.js';
|
|
|
|
|
import { REGLAGES_GENERATION } from '../src/interface/generation/reglages.js';
|
|
|
|
|
import { TABLES_INTERFACE } from '../src/interface/libelles.js';
|
|
|
|
|
import { RACCOURCIS } from '../src/interface/raccourcis.js';
|
|
|
|
|
import { diagnostiquer } from '../src/moteur/diagnostic.js';
|
|
|
|
|
import { analyser, configurationDepuisCharge } from '../src/stockage/document.js';
|
|
|
|
|
import { VERSION } from '../src/version.genere.js';
|
|
|
|
|
import { CHEMIN_MANIFESTE, ecartDeVersion } from './documentation/manifeste.js';
|
|
|
|
|
|
|
|
|
|
/** @typedef {{nom: string, debut: number, fin: number, contenu: string}} Region
|
|
|
|
|
* debut et fin bornent le contenu dans le texte : texte.slice(debut, fin). */
|
|
|
|
|
/** @typedef {{region: string|null, faute: string}} FauteRegion */
|
|
|
|
|
/** @typedef {{chemin: string, faute: string}} Faute chemin relatif à la racine */
|
|
|
|
|
|
|
|
|
|
/** La racine du projet. */
|
|
|
|
|
export const RACINE = fileURLToPath(new URL('..', import.meta.url));
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Les fichiers porteurs de régions, relatifs à la racine : la liste fermée
|
|
|
|
|
* des documents que verifier et engendrer lisent. Chacun porte au moins une
|
2026-10-09 17:14:56 -04:00
|
|
|
* région : README.md les tableaux de mesure (contrastes, constantes), le
|
|
|
|
|
* guide d'usage ses annexes. GUIDE-WINDOWS.md n'y figure pas : il décrit des
|
|
|
|
|
* écrans qu'aucun scénario ne produit, et ne porte aucune région (§ 16.1).
|
[ADD] docs: generated regions in README and guides, checked on build
Tables in the documentation must come from the code, not be copied by hand.
scripts/documentation.js fills marked regions of README.md and the guides
from the single sources: settings, shortcuts, CSV columns, constants and
measured contrasts. A check mode reports any region that differs, and a
missing manifest blocks a delivery build. Editing a carrier document reruns
the documentation tests.
Checked: red first; 3454 node, 655 browser and 76 long tests green from the
index alone.
--- FR ---
[ADD] docs : régions engendrées du README et des guides, vérifiées
Les tableaux de la documentation doivent venir du code, non d'une copie à la
main. scripts/documentation.js remplit les régions marquées de README.md et
des guides à partir des sources uniques : réglages, raccourcis, colonnes CSV,
constantes et contrastes relevés. Un mode de vérification signale toute
région qui diffère, et un manifeste absent bloque une construction de
livraison. Modifier un document porteur relance les épreuves de la
documentation.
Vérifié : rouge d'abord ; 3454 node, 655 navigateur et 76 longues vertes
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 13:06:01 -04:00
|
|
|
* @type {ReadonlyArray<string>}
|
|
|
|
|
*/
|
2026-10-09 17:14:56 -04:00
|
|
|
export const PORTEURS = Object.freeze(['README.md', 'GUIDE-USAGE.md']);
|
[ADD] docs: generated regions in README and guides, checked on build
Tables in the documentation must come from the code, not be copied by hand.
scripts/documentation.js fills marked regions of README.md and the guides
from the single sources: settings, shortcuts, CSV columns, constants and
measured contrasts. A check mode reports any region that differs, and a
missing manifest blocks a delivery build. Editing a carrier document reruns
the documentation tests.
Checked: red first; 3454 node, 655 browser and 76 long tests green from the
index alone.
--- FR ---
[ADD] docs : régions engendrées du README et des guides, vérifiées
Les tableaux de la documentation doivent venir du code, non d'une copie à la
main. scripts/documentation.js remplit les régions marquées de README.md et
des guides à partir des sources uniques : réglages, raccourcis, colonnes CSV,
constantes et contrastes relevés. Un mode de vérification signale toute
région qui diffère, et un manifeste absent bloque une construction de
livraison. Modifier un document porteur relance les épreuves de la
documentation.
Vérifié : rouge d'abord ; 3454 node, 655 navigateur et 76 longues vertes
depuis l'index seul.
Assisted-by: Claude Opus 5.5
2026-10-09 13:06:01 -04:00
|
|
|
|
|
|
|
|
/** Une faute de structure des régions : la liste de ses fautes, par région. */
|
|
|
|
|
export class ErreurRegions extends Error {
|
|
|
|
|
/** @param {FauteRegion[]} fautes */
|
|
|
|
|
constructor(fautes) {
|
|
|
|
|
super(fautes.map(({ region, faute }) => (region === null ? faute : `région « ${region} » : ${faute}`)).join(' ; '));
|
|
|
|
|
this.name = 'ErreurRegions';
|
|
|
|
|
this.fautes = fautes;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Les marqueurs ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// Un commentaire HTML, fermé ou courant jusqu'à la fin du texte.
|
|
|
|
|
const COMMENTAIRE = /<!--([\s\S]*?)(-->|$)/g;
|
|
|
|
|
// Le corps d'un commentaire qui se veut un marqueur.
|
|
|
|
|
const SE_VEUT_MARQUEUR = /^\s*engendre\b/;
|
|
|
|
|
// Le corps d'un marqueur lisible : sa sorte et son nom.
|
|
|
|
|
const MARQUEUR = /^\s*engendre:(debut|fin)\s+([a-z0-9]+(?:-[a-z0-9]+)*)\s*$/;
|
|
|
|
|
|
|
|
|
|
// Un extrait d'un marqueur pour un message, sur une ligne.
|
|
|
|
|
const extrait = (texte) => JSON.stringify(texte.length > 60 ? `${texte.slice(0, 60)}…` : texte);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Les régions d'un texte, dans leur ordre. Un commentaire dont le corps
|
|
|
|
|
* s'ouvre sur « engendre » est un marqueur : lisible, ou une faute ; tout
|
|
|
|
|
* autre commentaire est du texte.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} texte
|
|
|
|
|
* @returns {Region[]}
|
|
|
|
|
* @throws {ErreurRegions} chaque faute de structure : marqueur illisible,
|
|
|
|
|
* début dans une région ouverte, fin sans début, fin qui ferme une autre
|
|
|
|
|
* région, région en double, marqueur sans fin
|
|
|
|
|
*/
|
|
|
|
|
export function lireRegions(texte) {
|
|
|
|
|
if (typeof texte !== 'string') throw new TypeError('les régions se lisent dans une chaîne');
|
|
|
|
|
const regions = [];
|
|
|
|
|
const fautes = [];
|
|
|
|
|
const vues = new Set();
|
|
|
|
|
let ouverte = null;
|
|
|
|
|
for (const trouve of texte.matchAll(COMMENTAIRE)) {
|
|
|
|
|
const [entier, corps, fermeture] = trouve;
|
|
|
|
|
if (!SE_VEUT_MARQUEUR.test(corps)) continue;
|
|
|
|
|
const lu = fermeture === '-->' ? MARQUEUR.exec(corps) : null;
|
|
|
|
|
if (lu === null) {
|
|
|
|
|
fautes.push({ region: null, faute: `marqueur illisible : ${extrait(entier)}` });
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const [, sorte, nom] = lu;
|
|
|
|
|
if (sorte === 'debut') {
|
|
|
|
|
if (ouverte !== null) fautes.push({ region: nom, faute: `ouverte dans la région « ${ouverte.nom} »` });
|
|
|
|
|
else ouverte = { nom, debut: trouve.index + entier.length };
|
|
|
|
|
} else if (ouverte === null) {
|
|
|
|
|
fautes.push({ region: nom, faute: 'fin sans début' });
|
|
|
|
|
} else if (ouverte.nom !== nom) {
|
|
|
|
|
fautes.push({ region: nom, faute: `sa fin ferme la région « ${ouverte.nom} »` });
|
|
|
|
|
} else {
|
|
|
|
|
if (vues.has(nom)) fautes.push({ region: nom, faute: 'région en double' });
|
|
|
|
|
vues.add(nom);
|
|
|
|
|
regions.push({ nom, debut: ouverte.debut, fin: trouve.index, contenu: texte.slice(ouverte.debut, trouve.index) });
|
|
|
|
|
ouverte = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (ouverte !== null) fautes.push({ region: ouverte.nom, faute: 'marqueur sans fin' });
|
|
|
|
|
if (fautes.length > 0) throw new ErreurRegions(fautes);
|
|
|
|
|
return regions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Le contenu d'une région, tel qu'engendrer l'écrit : deux sauts de ligne,
|
|
|
|
|
* le texte produit sans ses sauts de ligne finaux, deux sauts de ligne.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} produit
|
|
|
|
|
* @returns {string}
|
|
|
|
|
*/
|
|
|
|
|
export function contenuDeRegion(produit) {
|
|
|
|
|
if (typeof produit !== 'string') throw new TypeError('un producteur rend une chaîne');
|
|
|
|
|
return `\n\n${produit.replace(/\n+$/, '')}\n\n`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Les noms des régions qu'aucun producteur ne porte, en fautes.
|
|
|
|
|
function nomsInconnus(regions, producteurs) {
|
|
|
|
|
return regions
|
|
|
|
|
.filter(({ nom }) => !producteurs.has(nom))
|
|
|
|
|
.map(({ nom }) => ({ region: nom, faute: 'nom inconnu : aucun producteur ne le porte' }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Le texte dont chaque région porte ce que son producteur rend ; le reste,
|
|
|
|
|
* marqueurs compris, inchangé à l'unité près. Un texte sans région se rend
|
|
|
|
|
* tel quel.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} texte
|
|
|
|
|
* @param {Map<string, () => string>} producteurs par nom de région
|
|
|
|
|
* @returns {string}
|
|
|
|
|
* @throws {ErreurRegions} une faute de structure, ou un nom inconnu, avant
|
|
|
|
|
* tout appel d'un producteur
|
|
|
|
|
*/
|
|
|
|
|
export function remplacerRegions(texte, producteurs) {
|
|
|
|
|
const regions = lireRegions(texte);
|
|
|
|
|
const inconnus = nomsInconnus(regions, producteurs);
|
|
|
|
|
if (inconnus.length > 0) throw new ErreurRegions(inconnus);
|
|
|
|
|
let rendu = '';
|
|
|
|
|
let curseur = 0;
|
|
|
|
|
for (const { nom, debut, fin } of regions) {
|
|
|
|
|
rendu += texte.slice(curseur, debut) + contenuDeRegion(producteurs.get(nom)());
|
|
|
|
|
curseur = fin;
|
|
|
|
|
}
|
|
|
|
|
return rendu + texte.slice(curseur);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// La première ligne où deux contenus diffèrent : son rang dans le contenu,
|
|
|
|
|
// et la ligne de chacun, null au-delà de sa fin.
|
|
|
|
|
function premiereDifference(lu, attendu) {
|
|
|
|
|
const a = lu.split('\n');
|
|
|
|
|
const b = attendu.split('\n');
|
|
|
|
|
let rang = 0;
|
|
|
|
|
while (rang < a.length && rang < b.length && a[rang] === b[rang]) rang += 1;
|
|
|
|
|
return { rang, lue: a[rang] ?? null, attendue: b[rang] ?? null };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Le numéro, à partir de 1, de la ligne du texte où tombe l'indice.
|
|
|
|
|
const ligneDe = (texte, indice) => texte.slice(0, indice).split('\n').length;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Les fautes des régions d'un texte : de structure, de nom, de producteur,
|
|
|
|
|
* ou de contenu — une région qui diffère de ce qu'engendrer y écrirait,
|
|
|
|
|
* nommée avec la première ligne qui s'écarte. Un texte sans région est une
|
|
|
|
|
* faute.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} texte
|
|
|
|
|
* @param {Map<string, () => string>} producteurs
|
|
|
|
|
* @returns {FauteRegion[]} vide quand toutes les régions sont conformes
|
|
|
|
|
*/
|
|
|
|
|
export function verifierRegions(texte, producteurs) {
|
|
|
|
|
let regions;
|
|
|
|
|
try {
|
|
|
|
|
regions = lireRegions(texte);
|
|
|
|
|
} catch (erreur) {
|
|
|
|
|
if (erreur instanceof ErreurRegions) return erreur.fautes;
|
|
|
|
|
throw erreur;
|
|
|
|
|
}
|
|
|
|
|
if (regions.length === 0) return [{ region: null, faute: 'aucune région engendrée : un porteur en a au moins une' }];
|
|
|
|
|
const fautes = nomsInconnus(regions, producteurs);
|
|
|
|
|
for (const { nom, debut, contenu } of regions) {
|
|
|
|
|
if (!producteurs.has(nom)) continue;
|
|
|
|
|
let attendu;
|
|
|
|
|
try {
|
|
|
|
|
attendu = contenuDeRegion(producteurs.get(nom)());
|
|
|
|
|
} catch (erreur) {
|
|
|
|
|
fautes.push({ region: nom, faute: `le producteur échoue : ${erreur.message}` });
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (contenu === attendu) continue;
|
|
|
|
|
const { rang, lue, attendue } = premiereDifference(contenu, attendu);
|
|
|
|
|
const ligne = ligneDe(texte, debut) + rang;
|
|
|
|
|
const cite = (texteLigne) => (texteLigne === null ? 'rien' : `« ${texteLigne} »`);
|
|
|
|
|
fautes.push({
|
|
|
|
|
region: nom,
|
|
|
|
|
faute:
|
|
|
|
|
`diffère de ce que le producteur engendre, ligne ${ligne} : ${cite(lue)} au lieu de ${cite(attendue)} ; ` +
|
|
|
|
|
'une correction à la main se perd au prochain engendrer',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return fautes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Les tableaux ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]);
|
|
|
|
|
const { nombre } = creerFormat(t);
|
|
|
|
|
const VIDE = t('format.vide');
|
|
|
|
|
// Les clés de l'application : un libellé d'historique y vit.
|
|
|
|
|
const CLES_APPLICATION = new Set(creerTraduction(TABLES_APPLICATION).cles());
|
|
|
|
|
|
|
|
|
|
// Deux chaînes, comparées unité UTF-16 par unité.
|
|
|
|
|
const comparerTextes = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
|
|
|
|
|
|
|
|
// Un identifiant, en code.
|
|
|
|
|
const code = (texte) => `\`${texte}\``;
|
|
|
|
|
|
|
|
|
|
// Le nombre de décimales qu'un nombre porte dans son écriture la plus
|
|
|
|
|
// courte, celle de String : 0,3 en porte une, 200 000 aucune.
|
|
|
|
|
function decimalesDe(valeur) {
|
|
|
|
|
const ecrit = String(valeur);
|
|
|
|
|
if (/e/i.test(ecrit)) throw new RangeError(`documentation : ${ecrit} ne s'écrit pas en chiffres`);
|
|
|
|
|
return (ecrit.split('.')[1] ?? '').length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Une cellule : un saut de ligne, et les blancs qui l'entourent, deviennent
|
|
|
|
|
// une espace ; une barre s'échappe, qui fermerait la cellule.
|
|
|
|
|
const cellule = (texte) => String(texte).replace(/\s*(?:\r\n|\r|\n)\s*/g, ' ').replace(/\|/g, '\\|');
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Un tableau Markdown : la ligne d'en-tête, le séparateur, une ligne par
|
|
|
|
|
* entrée, sans saut de ligne final.
|
|
|
|
|
*
|
|
|
|
|
* @param {string[]} entetes
|
|
|
|
|
* @param {Array<Array<string>>} lignes
|
|
|
|
|
* @returns {string}
|
|
|
|
|
*/
|
|
|
|
|
export function tableau(entetes, lignes) {
|
|
|
|
|
const ligne = (cellules) => `| ${cellules.map(cellule).join(' | ')} |`;
|
|
|
|
|
return [ligne(entetes), `|${entetes.map(() => '---').join('|')}|`, ...lignes.map(ligne)].join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Le tableau des réglages (§ 19.8) : chaque entrée de la définition, dans
|
|
|
|
|
* son ordre — son libellé, qui porte l'unité d'un champ, l'écran qui la
|
|
|
|
|
* montre, sa valeur par défaut.
|
|
|
|
|
*
|
|
|
|
|
* @param {ReadonlyArray<import('../src/interface/generation/reglages.js').EntreeReglage>} [definition]
|
|
|
|
|
* @returns {string}
|
|
|
|
|
*/
|
|
|
|
|
export function tableauReglages(definition = REGLAGES_GENERATION) {
|
|
|
|
|
const defaut = ({ genre, defaut: valeur, cle }) => {
|
|
|
|
|
if (genre === 'champ') return nombre(valeur);
|
|
|
|
|
if (genre === 'bascule') return valeur ? 'cochée' : 'décochée';
|
|
|
|
|
throw new RangeError(`reglages : genre inconnu ${JSON.stringify(genre)} pour ${cle}`);
|
|
|
|
|
};
|
|
|
|
|
return tableau(
|
|
|
|
|
['Réglage', 'Écran', 'Valeur par défaut'],
|
|
|
|
|
definition.map((entree) => [t(entree.etiquette), t(`coquille.section.${entree.section}`), defaut(entree)]),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Le tableau des colonnes du CSV (§ 10.1, § 19.8) : chaque champ, dans
|
|
|
|
|
* l'ordre de l'export — son en-tête, puis ses synonymes.
|
|
|
|
|
*
|
|
|
|
|
* @param {ReadonlyArray<readonly [string, ReadonlyArray<string>]>} [entetes]
|
|
|
|
|
* @returns {string}
|
|
|
|
|
*/
|
|
|
|
|
export function tableauColonnesCsv(entetes = ENTETES_DES_CHAMPS) {
|
|
|
|
|
return tableau(
|
|
|
|
|
['Colonne', 'Synonymes reconnus'],
|
|
|
|
|
entetes.map(([, [entete, ...synonymes]]) => [code(entete), synonymes.length === 0 ? VIDE : synonymes.map(code).join(', ')]),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Les détails d'exemple du libellé d'historique de chaque commande du
|
|
|
|
|
* registre : les noms sont inventés. Le libellé lui-même se compose par la
|
|
|
|
|
* traduction, de la clé ENTREE_<NOM> de la commande.
|
|
|
|
|
*/
|
|
|
|
|
export const EXEMPLES_COMMANDES = Object.freeze({
|
|
|
|
|
ajouterParticipant: { personne: 'Iris Ombrelle' },
|
|
|
|
|
modifierParticipant: { personne: 'Ondine Lacasse', champs: ['courriel'], devenu: null },
|
|
|
|
|
exclureParticipant: { personne: 'Théo Pervenche' },
|
|
|
|
|
reintegrerParticipant: { personne: 'Théo Pervenche' },
|
|
|
|
|
supprimerParticipant: { personne: 'Ondine Lacasse' },
|
|
|
|
|
importerParticipants: { mode: 'ajouter', ajoutes: 12, misAJour: 0, retires: 0 },
|
|
|
|
|
changerEtat: { etat: 'bloque' },
|
|
|
|
|
debloquer: { etat: 'retenu' },
|
|
|
|
|
enregistrerGeneration: { n: 5, premier: 3, dernier: 7 },
|
|
|
|
|
retenirProposition: { id: 3 },
|
|
|
|
|
effacerPropositions: { n: 4, gardee: 2 },
|
|
|
|
|
creerTable: { numero: 3 },
|
|
|
|
|
supprimerTable: { numero: 2, reservations: 2, titres: 1 },
|
|
|
|
|
deplacerTable: { numero: 3 },
|
|
|
|
|
changerPlacesTable: { numero: 5, sieges: 10 },
|
|
|
|
|
realignerTable: { numero: 5, sieges: 8 },
|
|
|
|
|
changerDefaut: { sieges: 10, realignees: 4 },
|
|
|
|
|
changerTours: { tours: 3, retirees: 2 },
|
|
|
|
|
modifierReglage: { cle: 'separerAppartenances', valeur: false },
|
|
|
|
|
changerCibleRedondance: { cible: 1200 },
|
|
|
|
|
reserver: { personne: 'Théo Pervenche', numero: 4, siege: 2, portee: 'tour', tour: 2 },
|
|
|
|
|
retirerReservation: { personne: 'Théo Pervenche', numero: 4, portee: 'tous', tour: null },
|
|
|
|
|
deplacerReservation: { personne: 'Théo Pervenche', de: 4, vers: 7, siege: null, portee: 'tour', tour: 2 },
|
|
|
|
|
poserTitre: { libelle: 'accueil', numero: 3, siege: 1 },
|
|
|
|
|
retirerTitre: { libelle: 'accueil', numero: 3, siege: 1 },
|
|
|
|
|
designer: { personne: 'Iris Ombrelle', numero: 3, siege: 1, libelle: 'accueil' },
|
|
|
|
|
convertirEnTous: { personne: 'Iris Ombrelle', numero: 3 },
|
|
|
|
|
deplacerDansRetenu: { personne: 'Iris Ombrelle', de: 4, vers: 7, siege: null, tour: 2 },
|
|
|
|
|
echangerDansRetenu: { a: 'Iris Ombrelle', deA: 4, b: 'Théo Pervenche', deB: null, tour: 2 },
|
|
|
|
|
retirerDuTour: { personne: 'Iris Ombrelle', numero: 4, tour: 2 },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// La clé du libellé d'historique d'une commande : ENTREE_ puis son nom, un
|
|
|
|
|
// souligné devant chaque capitale, en capitales.
|
|
|
|
|
const cleDHistorique = (nom) => `ENTREE_${nom.replace(/[A-Z]/g, (lettre) => `_${lettre}`).toUpperCase()}`;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Le tableau des commandes (§ 8.2, § 19.8) : chaque commande du registre,
|
|
|
|
|
* dans son ordre, et un exemple de son libellé d'historique.
|
|
|
|
|
*
|
|
|
|
|
* @param {Map<string, Function>} [commandes]
|
|
|
|
|
* @param {Record<string, Object>} [exemples]
|
|
|
|
|
* @returns {string}
|
|
|
|
|
* @throws {RangeError} une commande sans exemple ou sans libellé
|
|
|
|
|
* d'historique, un exemple sans commande, chacun nommé
|
|
|
|
|
*/
|
|
|
|
|
export function tableauCommandes(commandes = COMMANDES, exemples = EXEMPLES_COMMANDES) {
|
|
|
|
|
const noms = [...commandes.keys()];
|
|
|
|
|
const sansCommande = Object.keys(exemples).filter((nom) => !commandes.has(nom)).sort(comparerTextes);
|
|
|
|
|
if (sansCommande.length > 0) throw new RangeError(`commandes : exemple sans commande au registre : ${sansCommande.join(', ')}`);
|
|
|
|
|
return tableau(
|
|
|
|
|
['Commande', "Libellé d'historique (exemple)"],
|
|
|
|
|
noms.map((nom) => {
|
|
|
|
|
if (!Object.hasOwn(exemples, nom)) throw new RangeError(`commandes : « ${nom} » n'a pas d'exemple (EXEMPLES_COMMANDES)`);
|
|
|
|
|
const cle = cleDHistorique(nom);
|
|
|
|
|
if (!CLES_APPLICATION.has(cle)) throw new RangeError(`commandes : « ${nom} » n'a pas de libellé d'historique ${cle}`);
|
|
|
|
|
return [code(nom), t(cle, exemples[nom])];
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Le nom de chaque contexte de la table des raccourcis, tel que le
|
|
|
|
|
// tableau le montre.
|
|
|
|
|
const NOMS_CONTEXTES = new Map([
|
|
|
|
|
['plan', 'Plan'],
|
|
|
|
|
['grille', 'Grille des participants'],
|
|
|
|
|
['champ', 'Champ de saisie'],
|
|
|
|
|
['commande', 'Bouton de commande'],
|
|
|
|
|
['geste', 'Geste en cours'],
|
|
|
|
|
['fenetre', 'Panneau ou infobulle'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Le nom de chaque touche, par sa valeur de KeyboardEvent.key.
|
|
|
|
|
const NOMS_TOUCHES = new Map([
|
|
|
|
|
['ArrowRight', '\u{2192}'],
|
|
|
|
|
['ArrowDown', '\u{2193}'],
|
|
|
|
|
['ArrowLeft', '\u{2190}'],
|
|
|
|
|
['ArrowUp', '\u{2191}'],
|
|
|
|
|
['Enter', 'Entrée'],
|
|
|
|
|
[' ', 'Espace'],
|
|
|
|
|
['Escape', 'Échap'],
|
|
|
|
|
['Tab', 'Tab'],
|
|
|
|
|
['F2', 'F2'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Le nom d'une touche, « Maj+ » gardé devant celui de sa touche.
|
|
|
|
|
function nomDeTouche(touche) {
|
|
|
|
|
if (touche.startsWith('Maj+')) return `Maj+${nomDeTouche(touche.slice('Maj+'.length))}`;
|
|
|
|
|
const nom = NOMS_TOUCHES.get(touche);
|
|
|
|
|
if (nom === undefined) throw new RangeError(`raccourcis : touche sans nom ${JSON.stringify(touche)}`);
|
|
|
|
|
return nom;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Le tableau des raccourcis (§ 7.3, § 19.8) : une ligne par contexte et par
|
|
|
|
|
* effet, dans l'ordre de la table — le contexte, ses touches, le libellé de
|
|
|
|
|
* l'effet.
|
|
|
|
|
*
|
|
|
|
|
* @param {ReadonlyArray<{touche: string, contexte: string, effet: string, cle: string}>} [raccourcis]
|
|
|
|
|
* @returns {string}
|
|
|
|
|
* @throws {RangeError} une touche ou un contexte sans nom
|
|
|
|
|
*/
|
|
|
|
|
export function tableauRaccourcis(raccourcis = RACCOURCIS) {
|
|
|
|
|
const effets = new Map();
|
|
|
|
|
for (const { touche, contexte, effet, cle } of raccourcis) {
|
|
|
|
|
const groupe = `${contexte}.${effet}`;
|
|
|
|
|
if (!effets.has(groupe)) effets.set(groupe, { contexte, cle, touches: [] });
|
|
|
|
|
effets.get(groupe).touches.push(touche);
|
|
|
|
|
}
|
|
|
|
|
return tableau(
|
|
|
|
|
['Où', 'Touches', 'Effet'],
|
|
|
|
|
[...effets.values()].map(({ contexte, cle, touches }) => {
|
|
|
|
|
const nom = NOMS_CONTEXTES.get(contexte);
|
|
|
|
|
if (nom === undefined) throw new RangeError(`raccourcis : contexte sans nom ${JSON.stringify(contexte)}`);
|
|
|
|
|
return [nom, touches.map(nomDeTouche).join(' ou '), t(cle)];
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Le tableau des démonstrations (§ 15, § 19.8) : chaque fichier livré, dans
|
|
|
|
|
* l'ordre de LIVREES, lu puis passé au diagnostic — son nom au catalogue,
|
|
|
|
|
* son effectif, ses tables, ses tours, l'étendue de son plafond a priori.
|
|
|
|
|
*
|
|
|
|
|
* @param {ReadonlyArray<{cle: string, texte: string}>} [livrees]
|
|
|
|
|
* @returns {string}
|
|
|
|
|
*/
|
|
|
|
|
export function tableauDemonstrations(livrees = LIVREES) {
|
|
|
|
|
return tableau(
|
|
|
|
|
['Démonstration', 'Effectif', 'Tables', 'Tours', 'Plafond a priori, minimum', 'Plafond a priori, maximum'],
|
|
|
|
|
livrees.map(({ cle, texte }) => {
|
|
|
|
|
const entree = CATALOGUE.find((candidate) => candidate.cle === cle);
|
|
|
|
|
if (entree === undefined) throw new RangeError(`demonstrations : « ${cle} » n'est pas au catalogue`);
|
|
|
|
|
const configuration = configurationDepuisCharge(analyser(texte).charge);
|
|
|
|
|
const { plafondAPriori } = diagnostiquer(configuration);
|
|
|
|
|
return [
|
|
|
|
|
entree.nom,
|
|
|
|
|
nombre(configuration.participants.length),
|
|
|
|
|
nombre(configuration.tables.length),
|
|
|
|
|
nombre(configuration.tours),
|
|
|
|
|
nombre(plafondAPriori.min),
|
|
|
|
|
nombre(plafondAPriori.max),
|
|
|
|
|
];
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const THEMES = Object.freeze(['clair', 'sombre']);
|
|
|
|
|
|
|
|
|
|
// Vrai pour un style relevé lisible : jeton et rôle nommés, ratio et seuil
|
|
|
|
|
// finis.
|
|
|
|
|
const styleLisible = (style) =>
|
|
|
|
|
style !== null &&
|
|
|
|
|
typeof style === 'object' &&
|
|
|
|
|
typeof style.jeton === 'string' &&
|
|
|
|
|
style.jeton !== '' &&
|
|
|
|
|
typeof style.role === 'string' &&
|
|
|
|
|
style.role !== '' &&
|
|
|
|
|
Number.isFinite(style.ratio) &&
|
|
|
|
|
Number.isFinite(style.seuil);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Le tableau des ratios de contraste (§ 14.5, § 19.8), engendré des styles
|
|
|
|
|
* relevés au pilotage : une ligne par jeton et par rôle, rangée par jeton
|
|
|
|
|
* puis par rôle, unité UTF-16 par unité ; pour chaque thème, le plus petit
|
|
|
|
|
* ratio relevé et la première capture, dans l'ordre du manifeste, où il
|
|
|
|
|
* l'est — rien quand le thème ne relève pas ce jeton dans ce rôle.
|
|
|
|
|
*
|
|
|
|
|
* Un style relevé est { jeton, role, ratio, seuil, … } sous
|
|
|
|
|
* captures[].styles.clair et .sombre ; ses autres champs ne se lisent pas.
|
|
|
|
|
*
|
|
|
|
|
* @param {object|null} manifeste
|
|
|
|
|
* @returns {string}
|
|
|
|
|
* @throws {Error} un manifeste absent, un style illisible, deux seuils pour
|
|
|
|
|
* un même jeton dans un même rôle, aucun style relevé
|
|
|
|
|
*/
|
|
|
|
|
export function tableauContrastes(manifeste) {
|
|
|
|
|
if (manifeste === null || typeof manifeste !== 'object' || !Array.isArray(manifeste.captures)) {
|
|
|
|
|
throw new Error(`contrastes : manifeste absent ou sans liste de captures (${CHEMIN_MANIFESTE})`);
|
|
|
|
|
}
|
|
|
|
|
const groupes = new Map();
|
|
|
|
|
for (const capture of manifeste.captures) {
|
|
|
|
|
for (const theme of THEMES) {
|
|
|
|
|
const styles = capture?.styles?.[theme];
|
|
|
|
|
if (!Array.isArray(styles)) throw new Error(`contrastes : ${capture?.fichier} : styles.${theme} n'est pas une liste`);
|
|
|
|
|
for (const style of styles) {
|
|
|
|
|
if (!styleLisible(style)) {
|
|
|
|
|
throw new Error(`contrastes : ${capture.fichier}, thème ${theme} : un style sans jeton, rôle, ratio ou seuil`);
|
|
|
|
|
}
|
|
|
|
|
const { jeton, role, ratio, seuil } = style;
|
|
|
|
|
const cle = JSON.stringify([jeton, role]);
|
|
|
|
|
if (!groupes.has(cle)) groupes.set(cle, { jeton, role, seuil, clair: null, sombre: null });
|
|
|
|
|
const groupe = groupes.get(cle);
|
|
|
|
|
if (groupe.seuil !== seuil) {
|
|
|
|
|
throw new Error(`contrastes : « ${jeton} », rôle ${role} : deux seuils, ${groupe.seuil} et ${seuil} (${capture.fichier})`);
|
|
|
|
|
}
|
|
|
|
|
if (groupe[theme] === null || ratio < groupe[theme].ratio) groupe[theme] = { ratio, capture: capture.fichier };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (groupes.size === 0) throw new Error('contrastes : aucun style relevé dans le manifeste');
|
|
|
|
|
const ranges = [...groupes.values()].sort((a, b) => comparerTextes(a.jeton, b.jeton) || comparerTextes(a.role, b.role));
|
|
|
|
|
const releve = (mesure) => (mesure === null ? [VIDE, VIDE] : [nombre(mesure.ratio, { decimales: 2 }), mesure.capture]);
|
|
|
|
|
return tableau(
|
|
|
|
|
['Jeton', 'Rôle', 'Seuil', 'Thème clair', 'Capture', 'Thème sombre', 'Capture'],
|
|
|
|
|
ranges.map(({ jeton, role, seuil, clair, sombre }) => [
|
|
|
|
|
code(jeton),
|
|
|
|
|
role,
|
|
|
|
|
nombre(seuil, { decimales: decimalesDe(seuil) }),
|
|
|
|
|
...releve(clair),
|
|
|
|
|
...releve(sombre),
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const STATUTS = new Map([
|
|
|
|
|
['mesuree', 'mesurée'],
|
|
|
|
|
['defautDeTravail', 'défaut de travail'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Le tableau du registre des constantes (§ 14.1, § 19.8), lu seul : chaque
|
|
|
|
|
* constante, dans l'ordre du registre — son nom, sa valeur, son unité, son
|
|
|
|
|
* propriétaire, son statut en toutes lettres, son protocole. Une valeur
|
|
|
|
|
* relevée à l'exécution (null) s'écrit vide.
|
|
|
|
|
*
|
|
|
|
|
* @param {ReadonlyArray<import('../src/geometrie/constantes.js').Constante>} [constantes]
|
|
|
|
|
* @returns {string}
|
|
|
|
|
* @throws {RangeError} un statut inconnu
|
|
|
|
|
*/
|
|
|
|
|
export function tableauRegistre(constantes = CONSTANTES) {
|
|
|
|
|
return tableau(
|
|
|
|
|
['Constante', 'Valeur', 'Unité', 'Propriétaire', 'Statut', 'Protocole'],
|
|
|
|
|
constantes.map(({ nom, valeur, unite, proprietaire, statut, protocole }) => {
|
|
|
|
|
const enLettres = STATUTS.get(statut);
|
|
|
|
|
if (enLettres === undefined) throw new RangeError(`constantes : statut inconnu ${JSON.stringify(statut)} pour ${nom}`);
|
|
|
|
|
const ecrite = valeur === null ? VIDE : nombre(valeur, { decimales: decimalesDe(valeur) });
|
|
|
|
|
return [code(nom), ecrite, unite, proprietaire, enLettres, protocole];
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Les producteurs --------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @typedef {{manifeste: () => object|null}} Contexte
|
|
|
|
|
* manifeste : le manifeste des captures lu, null quand il manque
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const producteur = (nom, produire) => Object.freeze({ nom, produire });
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Les producteurs, par nom de région, dans l'ordre du § 19.8 : chacun
|
|
|
|
|
* (contexte) → le texte de sa région.
|
|
|
|
|
* @type {ReadonlyArray<Readonly<{nom: string, produire: (contexte: Contexte) => string}>>}
|
|
|
|
|
*/
|
|
|
|
|
export const PRODUCTEURS = Object.freeze([
|
|
|
|
|
producteur('reglages', () => tableauReglages()),
|
|
|
|
|
producteur('colonnes-csv', () => tableauColonnesCsv()),
|
|
|
|
|
producteur('commandes', () => tableauCommandes()),
|
|
|
|
|
producteur('raccourcis', () => tableauRaccourcis()),
|
|
|
|
|
producteur('demonstrations', () => tableauDemonstrations()),
|
|
|
|
|
producteur('contrastes', (contexte) => tableauContrastes(contexte.manifeste())),
|
|
|
|
|
producteur('constantes', () => tableauRegistre()),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Les producteurs liés à un contexte, par nom, dans l'ordre de PRODUCTEURS :
|
|
|
|
|
* chacun produit une fois, puis rend le même texte.
|
|
|
|
|
*
|
|
|
|
|
* @param {Contexte} contexte
|
|
|
|
|
* @returns {Map<string, () => string>}
|
|
|
|
|
*/
|
|
|
|
|
export function producteursDe(contexte) {
|
|
|
|
|
return new Map(
|
|
|
|
|
PRODUCTEURS.map(({ nom, produire }) => {
|
|
|
|
|
let texte = null;
|
|
|
|
|
return [nom, () => (texte ??= produire(contexte))];
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- L'arbre ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// Vrai pour l'erreur d'un fichier absent.
|
|
|
|
|
const estAbsent = (erreur) => erreur?.code === 'ENOENT';
|
|
|
|
|
|
|
|
|
|
// Le lecteur par défaut d'une racine : le texte UTF-8 d'un chemin relatif.
|
|
|
|
|
const lecteurDe = (racine) => (chemin) => readFileSync(join(racine, chemin), 'utf8');
|
|
|
|
|
|
|
|
|
|
// Le contexte d'un arbre : le manifeste, lu au premier besoin, une fois ;
|
|
|
|
|
// null quand il manque. Un manifeste qui n'est pas du JSON lève.
|
|
|
|
|
function contexteDe(lire) {
|
|
|
|
|
let lu;
|
|
|
|
|
return {
|
|
|
|
|
manifeste() {
|
|
|
|
|
if (lu !== undefined) return lu;
|
|
|
|
|
let texte;
|
|
|
|
|
try {
|
|
|
|
|
texte = lire(CHEMIN_MANIFESTE);
|
|
|
|
|
} catch (erreur) {
|
|
|
|
|
if (!estAbsent(erreur)) throw erreur;
|
|
|
|
|
lu = null;
|
|
|
|
|
return lu;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
lu = JSON.parse(texte);
|
|
|
|
|
} catch {
|
|
|
|
|
throw new Error(`${CHEMIN_MANIFESTE} : JSON illisible`);
|
|
|
|
|
}
|
|
|
|
|
return lu;
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Une faute de région, au chemin de son porteur.
|
|
|
|
|
const auChemin = (chemin) => ({ region, faute }) => ({
|
|
|
|
|
chemin,
|
|
|
|
|
faute: region === null ? faute : `région « ${region} » : ${faute}`,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// La faute d'une liste de porteurs vide.
|
|
|
|
|
const SANS_PORTEUR = Object.freeze({ chemin: '.', faute: 'aucun fichier porteur : un balayage vide ne passe pas' });
|
|
|
|
|
|
|
|
|
|
// Le texte d'un porteur, ou la faute qui dit qu'il manque.
|
|
|
|
|
function lirePorteur(lire, chemin) {
|
|
|
|
|
try {
|
|
|
|
|
return { texte: lire(chemin), faute: null };
|
|
|
|
|
} catch (erreur) {
|
|
|
|
|
if (!estAbsent(erreur)) throw erreur;
|
|
|
|
|
return { texte: null, faute: { chemin, faute: 'porteur absent' } };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* La vérification d'un arbre : les régions de chaque porteur, puis l'écart
|
|
|
|
|
* de version du manifeste. Seuls les porteurs et le manifeste se lisent.
|
|
|
|
|
*
|
|
|
|
|
* @param {{racine?: string, porteurs?: ReadonlyArray<string>, livraison?: boolean,
|
|
|
|
|
* version?: string, lire?: (chemin: string) => string}} options
|
|
|
|
|
* @returns {{fautes: Faute[], ecart: null|{manifeste: string|null, construction: string},
|
|
|
|
|
* regions: number, code: 0|1}}
|
|
|
|
|
* code 1 sur une faute, ou sur un écart sous livraison
|
|
|
|
|
*/
|
|
|
|
|
export function verifierArbre({ racine = RACINE, porteurs = PORTEURS, livraison = false, version = VERSION.affichee, lire = lecteurDe(racine) } = {}) {
|
|
|
|
|
const contexte = contexteDe(lire);
|
|
|
|
|
const producteurs = producteursDe(contexte);
|
|
|
|
|
const fautes = porteurs.length === 0 ? [SANS_PORTEUR] : [];
|
|
|
|
|
let regions = 0;
|
|
|
|
|
for (const chemin of porteurs) {
|
|
|
|
|
const { texte, faute } = lirePorteur(lire, chemin);
|
|
|
|
|
if (faute !== null) {
|
|
|
|
|
fautes.push(faute);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
fautes.push(...verifierRegions(texte, producteurs).map(auChemin(chemin)));
|
|
|
|
|
try {
|
|
|
|
|
regions += lireRegions(texte).length;
|
|
|
|
|
} catch (erreur) {
|
|
|
|
|
if (!(erreur instanceof ErreurRegions)) throw erreur;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let ecart = null;
|
|
|
|
|
try {
|
|
|
|
|
const manifeste = contexte.manifeste();
|
|
|
|
|
ecart = manifeste === null ? { manifeste: null, construction: version } : ecartDeVersion(manifeste, version);
|
|
|
|
|
} catch (erreur) {
|
|
|
|
|
fautes.push({ chemin: CHEMIN_MANIFESTE, faute: erreur.message });
|
|
|
|
|
}
|
|
|
|
|
const code = fautes.length > 0 || (livraison && ecart !== null) ? 1 : 0;
|
|
|
|
|
return { fautes, ecart, regions, code };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* L'engendrement d'un arbre : chaque porteur reçoit ses régions produites ;
|
|
|
|
|
* seuls ceux qui changent s'écrivent, et rien ne s'écrit quand un porteur
|
|
|
|
|
* est en faute.
|
|
|
|
|
*
|
|
|
|
|
* @param {{racine?: string, porteurs?: ReadonlyArray<string>, lire?: (chemin: string) => string,
|
|
|
|
|
* ecrire?: (chemin: string, texte: string) => void}} options
|
|
|
|
|
* @returns {{fautes: Faute[], ecrits: string[], code: 0|1}}
|
|
|
|
|
*/
|
|
|
|
|
export function engendrerArbre({
|
|
|
|
|
racine = RACINE,
|
|
|
|
|
porteurs = PORTEURS,
|
|
|
|
|
lire = lecteurDe(racine),
|
|
|
|
|
ecrire = (chemin, texte) => writeFileSync(join(racine, chemin), texte),
|
|
|
|
|
} = {}) {
|
|
|
|
|
const producteurs = producteursDe(contexteDe(lire));
|
|
|
|
|
const fautes = porteurs.length === 0 ? [SANS_PORTEUR] : [];
|
|
|
|
|
const aEcrire = [];
|
|
|
|
|
for (const chemin of porteurs) {
|
|
|
|
|
const { texte, faute } = lirePorteur(lire, chemin);
|
|
|
|
|
if (faute !== null) {
|
|
|
|
|
fautes.push(faute);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
if (lireRegions(texte).length === 0) {
|
|
|
|
|
fautes.push({ chemin, faute: 'aucune région engendrée : un porteur en a au moins une' });
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const rendu = remplacerRegions(texte, producteurs);
|
|
|
|
|
if (rendu !== texte) aEcrire.push([chemin, rendu]);
|
|
|
|
|
} catch (erreur) {
|
|
|
|
|
if (erreur instanceof ErreurRegions) fautes.push(...erreur.fautes.map(auChemin(chemin)));
|
|
|
|
|
else fautes.push({ chemin, faute: `le producteur échoue : ${erreur.message}` });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (fautes.length > 0) return { fautes, ecrits: [], code: 1 };
|
|
|
|
|
for (const [chemin, rendu] of aEcrire) ecrire(chemin, rendu);
|
|
|
|
|
return { fautes, ecrits: aEcrire.map(([chemin]) => chemin), code: 0 };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const USAGE =
|
|
|
|
|
'usage : node scripts/documentation.js engendrer | verifier [--livraison] [--racine <dossier>] [--porteur <chemin>]…';
|
|
|
|
|
|
|
|
|
|
// Les arguments lus : { mode, livraison, racine, porteurs }, ou null quand
|
|
|
|
|
// ils ne se lisent pas.
|
|
|
|
|
function lireArguments(argv) {
|
|
|
|
|
const [mode, ...reste] = argv;
|
|
|
|
|
if (mode !== 'engendrer' && mode !== 'verifier') return null;
|
|
|
|
|
const lus = { mode, livraison: false, racine: null, porteurs: [] };
|
|
|
|
|
for (let rang = 0; rang < reste.length; rang += 1) {
|
|
|
|
|
const option = reste[rang];
|
|
|
|
|
if (option === '--livraison' && mode === 'verifier') lus.livraison = true;
|
|
|
|
|
else if ((option === '--racine' || option === '--porteur') && rang + 1 < reste.length) {
|
|
|
|
|
rang += 1;
|
|
|
|
|
if (option === '--racine') lus.racine = resolve(reste[rang]);
|
|
|
|
|
else lus.porteurs.push(reste[rang]);
|
|
|
|
|
} else return null;
|
|
|
|
|
}
|
|
|
|
|
return lus;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* La ligne de commande : lit ses arguments, lance le mode, écrit son bilan
|
|
|
|
|
* ligne à ligne sur la sortie.
|
|
|
|
|
*
|
|
|
|
|
* @param {string[]} argv sans l'exécutable ni le script
|
|
|
|
|
* @param {{racine?: string, porteurs?: ReadonlyArray<string>, version?: string,
|
|
|
|
|
* lire?: (chemin: string) => string, ecrire?: (chemin: string, texte: string) => void,
|
|
|
|
|
* sortie?: (ligne: string) => void}} [options]
|
|
|
|
|
* @returns {0|1|2} 2 pour des arguments illisibles, sans rien lire
|
|
|
|
|
*/
|
|
|
|
|
export function executer(argv, { racine = RACINE, porteurs = PORTEURS, version, lire, ecrire, sortie = (ligne) => process.stdout.write(`${ligne}\n`) } = {}) {
|
|
|
|
|
const lus = lireArguments(argv);
|
|
|
|
|
if (lus === null) {
|
|
|
|
|
sortie(USAGE);
|
|
|
|
|
return 2;
|
|
|
|
|
}
|
|
|
|
|
const options = {
|
|
|
|
|
racine: lus.racine ?? racine,
|
|
|
|
|
porteurs: lus.porteurs.length > 0 ? lus.porteurs : porteurs,
|
|
|
|
|
...(lire === undefined ? {} : { lire }),
|
|
|
|
|
};
|
|
|
|
|
const ecrireFautes = (fautes) => fautes.forEach(({ chemin, faute }) => sortie(`${chemin} : ${faute}`));
|
|
|
|
|
if (lus.mode === 'engendrer') {
|
|
|
|
|
const bilan = engendrerArbre({ ...options, ...(ecrire === undefined ? {} : { ecrire }) });
|
|
|
|
|
ecrireFautes(bilan.fautes);
|
|
|
|
|
bilan.ecrits.forEach((chemin) => sortie(`engendré : ${chemin}`));
|
|
|
|
|
sortie(`engendrer : ${options.porteurs.length} porteur(s), ${bilan.ecrits.length} réécrit(s), ${bilan.fautes.length} faute(s)`);
|
|
|
|
|
return bilan.code;
|
|
|
|
|
}
|
|
|
|
|
const bilan = verifierArbre({ ...options, livraison: lus.livraison, ...(version === undefined ? {} : { version }) });
|
|
|
|
|
ecrireFautes(bilan.fautes);
|
|
|
|
|
if (bilan.ecart !== null) {
|
|
|
|
|
const portee = lus.livraison ? 'bloquant' : 'non bloquant';
|
|
|
|
|
const manifeste = bilan.ecart.manifeste ?? 'aucune (manifeste absent ou sans capture)';
|
|
|
|
|
sortie(`écart de version (${portee}) : manifeste ${manifeste}, construction ${bilan.ecart.construction}`);
|
|
|
|
|
}
|
|
|
|
|
sortie(`verifier : ${options.porteurs.length} porteur(s), ${bilan.regions} région(s), ${bilan.fautes.length} faute(s)`);
|
|
|
|
|
return bilan.code;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
|
|
|
process.exitCode = executer(process.argv.slice(2));
|
|
|
|
|
}
|