133 lines
6.5 KiB
JavaScript
133 lines
6.5 KiB
JavaScript
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|||
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|||
|
|
|
|||
|
|
// La police embarquée (§ 11.1, § 14.15, § 19.4) : les deux fichiers de
|
|||
|
|
// src/polices sont ceux que PROVENANCE déclare, octet pour octet ; chacun
|
|||
|
|
// couvre le latin étendu du français et ses ligatures, lu par fontkit ;
|
|||
|
|
// regulier.js et gras.js nomment ces fichiers ; la licence voyage à côté, à
|
|||
|
|
// son seul emplacement, public/licences/. La longueur de la chaîne témoin se
|
|||
|
|
// calcule des avances de la police, sans crénage (contrainte 26).
|
|||
|
|
import assert from 'node:assert/strict';
|
|||
|
|
import { createHash } from 'node:crypto';
|
|||
|
|
import { readFileSync } from 'node:fs';
|
|||
|
|
import { fileURLToPath } from 'node:url';
|
|||
|
|
import fontkit from '@pdf-lib/fontkit';
|
|||
|
|
import { describe, test } from './lanceur.js';
|
|||
|
|
import { cheminPolice, FICHIERS_POLICES, lirePolices } from './polices.js';
|
|||
|
|
import { CHAINE_TEMOIN, lireProvenance, longueurTemoin } from './temoin.js';
|
|||
|
|
|
|||
|
|
const RACINE = fileURLToPath(new URL('..', import.meta.url));
|
|||
|
|
const lire = (chemin) => readFileSync(`${RACINE}${chemin}`);
|
|||
|
|
const sha256 = (octets) => createHash('sha256').update(octets).digest('hex');
|
|||
|
|
const provenance = () => lireProvenance(readFileSync(cheminPolice('PROVENANCE'), 'utf8'));
|
|||
|
|
|
|||
|
|
// Les blocs du latin que la police couvre en entier, bornes comprises, et
|
|||
|
|
// les signes du français hors de ces blocs.
|
|||
|
|
const BLOCS = [
|
|||
|
|
['Latin-1', 0xa0, 0xff],
|
|||
|
|
['Latin étendu A', 0x100, 0x17f],
|
|||
|
|
['Latin étendu B', 0x180, 0x24f],
|
|||
|
|
['Latin étendu additionnel', 0x1e00, 0x1eff],
|
|||
|
|
];
|
|||
|
|
const SIGNES = 'œŒæÆ’«»–—…fi';
|
|||
|
|
|
|||
|
|
// Les points de code à couvrir, dans l'ordre des blocs puis des signes.
|
|||
|
|
function pointsACouvrir() {
|
|||
|
|
const points = [];
|
|||
|
|
for (const [, debut, fin] of BLOCS) for (let point = debut; point <= fin; point += 1) points.push(point);
|
|||
|
|
for (const signe of SIGNES) points.push(signe.codePointAt(0));
|
|||
|
|
return points;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
describe('polices : les fichiers (§ 11.1, § 14.15)', () => {
|
|||
|
|
test('PROVENANCE déclare la version amont et les deux fichiers, dont la taille et l’empreinte sont celles du disque', () => {
|
|||
|
|
const { version, fichiers } = provenance();
|
|||
|
|
assert.match(version ?? '', /^\d+\.\d+$/);
|
|||
|
|
const polices = lirePolices();
|
|||
|
|
const attendus = [
|
|||
|
|
[FICHIERS_POLICES.regulier, polices.regulier],
|
|||
|
|
[FICHIERS_POLICES.gras, polices.gras],
|
|||
|
|
];
|
|||
|
|
for (const [nom, octets] of attendus) {
|
|||
|
|
const entree = fichiers.find((f) => f.nom === nom);
|
|||
|
|
assert.ok(entree !== undefined, `${nom} absent de PROVENANCE`);
|
|||
|
|
assert.equal(octets.length, entree.octets, nom);
|
|||
|
|
assert.equal(sha256(octets), entree.sha256, nom);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('chaque graisse est Noto Sans de la version déclarée, Regular puis Bold', () => {
|
|||
|
|
const { version } = provenance();
|
|||
|
|
const polices = lirePolices();
|
|||
|
|
for (const [graisse, sousFamille] of [['regulier', 'Regular'], ['gras', 'Bold']]) {
|
|||
|
|
const police = fontkit.create(polices[graisse]);
|
|||
|
|
assert.equal(police.familyName, 'Noto Sans', graisse);
|
|||
|
|
assert.equal(police.subfamilyName, sousFamille, graisse);
|
|||
|
|
assert.ok(police.version.startsWith(`Version ${version};`), `${graisse} : ${police.version}`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('chaque graisse couvre Latin-1, les latins étendus A, B et additionnel, et « œŒæÆ’«»–—…fi »', () => {
|
|||
|
|
const points = pointsACouvrir();
|
|||
|
|
assert.equal(points.length, 96 + 128 + 208 + 256 + 11);
|
|||
|
|
const polices = lirePolices();
|
|||
|
|
for (const graisse of ['regulier', 'gras']) {
|
|||
|
|
const police = fontkit.create(polices[graisse]);
|
|||
|
|
const manquants = points
|
|||
|
|
.filter((point) => police.glyphForCodePoint(point).id === 0)
|
|||
|
|
.map((point) => `U+${point.toString(16).toUpperCase().padStart(4, '0')}`);
|
|||
|
|
assert.deepEqual(manquants, [], graisse);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('regulier.js et gras.js réexportent chacun leur fichier, incrusté par « ?inline »', () => {
|
|||
|
|
for (const [module, nom] of [['regulier.js', FICHIERS_POLICES.regulier], ['gras.js', FICHIERS_POLICES.gras]]) {
|
|||
|
|
const texte = readFileSync(cheminPolice(module), 'utf8');
|
|||
|
|
// « fro[m] » : la garde des chargements (test/chargements.js) ne lit
|
|||
|
|
// pas ce motif comme un import.
|
|||
|
|
const reexports = [...texte.matchAll(/^export \{ default \} fro[m] '\.\/([^'?]+)\?inline';$/gm)].map((m) => m[1]);
|
|||
|
|
assert.deepEqual(reexports, [nom], module);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('la licence de la police est public/licences/NotoSans-OFL.txt, l’OFL.txt de l’archive amont, et TIERS.txt la nomme', () => {
|
|||
|
|
const { fichiers } = provenance();
|
|||
|
|
const ofl = fichiers.find((f) => f.nom === 'NotoSans-OFL.txt');
|
|||
|
|
assert.ok(ofl !== undefined, 'NotoSans-OFL.txt absent de PROVENANCE');
|
|||
|
|
assert.equal(ofl.amont, 'OFL.txt');
|
|||
|
|
const texte = lire('public/licences/NotoSans-OFL.txt');
|
|||
|
|
assert.equal(sha256(texte), ofl.sha256);
|
|||
|
|
assert.match(texte.toString('utf8'), /SIL OPEN FONT LICENSE Version 1\.1/);
|
|||
|
|
assert.match(lire('public/licences/TIERS.txt').toString('utf8'), /^Noto Sans .* — OFL-1\.1 — NotoSans-OFL\.txt$/m);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('polices : la chaîne témoin (§ 19.4, contrainte 26)', () => {
|
|||
|
|
test('la chaîne témoin est celle du contrat', () => {
|
|||
|
|
assert.equal(CHAINE_TEMOIN, 'Ôjgq Æ W — Benoît Œuvre fi');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('sa longueur est la somme des avances, sans crénage ni ligature : la mise en forme de fontkit sans « kern » ni « liga » la redonne', () => {
|
|||
|
|
const polices = lirePolices();
|
|||
|
|
for (const graisse of ['regulier', 'gras']) {
|
|||
|
|
const police = fontkit.create(polices[graisse]);
|
|||
|
|
const brute = police.layout(CHAINE_TEMOIN, { kern: false, liga: false }).advanceWidth;
|
|||
|
|
const avecCrenage = police.layout(CHAINE_TEMOIN, { liga: false }).advanceWidth;
|
|||
|
|
assert.ok(Math.abs(longueurTemoin(polices[graisse], police.unitsPerEm) - brute) < 1e-9, graisse);
|
|||
|
|
// Le crénage change la longueur : la somme ne le confond pas avec elle.
|
|||
|
|
assert.notEqual(brute, avecCrenage, graisse);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('la longueur croît avec le corps, en proportion ; le gras est plus long ; un corps hors domaine lève', () => {
|
|||
|
|
const { regulier, gras } = lirePolices();
|
|||
|
|
const a16 = longueurTemoin(regulier, 16);
|
|||
|
|
assert.ok(Math.abs(longueurTemoin(regulier, 32) - 2 * a16) < 1e-9);
|
|||
|
|
assert.ok(longueurTemoin(gras, 16) > a16);
|
|||
|
|
assert.ok(longueurTemoin(regulier, 16, 'W') > longueurTemoin(regulier, 16, 'i'));
|
|||
|
|
for (const corps of [0, -1, Number.NaN, Number.POSITIVE_INFINITY, '16']) {
|
|||
|
|
assert.throws(() => longueurTemoin(regulier, corps), RangeError, String(corps));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|