[ADD] documentation: verifier checks every cited image against the disk
verifier read no image: an older PNG published under an unchanged manifest, or an image outside doc/captures cited by the guide, left every series green. It now refuses any image a carrier cites outside doc/captures/<name>.png, in every Markdown or HTML form, and checks each cited capture's hash and size against its manifest entry. When every carrier is read, a manifest entry no carrier cites is a fault too; a check narrowed by --porteur does not judge the carriers it skips. Checked: 3877 node tests from the index alone; verifier at 0 fault. --- FR --- [ADD] documentation : verifier confronte chaque image citée au disque verifier ne lisait aucune image : un PNG plus ancien publié sous un manifeste inchangé, ou une image hors de doc/captures citée par le guide, laissaient toutes les séries vertes. Il refuse désormais toute image citée hors de doc/captures/<nom>.png, sous chaque forme Markdown ou HTML, et confronte l'empreinte et la taille de chaque capture citée à son entrée du manifeste. Quand tous les porteurs sont lus, une entrée que nul ne cite est une faute ; restreint par --porteur, il ne juge pas les porteurs qu'il ne lit pas. Vérifié : 3877 node depuis l'index seul ; verifier à 0 faute. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
aaa2c1a3a7
commit
55765e93c2
6 changed files with 376 additions and 16 deletions
|
|
@ -30,7 +30,8 @@
|
|||
// 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.
|
||||
// fichiers de PORTEURS se lisent, avec le manifeste et les captures qu'ils
|
||||
// citent.
|
||||
//
|
||||
// verifier mesure aussi l'écart entre la version réelle inscrite au
|
||||
// manifeste des captures et celle de la construction (ecartDeVersion,
|
||||
|
|
@ -38,6 +39,18 @@
|
|||
// bloquant sous --livraison (§ 19.7). Un manifeste absent ne prouve aucune
|
||||
// version, et compte pour un écart.
|
||||
//
|
||||
// verifier contrôle enfin les images que les porteurs affichent (§ 19.1,
|
||||
// § 19.7). Toute image citée — image Markdown, en ligne ou par référence,
|
||||
// balise <img>, lien vers un fichier image — a la forme
|
||||
// doc/captures/<nom>.png ; toute autre cible est une faute qui la nomme.
|
||||
// Chaque capture citée a son entrée au manifeste, son fichier sur le disque,
|
||||
// et ce fichier a l'empreinte et les dimensions que l'entrée inscrit
|
||||
// (fautesDImage) : un PNG remplacé par une version antérieure, le manifeste
|
||||
// inchangé, fait échouer la vérification, donc la construction de livraison.
|
||||
// Réciproquement, chaque capture inscrite au manifeste est citée par un
|
||||
// porteur : un document privé de ses images ne passe pas sur un balayage
|
||||
// vide.
|
||||
//
|
||||
// Ligne de commande, depuis la racine du projet ou d'ailleurs :
|
||||
// node scripts/documentation.js engendrer | verifier [--livraison]
|
||||
// [--racine <dossier>] [--porteur <chemin>]…
|
||||
|
|
@ -62,7 +75,7 @@ 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';
|
||||
import { CHEMIN_MANIFESTE, cheminImage, ecartDeVersion, fautesDImage } from './documentation/manifeste.js';
|
||||
import { tableauConstantes } from './banc/releve.js';
|
||||
|
||||
/** @typedef {{nom: string, debut: number, fin: number, contenu: string}} Region
|
||||
|
|
@ -617,6 +630,154 @@ export function producteursDe(contexte) {
|
|||
);
|
||||
}
|
||||
|
||||
// --- Les images que les porteurs affichent (§ 19.1, § 19.7) -----------------------
|
||||
|
||||
// La seule forme d'une image affichée : une capture du parcours, en PNG.
|
||||
const FORME_CAPTURE = /^doc\/captures\/[A-Za-z0-9._-]+\.png$/;
|
||||
// L'extension d'un fichier image : un lien vers un tel fichier montre une
|
||||
// image au lecteur, comme une image en ligne.
|
||||
const EXTENSION_IMAGE = /\.(?:png|jpe?g|gif|svg|webp|avif|bmp|ico|tiff?)$/i;
|
||||
|
||||
// La cible d'une image ou d'un lien Markdown : entre chevrons, ou sans blanc
|
||||
// avec au plus un niveau de parenthèses ; puis un titre facultatif.
|
||||
const CIBLE = String.raw`\s*(<[^>\n]*>|[^\s()<>]*(?:\([^\s()]*\)[^\s()<>]*)*)(?:\s+(?:"[^"\n]*"|'[^'\n]*'|\([^)\n]*\)))?\s*\)`;
|
||||
// Un caractère du texte entre crochets : un caractère échappé, crochet
|
||||
// compris, ou tout autre qu'un crochet ; un saut de ligne seul, que ne suit
|
||||
// pas une ligne vide, car une ligne vide ferme le paragraphe et ni image ni
|
||||
// lien ne la franchit. Les quatre formes s'excluent : aucun retour en
|
||||
// arrière ne se multiplie.
|
||||
const CARACTERE = String.raw`(?:\\[^\n]|\\(?=\n)|[^\[\]\\\n]|\n(?![ \t\r]*\n))`;
|
||||
// Le texte entre crochets, avec au plus un niveau de crochets imbriqués :
|
||||
// une image dans un lien, [](…), garde ses deux cibles, et le texte
|
||||
// d'une image, ![Écran [ancien]](…), peut en porter.
|
||||
const CROCHETS = String.raw`\[((?:${CARACTERE}|\[${CARACTERE}*\])*)\]`;
|
||||
// Une étiquette de référence : sans crochet non échappé (CommonMark).
|
||||
const ETIQUETTE = String.raw`\[(${CARACTERE}*)\]`;
|
||||
const IMAGE_EN_LIGNE = new RegExp(String.raw`!${CROCHETS}\(${CIBLE}`, 'g');
|
||||
const LIEN_EN_LIGNE = new RegExp(String.raw`(?<!!)${CROCHETS}\(${CIBLE}`, 'g');
|
||||
// Une définition de référence, en tête de ligne : [étiquette]: cible.
|
||||
const DEFINITION = /^ {0,3}\[([^\]\n]+)\]:[ \t]*(<[^>\n]*>|\S+)/gm;
|
||||
// Une image par référence : ![texte][étiquette], ![texte][] ou ![texte],
|
||||
// pourvu que ce dernier ne s'ouvre pas sur une cible en ligne.
|
||||
const IMAGE_PAR_REFERENCE = new RegExp(String.raw`!${CROCHETS}(?:${ETIQUETTE}|(?![(\[]))`, 'g');
|
||||
// Une balise HTML d'image, ou une source d'une balise <picture>.
|
||||
const BALISE_IMAGE = /<(img|source)\b[^>]*>/gi;
|
||||
const ATTRIBUT_SRC = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/i;
|
||||
const ATTRIBUT_SRCSET = /\bsrcset\s*=/i;
|
||||
|
||||
// Une étiquette de référence comparable : sans casse, blancs réduits.
|
||||
const etiquette = (texte) => texte.trim().replace(/\s+/g, ' ').toLowerCase();
|
||||
// Une cible sans ses chevrons.
|
||||
const sansChevrons = (cible) => (cible.startsWith('<') && cible.endsWith('>') ? cible.slice(1, -1) : cible);
|
||||
|
||||
/**
|
||||
* Les images qu'un document Markdown affiche, dans l'ordre du texte : les
|
||||
* images en ligne et par référence, les balises <img> et <source>, les
|
||||
* liens dont la cible a une extension d'image. Chaque citation est
|
||||
* { cible, sorte, indice } : cible est le chemin cité, null pour une balise
|
||||
* sans src lisible ou qui porte un srcset ; sorte est 'image', 'reference',
|
||||
* 'lien', 'balise' ou 'srcset' ; indice est la position dans le texte. Un
|
||||
* texte de code n'est pas mis à part : un exemple d'image se cite aussi.
|
||||
*
|
||||
* @param {string} texte
|
||||
* @returns {Array<{cible: string|null, sorte: string, indice: number}>}
|
||||
*/
|
||||
export function imagesCitees(texte) {
|
||||
if (typeof texte !== 'string') throw new TypeError('les images se relèvent dans une chaîne');
|
||||
const citees = [];
|
||||
for (const trouve of texte.matchAll(IMAGE_EN_LIGNE)) citees.push({ cible: sansChevrons(trouve[2]), sorte: 'image', indice: trouve.index });
|
||||
for (const trouve of texte.matchAll(LIEN_EN_LIGNE)) {
|
||||
const cible = sansChevrons(trouve[2]);
|
||||
if (EXTENSION_IMAGE.test(cible.replace(/[?#].*$/, ''))) citees.push({ cible, sorte: 'lien', indice: trouve.index });
|
||||
}
|
||||
const definitions = new Map();
|
||||
for (const trouve of texte.matchAll(DEFINITION)) {
|
||||
const cle = etiquette(trouve[1]);
|
||||
if (!definitions.has(cle)) definitions.set(cle, { cible: sansChevrons(trouve[2]), indice: trouve.index, utilisee: false });
|
||||
}
|
||||
for (const trouve of texte.matchAll(IMAGE_PAR_REFERENCE)) {
|
||||
const definition = definitions.get(etiquette(trouve[2] || trouve[1]));
|
||||
if (definition === undefined) continue;
|
||||
definition.utilisee = true;
|
||||
citees.push({ cible: definition.cible, sorte: 'reference', indice: trouve.index });
|
||||
}
|
||||
// Une définition vers un fichier image sert un lien par référence, ou
|
||||
// une image : elle est citée une fois, à sa place, si aucune image ne l'a
|
||||
// déjà prise.
|
||||
for (const { cible, indice, utilisee } of definitions.values()) {
|
||||
if (!utilisee && EXTENSION_IMAGE.test(cible.replace(/[?#].*$/, ''))) citees.push({ cible, sorte: 'lien', indice });
|
||||
}
|
||||
for (const trouve of texte.matchAll(BALISE_IMAGE)) {
|
||||
const balise = trouve[0];
|
||||
if (ATTRIBUT_SRCSET.test(balise)) citees.push({ cible: null, sorte: 'srcset', indice: trouve.index });
|
||||
const src = ATTRIBUT_SRC.exec(balise);
|
||||
if (src !== null) citees.push({ cible: src[1] ?? src[2] ?? src[3], sorte: 'balise', indice: trouve.index });
|
||||
else if (trouve[1].toLowerCase() === 'img') citees.push({ cible: null, sorte: 'balise', indice: trouve.index });
|
||||
}
|
||||
return citees.sort((a, b) => a.indice - b.indice);
|
||||
}
|
||||
|
||||
/** Vrai pour une cible citée qu'un document a le droit d'afficher. */
|
||||
export const estCapture = (cible) => typeof cible === 'string' && FORME_CAPTURE.test(cible);
|
||||
|
||||
/**
|
||||
* Les fautes des images d'un document : chaque citation dont la cible n'a
|
||||
* pas la forme doc/captures/<nom>.png, nommée avec sa ligne ; une balise
|
||||
* sans src lisible ; un srcset, dont les cibles échappent au contrôle.
|
||||
*
|
||||
* @param {string} chemin le document, relatif à la racine
|
||||
* @param {string} texte
|
||||
* @returns {Faute[]}
|
||||
*/
|
||||
export function fautesImagesCitees(chemin, texte) {
|
||||
const fautes = [];
|
||||
for (const { cible, sorte, indice } of imagesCitees(texte)) {
|
||||
const ligne = `ligne ${ligneDe(texte, indice)} : `;
|
||||
if (sorte === 'srcset') fautes.push({ chemin, faute: `${ligne}image en srcset : seules les captures doc/captures/<nom>.png s’affichent` });
|
||||
else if (cible === null) fautes.push({ chemin, faute: `${ligne}balise <img> sans src lisible` });
|
||||
else if (!estCapture(cible)) fautes.push({ chemin, faute: `${ligne}image hors de doc/captures : « ${cible} »` });
|
||||
}
|
||||
return fautes;
|
||||
}
|
||||
|
||||
// Les fautes des captures que les porteurs citent, contre le manifeste et le
|
||||
// disque : chaque image une fois, dans l'ordre de sa première citation.
|
||||
// Une image absente du manifeste ne se lit pas ; une image présente se lit
|
||||
// et passe fautesDImage. Quand tous les porteurs de PORTEURS sont lus
|
||||
// (complet), suivent, dans l'ordre du manifeste, ses captures que nul
|
||||
// porteur ne cite : une capture inscrite et jamais montrée n'est pas
|
||||
// examinée, et zéro capture examinée n'est pas zéro capture périmée (§ 19.7).
|
||||
// Une vérification restreinte par --porteur ne juge pas les citations des
|
||||
// porteurs qu'elle ne lit pas.
|
||||
function fautesDesCaptures(citees, manifeste, lireOctets, complet) {
|
||||
const entrees = new Map();
|
||||
for (const entree of Array.isArray(manifeste?.captures) ? manifeste.captures : []) {
|
||||
const chemin = cheminImage(entree?.fichier);
|
||||
if (!entrees.has(chemin)) entrees.set(chemin, entree);
|
||||
}
|
||||
const fautes = [];
|
||||
for (const [image, porteur] of citees) {
|
||||
const entree = entrees.get(image);
|
||||
if (entree === undefined) {
|
||||
fautes.push({ chemin: image, faute: `citée par ${porteur}, absente du manifeste` });
|
||||
continue;
|
||||
}
|
||||
let octets;
|
||||
try {
|
||||
octets = lireOctets(image);
|
||||
} catch (erreur) {
|
||||
if (!estAbsent(erreur)) throw erreur;
|
||||
fautes.push({ chemin: image, faute: `citée par ${porteur}, absente du disque` });
|
||||
continue;
|
||||
}
|
||||
fautes.push(...fautesDImage(entree, octets));
|
||||
}
|
||||
for (const image of complet ? entrees.keys() : []) {
|
||||
if (!citees.has(image)) fautes.push({ chemin: image, faute: 'inscrite au manifeste, citée par aucun porteur' });
|
||||
}
|
||||
return fautes;
|
||||
}
|
||||
|
||||
// --- L'arbre ---------------------------------------------------------------------
|
||||
|
||||
// Vrai pour l'erreur d'un fichier absent.
|
||||
|
|
@ -624,6 +785,9 @@ 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 lecteur d'octets par défaut d'une racine : le contenu d'un chemin
|
||||
// relatif, en Uint8Array.
|
||||
const lecteurOctetsDe = (racine) => (chemin) => readFileSync(join(racine, chemin));
|
||||
|
||||
// Le lecteur d'un fichier JSON de l'arbre, lu au premier besoin, une fois :
|
||||
// l'objet lu, ou null quand le fichier manque. Un texte qui n'est pas du
|
||||
|
|
@ -674,20 +838,34 @@ function lirePorteur(lire, chemin) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* La vérification d'un arbre : les régions et les images citées de chaque
|
||||
* porteur, l'écart de version du manifeste, puis chaque capture citée contre
|
||||
* son entrée du manifeste et son fichier, et, quand PORTEURS est lu en
|
||||
* entier, chaque entrée du manifeste contre les citations. Se lisent : les porteurs et le
|
||||
* manifeste par lire, les seules captures citées par lireOctets.
|
||||
*
|
||||
* @param {{racine?: string, porteurs?: ReadonlyArray<string>, livraison?: boolean,
|
||||
* version?: string, lire?: (chemin: string) => string}} options
|
||||
* version?: string, lire?: (chemin: string) => string,
|
||||
* lireOctets?: (chemin: string) => Uint8Array}} 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
|
||||
* regions: number, images: number, code: 0|1}}
|
||||
* images : le nombre de captures citées, chacune comptée une fois ; 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) } = {}) {
|
||||
export function verifierArbre({
|
||||
racine = RACINE,
|
||||
porteurs = PORTEURS,
|
||||
livraison = false,
|
||||
version = VERSION.affichee,
|
||||
lire = lecteurDe(racine),
|
||||
lireOctets = lecteurOctetsDe(racine),
|
||||
} = {}) {
|
||||
const contexte = contexteDe(lire);
|
||||
const producteurs = producteursDe(contexte);
|
||||
const fautes = porteurs.length === 0 ? [SANS_PORTEUR] : [];
|
||||
let regions = 0;
|
||||
// Chaque capture citée, au premier porteur qui la cite.
|
||||
const citees = new Map();
|
||||
for (const chemin of porteurs) {
|
||||
const { texte, faute } = lirePorteur(lire, chemin);
|
||||
if (faute !== null) {
|
||||
|
|
@ -700,16 +878,19 @@ export function verifierArbre({ racine = RACINE, porteurs = PORTEURS, livraison
|
|||
} catch (erreur) {
|
||||
if (!(erreur instanceof ErreurRegions)) throw erreur;
|
||||
}
|
||||
fautes.push(...fautesImagesCitees(chemin, texte));
|
||||
for (const { cible } of imagesCitees(texte)) if (estCapture(cible) && !citees.has(cible)) citees.set(cible, chemin);
|
||||
}
|
||||
let ecart = null;
|
||||
try {
|
||||
const manifeste = contexte.manifeste();
|
||||
ecart = manifeste === null ? { manifeste: null, construction: version } : ecartDeVersion(manifeste, version);
|
||||
fautes.push(...fautesDesCaptures(citees, manifeste, lireOctets, PORTEURS.every((porteur) => porteurs.includes(porteur))));
|
||||
} 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 };
|
||||
return { fautes, ecart, regions, images: citees.size, code };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -780,11 +961,12 @@ function lireArguments(argv) {
|
|||
*
|
||||
* @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,
|
||||
* lire?: (chemin: string) => string, lireOctets?: (chemin: string) => Uint8Array,
|
||||
* 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`) } = {}) {
|
||||
export function executer(argv, { racine = RACINE, porteurs = PORTEURS, version, lire, lireOctets, ecrire, sortie = (ligne) => process.stdout.write(`${ligne}\n`) } = {}) {
|
||||
const lus = lireArguments(argv);
|
||||
if (lus === null) {
|
||||
sortie(USAGE);
|
||||
|
|
@ -803,14 +985,19 @@ export function executer(argv, { racine = RACINE, porteurs = PORTEURS, version,
|
|||
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 }) });
|
||||
const bilan = verifierArbre({
|
||||
...options,
|
||||
livraison: lus.livraison,
|
||||
...(version === undefined ? {} : { version }),
|
||||
...(lireOctets === undefined ? {} : { lireOctets }),
|
||||
});
|
||||
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)`);
|
||||
sortie(`verifier : ${options.porteurs.length} porteur(s), ${bilan.regions} région(s), ${bilan.images} image(s), ${bilan.fautes.length} faute(s)`);
|
||||
return bilan.code;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@
|
|||
// Épreuves des documents livrés, sur les vrais fichiers (§ 16.1, § 19.7 à
|
||||
// § 19.9) : GUIDE-USAGE.md suit les étapes du parcours et apparie ses images
|
||||
// au manifeste et au disque ; les régions engendrées de README.md et du guide
|
||||
// sont telles qu'engendrer les écrirait ; la section sur l'algorithme cite
|
||||
// sont telles qu'engendrer les écrirait ; README.md et le guide n'affichent
|
||||
// que des captures doc/captures/<nom>.png, et chaque PNG publié a
|
||||
// l'empreinte et les dimensions que le manifeste inscrit, au niveau de
|
||||
// l'épreuve comme de verifier ; la section sur l'algorithme cite
|
||||
// des nombres que l'arithmétique et le diagnostic des démonstrations livrées
|
||||
// recalculent, et ne cite ni code, ni identifiant exporté, ni module ;
|
||||
// GUIDE-WINDOWS.md, texte seul, cite le gabarit du nom de l'exécutable et la
|
||||
|
|
@ -33,6 +36,9 @@ import {
|
|||
PRODUCTEURS,
|
||||
RACINE,
|
||||
engendrerArbre,
|
||||
executer,
|
||||
fautesImagesCitees,
|
||||
imagesCitees,
|
||||
lireRegions,
|
||||
tableauRegistre,
|
||||
verifierArbre,
|
||||
|
|
@ -46,6 +52,7 @@ import {
|
|||
REPORTEES,
|
||||
cheminImage,
|
||||
cheminsHorsRacine,
|
||||
fautesDImage,
|
||||
imagesReferencees,
|
||||
verifierAppariement,
|
||||
verifierOrdre,
|
||||
|
|
@ -57,6 +64,7 @@ const GUIDE_WINDOWS = 'GUIDE-WINDOWS.md';
|
|||
const README = 'README.md';
|
||||
|
||||
const lire = (chemin) => readFileSync(join(RACINE, chemin), 'utf8');
|
||||
const lireOctets = (chemin) => readFileSync(join(RACINE, chemin));
|
||||
const lireManifeste = () => JSON.parse(lire(CHEMIN_MANIFESTE));
|
||||
|
||||
// Les images PNG du dossier des captures, en chemins relatifs à la racine.
|
||||
|
|
@ -223,6 +231,161 @@ describe('documentation : le guide d’usage suit les étapes et apparie ses ima
|
|||
});
|
||||
});
|
||||
|
||||
// --- Les images que la documentation affiche (§ 19.1, § 19.7) -----------------------
|
||||
|
||||
// Un lecteur de l'arbre réel où un document est remplacé par un texte donné.
|
||||
const lireAvec = (remplace, texte) => (chemin) => (chemin === remplace ? texte : lire(chemin));
|
||||
|
||||
describe('documentation : les documents n’affichent que les captures, telles que le manifeste les inscrit (§ 19.1, § 19.7)', () => {
|
||||
test('le guide d’usage et README.md ne citent aucune image hors de doc/captures', () => {
|
||||
for (const document of [GUIDE_USAGE, README]) assert.deepEqual(fautesImagesCitees(document, lire(document)), [], document);
|
||||
// Le relevé n'est pas vide : il voit chaque image que l'appariement voit.
|
||||
const cibles = imagesCitees(lire(GUIDE_USAGE)).map((citee) => citee.cible);
|
||||
assert.deepEqual([...new Set(cibles)].sort(), [...imagesReferencees(lire(GUIDE_USAGE))].sort());
|
||||
assert.equal(new Set(cibles).size, CAPTURES.length);
|
||||
});
|
||||
|
||||
test('rouge : une image hors de doc/captures, ou qui n’y est pas un PNG, se relève en mémoire, chacune nommée', () => {
|
||||
const texte = lire(GUIDE_USAGE);
|
||||
const altere = `${texte}\n\n\n\n`;
|
||||
const fautes = fautesImagesCitees(GUIDE_USAGE, altere);
|
||||
assert.deepEqual(fautes.map((f) => f.chemin), [GUIDE_USAGE, GUIDE_USAGE]);
|
||||
assert.match(fautes[0].faute, /image hors de doc\/captures : « images\/ancienne\.png »/);
|
||||
assert.match(fautes[1].faute, /image hors de doc\/captures : « doc\/captures\/x\.jpg »/);
|
||||
// La construction refuse aussi : verifier lit les mêmes fautes.
|
||||
const bilan = verifierArbre({ lire: lireAvec(GUIDE_USAGE, altere) });
|
||||
assert.deepEqual(bilan.fautes, fautes);
|
||||
assert.equal(bilan.code, 1);
|
||||
});
|
||||
|
||||
test('rouge : les autres formes d’image — balise, référence, lien vers une image — se relèvent dans README.md', () => {
|
||||
const texte = lire(README);
|
||||
const avec = (ajout) => fautesImagesCitees(README, `${texte}\n${ajout}\n`).map((f) => f.faute.replace(/^ligne \d+ : /, ''));
|
||||
assert.deepEqual(avec('<img alt="ancien" src="images/ancienne.png">'), ['image hors de doc/captures : « images/ancienne.png »']);
|
||||
assert.deepEqual(avec("<IMG src='doc/captures/01-premier-ecran.png' srcset=\"images/grande.png 2x\">"), [
|
||||
'image en srcset : seules les captures doc/captures/<nom>.png s’affichent',
|
||||
]);
|
||||
assert.deepEqual(avec('<img alt="sans source">'), ['balise <img> sans src lisible']);
|
||||
assert.deepEqual(avec('![Un écran][ancien]\n\n[ancien]: images/ancienne.png'), ['image hors de doc/captures : « images/ancienne.png »']);
|
||||
// Une image par référence se relève aussi quand sa cible n'a pas
|
||||
// d'extension d'image : c'est la syntaxe qui l'affiche.
|
||||
assert.deepEqual(avec('![Un écran][Distante]\n\n[distante]: <https://images.exemple.invalid/ecran>'), [
|
||||
'image hors de doc/captures : « https://images.exemple.invalid/ecran »',
|
||||
]);
|
||||
assert.deepEqual(avec('Voir [l’ancien écran](images/ancienne.png).'), ['image hors de doc/captures : « images/ancienne.png »']);
|
||||
// Un texte entre crochets coupé à la ligne, ou qui porte des crochets,
|
||||
// imbriqués ou échappés, n'arrête pas le relevé : CommonMark affiche
|
||||
// ces images et ces liens.
|
||||
// Une cible sans extension d'image ne se relève que comme image : aucun
|
||||
// lien ne la rattrape.
|
||||
const distante = 'https://images.exemple.invalid/ecran';
|
||||
const formes = [
|
||||
['', 'images/ancienne.png'],
|
||||
['![Écran [ancien]](images/ancienne.png)', 'images/ancienne.png'],
|
||||
[``, distante],
|
||||
[`![Écran \\]](${distante})`, distante],
|
||||
['Voir [l’ancien\nécran](images/ancienne.png).', 'images/ancienne.png'],
|
||||
[`![Un\nécran][Distante]\n\n[distante]: <${distante}>`, distante],
|
||||
[`![Un écran][Image\ndistante]\n\n[image distante]: <${distante}>`, distante],
|
||||
];
|
||||
assert.deepEqual(
|
||||
formes.map(([forme]) => [forme, avec(forme)]),
|
||||
formes.map(([forme, cible]) => [forme, [`image hors de doc/captures : « ${cible} »`]]),
|
||||
);
|
||||
// Une ligne vide ferme le paragraphe : ni image ni lien ne la franchit.
|
||||
assert.deepEqual(avec(''), []);
|
||||
assert.deepEqual(avec(''), []);
|
||||
// Un lien vers un document qui n'est pas une image n'en est pas une.
|
||||
assert.deepEqual(avec('Voir [le guide](GUIDE-USAGE.md).'), []);
|
||||
});
|
||||
|
||||
test('chaque image du manifeste a sur le disque l’empreinte et les dimensions qu’il inscrit', () => {
|
||||
const { captures } = lireManifeste();
|
||||
assert.equal(captures.length, CAPTURES.length - A_VENIR.length);
|
||||
for (const capture of captures) assert.deepEqual(fautesDImage(capture, lireOctets(cheminImage(capture.fichier))), [], capture.fichier);
|
||||
});
|
||||
|
||||
test('rouge : les octets de 04-grille-participants passés pour 05-tables-etat donnent une faute qui nomme 05', () => {
|
||||
const entree = lireManifeste().captures.find((c) => c.fichier === '05-tables-etat');
|
||||
const fautes = fautesDImage(entree, lireOctets(cheminImage('04-grille-participants')));
|
||||
assert.deepEqual(fautes.map((f) => f.chemin), [cheminImage('05-tables-etat')]);
|
||||
assert.match(fautes[0].faute, /^image remplacée/);
|
||||
});
|
||||
|
||||
test('verifier, sur l’arbre réel : chaque image citée par un porteur est contrôlée, aucune faute', () => {
|
||||
const bilan = verifierArbre();
|
||||
assert.equal(bilan.images, CAPTURES.length - A_VENIR.length);
|
||||
assert.deepEqual(bilan.fautes, []);
|
||||
const lignes = [];
|
||||
assert.equal(executer(['verifier'], { sortie: (ligne) => lignes.push(ligne) }), 0);
|
||||
assert.match(lignes.at(-1), new RegExp(`, ${bilan.images} image\\(s\\), 0 faute\\(s\\)$`));
|
||||
});
|
||||
|
||||
test('rouge : une capture du manifeste que nul porteur ne cite est une faute de verifier ; zéro citée n’est pas zéro périmée', () => {
|
||||
const fichiers = lireManifeste().captures.map((capture) => cheminImage(capture.fichier));
|
||||
const nonCitee = (chemin) => ({ chemin, faute: 'inscrite au manifeste, citée par aucun porteur' });
|
||||
// Le guide privé de ses images : seuls ses liens vers une capture restent.
|
||||
const sansImages = lire(GUIDE_USAGE)
|
||||
.split('\n')
|
||||
.filter((ligne) => !/^!\[[^\]]*\]\(doc\/captures\//.test(ligne))
|
||||
.join('\n');
|
||||
const restantes = new Set(imagesCitees(sansImages).map((citee) => citee.cible));
|
||||
assert.ok(restantes.size > 0 && restantes.size < fichiers.length, `${restantes.size}`);
|
||||
// Puis privé aussi de ces liens : aucune capture citée.
|
||||
const sansCitation = sansImages.replace(/\]\(doc\/captures\/[^)]*\)/g, '](#)');
|
||||
assert.deepEqual(imagesCitees(sansCitation), []);
|
||||
for (const livraison of [false, true]) {
|
||||
const partiel = verifierArbre({ livraison, lire: lireAvec(GUIDE_USAGE, sansImages) });
|
||||
assert.deepEqual(partiel.fautes, fichiers.filter((chemin) => !restantes.has(chemin)).map(nonCitee));
|
||||
assert.deepEqual([partiel.images, partiel.code], [restantes.size, 1]);
|
||||
const vide = verifierArbre({ livraison, lire: lireAvec(GUIDE_USAGE, sansCitation) });
|
||||
assert.deepEqual(vide.fautes, fichiers.map(nonCitee));
|
||||
assert.deepEqual([vide.images, vide.code], [0, 1]);
|
||||
}
|
||||
// Restreinte par --porteur, la vérification ne juge pas les citations des
|
||||
// porteurs qu'elle ne lit pas : le seul README, sur l'arbre réel, passe.
|
||||
const restreinte = verifierArbre({ porteurs: ['README.md'] });
|
||||
assert.deepEqual([restreinte.fautes, restreinte.code], [[], 0]);
|
||||
assert.equal(executer(['verifier', '--porteur', 'README.md'], { sortie: () => {} }), 0);
|
||||
});
|
||||
|
||||
test('rouge : un PNG publié remplacé par un autre, le manifeste inchangé, fait échouer verifier et la livraison', () => {
|
||||
const remplacee = cheminImage('05-tables-etat');
|
||||
const ancienne = (chemin) => lireOctets(chemin === remplacee ? cheminImage('04-grille-participants') : chemin);
|
||||
for (const livraison of [false, true]) {
|
||||
const bilan = verifierArbre({ livraison, lireOctets: ancienne });
|
||||
assert.deepEqual(bilan.fautes.map((f) => f.chemin), [remplacee]);
|
||||
assert.match(bilan.fautes[0].faute, /^image remplacée/);
|
||||
assert.equal(bilan.code, 1);
|
||||
}
|
||||
const lignes = [];
|
||||
assert.equal(executer(['verifier', '--livraison'], { lireOctets: ancienne, sortie: (ligne) => lignes.push(ligne) }), 1);
|
||||
assert.ok(lignes.some((ligne) => ligne.startsWith(`${remplacee} : image remplacée`)), lignes.join('\n'));
|
||||
});
|
||||
|
||||
test('rouge : une image citée absente du disque, ou absente du manifeste, est une faute de verifier', () => {
|
||||
const absente = cheminImage('05-tables-etat');
|
||||
const sansFichier = (chemin) => {
|
||||
if (chemin === absente) throw Object.assign(new Error(`absent : ${chemin}`), { code: 'ENOENT' });
|
||||
return lireOctets(chemin);
|
||||
};
|
||||
const surDisque = verifierArbre({ lireOctets: sansFichier });
|
||||
assert.deepEqual(surDisque.fautes, [{ chemin: absente, faute: `citée par ${GUIDE_USAGE}, absente du disque` }]);
|
||||
// Un nom inventé, ni au manifeste ni sur le disque : le manifeste suffit à
|
||||
// le refuser, aucun octet ne se lit.
|
||||
const inventee = cheminImage('05z-table-inventee');
|
||||
const lus = [];
|
||||
const horsManifeste = verifierArbre({
|
||||
lire: lireAvec(GUIDE_USAGE, `${lire(GUIDE_USAGE)}\n\n`),
|
||||
lireOctets: (chemin) => (lus.push(chemin), lireOctets(chemin)),
|
||||
});
|
||||
assert.deepEqual(horsManifeste.fautes, [{ chemin: inventee, faute: `citée par ${GUIDE_USAGE}, absente du manifeste` }]);
|
||||
assert.equal(horsManifeste.code, 1);
|
||||
assert.ok(!lus.includes(inventee));
|
||||
assert.equal(lus.length, CAPTURES.length - A_VENIR.length);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Les régions engendrées ---------------------------------------------------------
|
||||
|
||||
describe('documentation : les régions engendrées des documents livrés (§ 19.8)', () => {
|
||||
|
|
|
|||
10
test/fixtures/documentation/README.md
vendored
10
test/fixtures/documentation/README.md
vendored
|
|
@ -19,3 +19,13 @@ dans une copie temporaire.
|
|||
<!-- engendre:fin contrastes -->
|
||||
|
||||
Le texte qui suit la région ne bouge pas quand elle s'engendre.
|
||||
|
||||
## Captures
|
||||
|
||||
Chaque capture du manifeste est citée ici : la vérification refuse une
|
||||
capture inscrite que nul porteur ne montre. Les deux images sont des aplats
|
||||
inventés, aux dimensions du cadre.
|
||||
|
||||

|
||||
|
||||

|
||||
|
|
|
|||
BIN
test/fixtures/documentation/doc/captures/01-premier-ecran.png
vendored
Normal file
BIN
test/fixtures/documentation/doc/captures/01-premier-ecran.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 549 B |
BIN
test/fixtures/documentation/doc/captures/02-import-apercu.png
vendored
Normal file
BIN
test/fixtures/documentation/doc/captures/02-import-apercu.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 197 B |
|
|
@ -32,7 +32,7 @@
|
|||
},
|
||||
"etat": null,
|
||||
"pdf": null,
|
||||
"image": { "sha256": "0000000000000000000000000000000000000000000000000000000000000001", "largeur": 1366, "hauteur": 700 },
|
||||
"image": { "sha256": "7fc987234e652157eb447403d62532084fc7f405b35fd3dc190dbd83bf28d89e", "largeur": 1366, "hauteur": 700 },
|
||||
"empreinte": "0000000000000000000000000000000000000000000000000000000000000002"
|
||||
},
|
||||
{
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
},
|
||||
"etat": null,
|
||||
"pdf": null,
|
||||
"image": { "sha256": "0000000000000000000000000000000000000000000000000000000000000003", "largeur": 1366, "hauteur": 700 },
|
||||
"image": { "sha256": "0841a7a16c56d362c183c7d760e6546f7bbb9577a978052c540bfce098f9f405", "largeur": 1366, "hauteur": 700 },
|
||||
"empreinte": "0000000000000000000000000000000000000000000000000000000000000004"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in a new issue