humalis 1.0.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyse/accessibilite.js +13 -5
- package/dist/analyse/analyse.js +43 -3
- package/dist/analyse/langues.js +30 -10
- package/dist/analyse/medias.js +4 -1
- package/dist/analyse/navigation.js +35 -0
- package/dist/analyse/niveaux.js +67 -0
- package/dist/analyse/noms.js +1 -1
- package/dist/analyse/site.js +30 -8
- package/dist/cibles/commun/colorerCode.js +59 -0
- package/dist/cibles/commun/mouvement.js +110 -4
- package/dist/cibles/mobile/projet.js +10 -2
- package/dist/cibles/mobile/rendu.js +6 -3
- package/dist/cibles/statique/page.js +99 -6
- package/dist/cibles/statique/projet.js +46 -0
- package/dist/cibles/statique/valeurs.js +5 -1
- package/dist/cibles/web/css-ecran.js +31 -1
- package/dist/cibles/web/ecran.js +161 -16
- package/dist/cibles/web/interactions.js +64 -3
- package/dist/cibles/web/projet.js +31 -2
- package/dist/cibles/web/theme.js +11 -2
- package/dist/cibles/web/valeurs.js +17 -0
- package/dist/i18n/messages.js +2 -0
- package/dist/lexer/lexer.js +106 -3
- package/dist/navigateur.js +5289 -0
- package/dist/parser/interactions.js +6 -2
- package/dist/parser/langues.js +1 -1
- package/dist/parser/medias.js +6 -1
- package/dist/parser/parser.js +91 -3
- package/dist/parser/theme.js +10 -1
- package/dist/ri/construire.js +27 -1
- package/dist/vocabulaire/table-interactions.js +16 -1
- package/dist/vocabulaire/table.js +4 -4
- package/docs/guide.en.md +183 -2
- package/docs/guide.md +183 -1
- package/llms.txt +14 -1
- package/package.json +4 -2
|
@@ -5,7 +5,7 @@ const SEUIL_NORMAL = 4.5;
|
|
|
5
5
|
const SEUIL_GRAND = 3;
|
|
6
6
|
const motsVides = new Set(['image', 'photo']);
|
|
7
7
|
/** Vérifications qui demandent une intention explicite de l'auteur. */
|
|
8
|
-
export function verifierAccessibilite(page, theme) {
|
|
8
|
+
export function verifierAccessibilite(page, theme, modes = ['clair', 'sombre']) {
|
|
9
9
|
const diagnostics = [];
|
|
10
10
|
const signaler = (gravite, message, ligne) => diagnostics.push({ gravite, categorie: 'accessibilite', message, ligne });
|
|
11
11
|
let titres = 0;
|
|
@@ -69,7 +69,7 @@ export function verifierAccessibilite(page, theme) {
|
|
|
69
69
|
niveauPrecedent = niveau;
|
|
70
70
|
}
|
|
71
71
|
if (['texte', 'titre', 'bouton', 'lien', 'champ'].includes(element.element))
|
|
72
|
-
verifierContraste(element, theme, signaler);
|
|
72
|
+
verifierContraste(element, theme, modes, signaler);
|
|
73
73
|
}
|
|
74
74
|
else if (instruction.type === 'Formulaire')
|
|
75
75
|
parcours(instruction.corps, new Map());
|
|
@@ -83,8 +83,14 @@ export function verifierAccessibilite(page, theme) {
|
|
|
83
83
|
else if (instruction.type === 'Bloc' || instruction.type === 'Boucle')
|
|
84
84
|
parcours(instruction.corps, champs);
|
|
85
85
|
else if (instruction.type === 'Condition') {
|
|
86
|
+
// Les deux branches s'excluent (« si langue est anglais » / « sinon ») : un titre dans
|
|
87
|
+
// chacune ne fait qu'un titre affiché. On garde le plus grand des deux comptes.
|
|
88
|
+
const avant = titres;
|
|
86
89
|
parcours(instruction.alors, champs);
|
|
90
|
+
const apresAlors = titres;
|
|
91
|
+
titres = avant;
|
|
87
92
|
parcours(instruction.sinon, champs);
|
|
93
|
+
titres = Math.max(titres, apresAlors);
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
96
|
};
|
|
@@ -93,18 +99,20 @@ export function verifierAccessibilite(page, theme) {
|
|
|
93
99
|
if (!clics.has(survol.cible))
|
|
94
100
|
signaler('avertissement', 'ce survol n’a pas d’action au clic : le clavier et les écrans tactiles ne peuvent pas le déclencher.', survol.ligne);
|
|
95
101
|
}
|
|
96
|
-
|
|
102
|
+
// Une page qui montre ses sous-pages (« sous page ») laisse le titre principal à chacune d'elles.
|
|
103
|
+
const montreSesSousPages = JSON.stringify(page.corps).includes('"type":"SousPage"');
|
|
104
|
+
if (titres === 0 && !montreSesSousPages)
|
|
97
105
|
signaler('avertissement', `la page « ${page.nom} » n'a pas de titre principal. Ajoutez « titre "…" ».`, page.ligne);
|
|
98
106
|
return diagnostics;
|
|
99
107
|
}
|
|
100
|
-
function verifierContraste(element, theme, signaler) {
|
|
108
|
+
function verifierContraste(element, theme, modes, signaler) {
|
|
101
109
|
const valeur = (nom) => element.styles.find((s) => s.propriete === nom)?.valeur;
|
|
102
110
|
const couleur = valeur('couleur') ?? valeur('texte');
|
|
103
111
|
const fond = valeur('fond');
|
|
104
112
|
if (!couleur && !fond)
|
|
105
113
|
return;
|
|
106
114
|
const seuil = valeur('taille') === 'grande' ? SEUIL_GRAND : SEUIL_NORMAL;
|
|
107
|
-
for (const mode of
|
|
115
|
+
for (const mode of modes) {
|
|
108
116
|
const hex = (nom) => theme.get(nom)?.[mode] ?? COULEURS_CSS[nom]?.[mode];
|
|
109
117
|
const fondHex = fond ? hex(fond) : FOND_PAGE[mode];
|
|
110
118
|
const encreHex = couleur ? hex(couleur) : fondHex ? encreSur(fondHex) : undefined;
|
package/dist/analyse/analyse.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ElementsPage } from './elements.js';
|
|
|
7
7
|
import { optionCanonique, erreurSurElement, OPTIONS } from './styles.js';
|
|
8
8
|
import { contraste, FOND_PAGE } from './couleurs.js';
|
|
9
9
|
import { verifierAccessibilite } from './accessibilite.js';
|
|
10
|
+
import { niveauxDesSousTitres } from './niveaux.js';
|
|
10
11
|
import { verifierElementEcran, verifierBlocEcran } from './ecran.js';
|
|
11
12
|
import { verifierChoixFichier, verifierEnvoiFichier, typeFichierChoisi, aUnApercu, recenserFichiers } from './fichiers.js';
|
|
12
13
|
import { verifierCarte, verifierMarqueur } from './carte.js';
|
|
@@ -44,6 +45,16 @@ const gardeDe = (test) => {
|
|
|
44
45
|
* modifie pas l'arbre et collecte toutes les erreurs au lieu de
|
|
45
46
|
* s'arrêter à la première.
|
|
46
47
|
*/
|
|
48
|
+
/**
|
|
49
|
+
* Les modes que l'application peut montrer. « mode clair » ou « mode sombre » n'en laisse qu'un,
|
|
50
|
+
* sauf si un bouton change le thème (« changer theme par … », « basculer theme »).
|
|
51
|
+
*/
|
|
52
|
+
function modesVus(programme) {
|
|
53
|
+
const mode = programme.theme?.mode?.valeur;
|
|
54
|
+
if (mode !== 'clair' && mode !== 'sombre')
|
|
55
|
+
return ['clair', 'sombre'];
|
|
56
|
+
return JSON.stringify(programme).includes('"action":"changerTheme"') ? ['clair', 'sombre'] : [mode];
|
|
57
|
+
}
|
|
47
58
|
export function analyserSens(programme) {
|
|
48
59
|
const resultat = {
|
|
49
60
|
diagnostics: [],
|
|
@@ -72,11 +83,17 @@ export function analyserSens(programme) {
|
|
|
72
83
|
fenetres: new Map(),
|
|
73
84
|
blocsNommes: new Map(),
|
|
74
85
|
valeursTheme: new Map((programme.theme?.couleurs ?? []).map((c) => [c.nom, { clair: c.clair, sombre: c.sombre ?? c.clair }])),
|
|
86
|
+
modesVus: modesVus(programme),
|
|
75
87
|
langues: { langues: [], traductions: new Map() },
|
|
76
88
|
referencesLangue: new Map(),
|
|
77
89
|
changementsLangue: new Map(),
|
|
78
90
|
};
|
|
79
91
|
// Les langues d'abord : avec elles, « langue » et « anglais » sont des valeurs intégrées.
|
|
92
|
+
// Le niveau automatique des sous-titres, avant tout ce qui lit ces niveaux.
|
|
93
|
+
for (const corps of [...programme.pages.map((p) => p.corps), programme.entete?.corps, programme.pied?.corps, ...programme.composants.map((c) => c.corps)]) {
|
|
94
|
+
if (corps)
|
|
95
|
+
niveauxDesSousTitres(corps);
|
|
96
|
+
}
|
|
80
97
|
resultat.langues = analyserLangues(programme, resultat.diagnostics);
|
|
81
98
|
languesActives.clear();
|
|
82
99
|
for (const langue of resultat.langues.langues)
|
|
@@ -160,7 +177,7 @@ export function analyserSens(programme) {
|
|
|
160
177
|
}
|
|
161
178
|
for (const page of programme.pages) {
|
|
162
179
|
new AnalysePage(page, nomsPages, resultat, classes, partages, composants, [], liens, fonctions).analyser();
|
|
163
|
-
resultat.diagnostics.push(...verifierAccessibilite(page, resultat.valeursTheme));
|
|
180
|
+
resultat.diagnostics.push(...verifierAccessibilite(page, resultat.valeursTheme, resultat.modesVus));
|
|
164
181
|
for (const [sans, genre] of [[page.sansEntete, 'entete'], [page.sansPied, 'pied']]) {
|
|
165
182
|
if (sans && !programme[genre]) {
|
|
166
183
|
resultat.diagnostics.push({
|
|
@@ -614,6 +631,24 @@ class AnalysePage {
|
|
|
614
631
|
case 'Bloc':
|
|
615
632
|
this.blocMiseEnPage(instruction, contexte, place);
|
|
616
633
|
return;
|
|
634
|
+
case 'CodeDocumentation':
|
|
635
|
+
if (contexte === 'evenement')
|
|
636
|
+
this.erreur('un bloc de code se place dans une page.', instruction.ligne);
|
|
637
|
+
return;
|
|
638
|
+
case 'TableauDocumentation':
|
|
639
|
+
if (!instruction.legende)
|
|
640
|
+
this.erreur('un tableau attend une légende entre guillemets : tableau "Commandes" :', instruction.ligne);
|
|
641
|
+
if (instruction.colonnes.length === 0)
|
|
642
|
+
this.erreur('un tableau attend une ligne « colonnes ».', instruction.ligne);
|
|
643
|
+
for (const rangee of instruction.rangees) {
|
|
644
|
+
if (instruction.colonnes.length > 0 && rangee.length !== instruction.colonnes.length)
|
|
645
|
+
this.erreur(`la rangée a ${rangee.length} cellules, mais le tableau a ${instruction.colonnes.length} colonnes.`, rangee[0]?.ligne ?? instruction.ligne);
|
|
646
|
+
}
|
|
647
|
+
if (instruction.legende)
|
|
648
|
+
this.typeDe(instruction.legende);
|
|
649
|
+
instruction.colonnes.forEach((cellule) => this.typeDe(cellule));
|
|
650
|
+
instruction.rangees.flat().forEach((cellule) => this.typeDe(cellule));
|
|
651
|
+
return;
|
|
617
652
|
case 'Action':
|
|
618
653
|
this.action(instruction, contexte);
|
|
619
654
|
if (instruction.message) {
|
|
@@ -1039,6 +1074,7 @@ class AnalysePage {
|
|
|
1039
1074
|
const valeur = this.resultat.valeursTheme.get(option.valeur) ?? COULEURS_CSS[option.valeur];
|
|
1040
1075
|
return { nom: option.valeur, clair: valeur.clair, sombre: valeur.sombre };
|
|
1041
1076
|
},
|
|
1077
|
+
modes: this.resultat.modesVus,
|
|
1042
1078
|
});
|
|
1043
1079
|
}
|
|
1044
1080
|
/** Un texte dans une couleur du thème qui se verrait mal sur le fond de la page (contraste inférieur à 3:1). */
|
|
@@ -1048,7 +1084,7 @@ class AnalysePage {
|
|
|
1048
1084
|
const couleur = this.resultat.valeursTheme.get(valeur);
|
|
1049
1085
|
if (!couleur)
|
|
1050
1086
|
return;
|
|
1051
|
-
for (const mode of
|
|
1087
|
+
for (const mode of this.resultat.modesVus) {
|
|
1052
1088
|
const rapport = contraste(couleur[mode], FOND_PAGE[mode]);
|
|
1053
1089
|
if (rapport < 3) {
|
|
1054
1090
|
this.avertissement(`la couleur « ${valeur} » (mode ${mode}) se voit mal sur le fond de la page (contraste ${rapport.toFixed(1).replace('.', ',')} pour 1, il en faut au moins 3). Choisissez une couleur plus ${mode === 'clair' ? 'foncée' : 'claire'} pour ce mode.`, ligne);
|
|
@@ -1100,7 +1136,7 @@ class AnalysePage {
|
|
|
1100
1136
|
return;
|
|
1101
1137
|
case 'changerTheme': {
|
|
1102
1138
|
const valeur = texteLitteral(action.arguments[0]);
|
|
1103
|
-
if (valeur === undefined || !['clair', 'sombre', 'automatique'].includes(valeur)) {
|
|
1139
|
+
if (valeur === undefined || !['clair', 'sombre', 'automatique', 'bascule'].includes(valeur)) {
|
|
1104
1140
|
this.erreur(`« changer theme par … » prend clair, sombre ou automatique (ici : « ${valeur ?? '?'} »).`, action.ligne);
|
|
1105
1141
|
}
|
|
1106
1142
|
return;
|
|
@@ -1364,6 +1400,9 @@ class AnalysePage {
|
|
|
1364
1400
|
typeDe(expression) {
|
|
1365
1401
|
switch (expression.type) {
|
|
1366
1402
|
case 'Chaine':
|
|
1403
|
+
if ([expression.parties, ...(expression.versions?.map((v) => v.parties) ?? [])].some((parties) => parties.some((p) => p.type === 'accent')) && !['titre', 'soustitre', 'texte'].includes(this.elementEnCours ?? '')) {
|
|
1404
|
+
this.erreur('accent s’emploie seulement dans un titre, un soustitre ou un texte.', expression.ligne);
|
|
1405
|
+
}
|
|
1367
1406
|
this.interpolations(expression);
|
|
1368
1407
|
this.versionsLues(expression);
|
|
1369
1408
|
return TEXTE;
|
|
@@ -1464,6 +1503,7 @@ class AnalysePage {
|
|
|
1464
1503
|
this.usagesEtat.push({ nom: expression.variable, ligne: expression.ligne, mot: 'erreur de' });
|
|
1465
1504
|
return TEXTE;
|
|
1466
1505
|
case 'MouvementReduit':
|
|
1506
|
+
case 'ThemeSombre':
|
|
1467
1507
|
return BOOLEEN;
|
|
1468
1508
|
case 'EtatUtilisateur':
|
|
1469
1509
|
this.litLaSession = true;
|
package/dist/analyse/langues.js
CHANGED
|
@@ -55,8 +55,18 @@ function textesDesOptions(instruction, vus) {
|
|
|
55
55
|
vus.push(serie.texteNom ? plat(serie.texteNom) : litteralDe(serie.nom, serie.ligne));
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
/**
|
|
59
|
-
|
|
58
|
+
/** « si langue est anglais : » (ou « n'est pas ») : ses deux branches sont chacune écrites dans une seule langue. */
|
|
59
|
+
const testeLaLangue = (instruction) => {
|
|
60
|
+
if (instruction.type !== 'Condition')
|
|
61
|
+
return false;
|
|
62
|
+
const test = instruction.test;
|
|
63
|
+
return test.type === 'Comparaison' && test.gauche.type === 'Reference' && test.gauche.nom === 'langue' && test.droite?.type === 'Reference' && langueDuMot(test.droite.nom) !== undefined;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Les textes que la personne voit : contenus d'éléments, titres de blocs, messages, aperçu.
|
|
67
|
+
* Ceux d'une branche « si langue est … » vont aussi dans `uneLangue` : ils n'ont pas à être traduits.
|
|
68
|
+
*/
|
|
69
|
+
function textesVisibles(instructions, vus, uneLangue) {
|
|
60
70
|
const ajouter = (e) => {
|
|
61
71
|
if (e?.type === 'Chaine')
|
|
62
72
|
vus.push(e);
|
|
@@ -86,12 +96,21 @@ function textesVisibles(instructions, vus) {
|
|
|
86
96
|
ajouter(instruction.titre);
|
|
87
97
|
for (const cle of ['corps', 'alors', 'sinon']) {
|
|
88
98
|
const enfants = instruction[cle];
|
|
89
|
-
if (Array.isArray(enfants))
|
|
90
|
-
|
|
99
|
+
if (!Array.isArray(enfants))
|
|
100
|
+
continue;
|
|
101
|
+
if (cle !== 'corps' && uneLangue && testeLaLangue(instruction)) {
|
|
102
|
+
const dedans = [];
|
|
103
|
+
textesVisibles(enfants, dedans, uneLangue);
|
|
104
|
+
for (const texte of dedans)
|
|
105
|
+
uneLangue.add(texte);
|
|
106
|
+
vus.push(...dedans);
|
|
107
|
+
}
|
|
108
|
+
else
|
|
109
|
+
textesVisibles(enfants, vus, uneLangue);
|
|
91
110
|
}
|
|
92
111
|
if (instruction.type === 'Selon')
|
|
93
112
|
for (const cas of instruction.cas)
|
|
94
|
-
textesVisibles(cas.corps, vus);
|
|
113
|
+
textesVisibles(cas.corps, vus, uneLangue);
|
|
95
114
|
}
|
|
96
115
|
}
|
|
97
116
|
/** Tous les littéraux de texte d'un arbre, où qu'ils soient. */
|
|
@@ -235,17 +254,18 @@ export function analyserLangues(programme, diagnostics) {
|
|
|
235
254
|
}
|
|
236
255
|
// Les textes visibles : ceux des pages, des options et du bloc « site ».
|
|
237
256
|
const visibles = [];
|
|
257
|
+
const uneLangue = new Set();
|
|
238
258
|
const pages = programme.pages;
|
|
239
259
|
for (const page of pages) {
|
|
240
|
-
textesVisibles(page.corps, visibles);
|
|
260
|
+
textesVisibles(page.corps, visibles, uneLangue);
|
|
241
261
|
if (page.apercu)
|
|
242
|
-
textesVisibles([page.apercu], visibles);
|
|
262
|
+
textesVisibles([page.apercu], visibles, uneLangue);
|
|
243
263
|
}
|
|
244
264
|
for (const disposition of [programme.entete, programme.pied])
|
|
245
265
|
if (disposition)
|
|
246
|
-
textesVisibles(disposition.corps, visibles);
|
|
266
|
+
textesVisibles(disposition.corps, visibles, uneLangue);
|
|
247
267
|
for (const composant of programme.composants)
|
|
248
|
-
textesVisibles(composant.corps, visibles);
|
|
268
|
+
textesVisibles(composant.corps, visibles, uneLangue);
|
|
249
269
|
for (const cle of ['nom', 'description']) {
|
|
250
270
|
const valeur = programme.site?.[cle];
|
|
251
271
|
if (valeur !== undefined)
|
|
@@ -298,7 +318,7 @@ export function analyserLangues(programme, diagnostics) {
|
|
|
298
318
|
}
|
|
299
319
|
// Les textes visibles sans version dans une langue déclarée.
|
|
300
320
|
for (const texte of visibles) {
|
|
301
|
-
if (!aDesLettres(texte.parties))
|
|
321
|
+
if (!aDesLettres(texte.parties) || uneLangue.has(texte) || texte.sansTraduction)
|
|
302
322
|
continue;
|
|
303
323
|
const cle = texteSource(texte.parties);
|
|
304
324
|
for (const langue of autres) {
|
package/dist/analyse/medias.js
CHANGED
|
@@ -27,6 +27,9 @@ export function verifierMouvement(styles, genre, dansFormulaire, ligne, s) {
|
|
|
27
27
|
s.erreur('« au defilement » suit le nom d’une animation : « animation montee au defilement ».', ligne);
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
30
|
+
if (animation.cascade && !['bloc', 'section', 'ligne', 'colonne', 'grille'].includes(genre)) {
|
|
31
|
+
s.erreur('« en cascade » fait apparaître les enfants d’un bloc l’un après l’autre : il s’écrit sur un bloc (section, bloc, ligne, colonne ou grille).', ligne);
|
|
32
|
+
}
|
|
30
33
|
if (genre === 'champ' || genre === 'formulaire' || dansFormulaire) {
|
|
31
34
|
s.erreur('pas d’animation sur un champ, ni dans un formulaire : un champ qui bouge pendant qu’on écrit gêne la saisie.', ligne);
|
|
32
35
|
}
|
|
@@ -191,7 +194,7 @@ function verifierReglagesGraphique(g, s) {
|
|
|
191
194
|
s.erreur(couleur.erreur, ligne);
|
|
192
195
|
continue;
|
|
193
196
|
}
|
|
194
|
-
const mode =
|
|
197
|
+
const mode = s.modes.find((m) => contraste(couleur[m], FOND_PAGE[m]) < 3);
|
|
195
198
|
if (mode) {
|
|
196
199
|
const rapport = contraste(couleur[mode], FOND_PAGE[mode]).toFixed(1).replace('.', ',');
|
|
197
200
|
s.avertissement(`la couleur « ${couleur.nom} » se voit mal sur le fond de la page en mode ${mode} (contraste ${rapport} pour 1, il en faut au moins 3) : choisissez une couleur plus ${mode === 'clair' ? 'foncée' : 'claire'}.`, ligne);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { simplifier } from './elements.js';
|
|
2
2
|
import { proposerNom } from './suggestions.js';
|
|
3
|
+
import { pascal } from '../cibles/web/noms.js';
|
|
3
4
|
const SCHEMAS_PERMIS = ['http', 'https', 'mailto', 'tel'];
|
|
4
5
|
/** Vrai si l'adresse est un schéma permis pour un lien externe (http, https, mailto, tel). */
|
|
5
6
|
export function estAdresseExterne(valeur) {
|
|
@@ -88,6 +89,16 @@ export function analyserNavigation(programme, diagnostics) {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
}
|
|
92
|
+
// Une page devient un composant React nommé comme elle (« bac_a_sable » → BacASable) :
|
|
93
|
+
// un composant du même nom serait caché par la page qui l'affiche.
|
|
94
|
+
const composants = new Map([...programme.composants, ...programme.composantsReact].flatMap((c) => [[c.nom, c.nom], [pascal(c.nom), c.nom]]));
|
|
95
|
+
for (const page of programme.pages) {
|
|
96
|
+
const produit = pascal(page.nom);
|
|
97
|
+
const composant = composants.get(produit);
|
|
98
|
+
if (composant !== undefined) {
|
|
99
|
+
erreur(`la page « ${page.nom} » et le composant « ${composant} » auraient le même nom dans le code produit (${produit}) : renommez l'un des deux (par exemple le composant « ${composant}Bloc »).`, page.ligne);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
91
102
|
analyserSousPages(programme, erreur);
|
|
92
103
|
analyserAncres(programme, diagnostics, erreur);
|
|
93
104
|
}
|
|
@@ -228,4 +239,28 @@ function analyserAncres(programme, diagnostics, erreur) {
|
|
|
228
239
|
verifier(page, i.cible, i.ligne);
|
|
229
240
|
});
|
|
230
241
|
}
|
|
242
|
+
// L'en-tête et le pied sont sur chaque page : « carte#desserts » se vérifie comme ailleurs ;
|
|
243
|
+
// « #desserts » vise la page affichée, qui change : il faut au moins une page qui l'ait.
|
|
244
|
+
for (const disposition of [programme.entete, programme.pied]) {
|
|
245
|
+
if (!disposition)
|
|
246
|
+
continue;
|
|
247
|
+
const de = disposition.genre === 'entete' ? "de l'en-tête" : 'du pied de page';
|
|
248
|
+
parcourir(disposition.corps, (i) => {
|
|
249
|
+
if (i.type !== 'Element' || i.cible === undefined)
|
|
250
|
+
return;
|
|
251
|
+
const lue = lireCible(i.cible);
|
|
252
|
+
if (lue.ancre === undefined)
|
|
253
|
+
return;
|
|
254
|
+
if (lue.page !== undefined && lue.page !== '') {
|
|
255
|
+
const visee = pages.get(lue.page);
|
|
256
|
+
if (visee)
|
|
257
|
+
verifier(visee, i.cible, i.ligne);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const montrees = programme.pages.filter((p) => !(disposition.genre === 'entete' ? p.sansEntete : p.sansPied) && p.parametres.length === 0);
|
|
261
|
+
if (!montrees.some((p) => ancres.get(p.nom)?.includes(lue.ancre))) {
|
|
262
|
+
erreur(`l'ancre « ${lue.ancre} » ${de} n'existe dans aucune page : écrivez avec ancre "${lue.ancre}" sur l'élément ou le bloc où le lien doit mener, ou écrivez le nom de la page (par exemple "accueil#${lue.ancre}").`, i.ligne);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
231
266
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export function niveauxDesSousTitres(instructions, parent = 1) {
|
|
2
|
+
let dernier = parent;
|
|
3
|
+
const parcourir = (enfants) => {
|
|
4
|
+
for (const instruction of enfants) {
|
|
5
|
+
if (instruction.type === 'Element' && instruction.element === 'titre')
|
|
6
|
+
dernier = 1;
|
|
7
|
+
else if (instruction.type === 'Element' && instruction.element === 'soustitre') {
|
|
8
|
+
if (instruction.niveau === undefined) {
|
|
9
|
+
const niveau = Math.min(parent + 1, 6);
|
|
10
|
+
if (niveau !== 2)
|
|
11
|
+
instruction.niveau = niveau;
|
|
12
|
+
dernier = niveau;
|
|
13
|
+
}
|
|
14
|
+
else
|
|
15
|
+
dernier = instruction.niveau;
|
|
16
|
+
}
|
|
17
|
+
else if (instruction.type === 'Bloc' && instruction.genre === 'section')
|
|
18
|
+
niveauxDeLaSection(instruction.corps, dernier);
|
|
19
|
+
else if (instruction.type === 'Bloc')
|
|
20
|
+
niveauxDesSousTitres(instruction.corps, dernier);
|
|
21
|
+
else {
|
|
22
|
+
// Les branches d'une condition, le corps d'une boucle ou d'un formulaire : le même niveau.
|
|
23
|
+
for (const cle of ['corps', 'alors', 'sinon']) {
|
|
24
|
+
const suite = instruction[cle];
|
|
25
|
+
if (Array.isArray(suite) && instruction.type !== 'Evenement')
|
|
26
|
+
parcourir(suite);
|
|
27
|
+
}
|
|
28
|
+
if (instruction.type === 'Selon')
|
|
29
|
+
for (const cas of instruction.cas)
|
|
30
|
+
parcourir(cas.corps);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
parcourir(instructions);
|
|
35
|
+
}
|
|
36
|
+
/** Les sous-titres d'un arbre d'instructions, dans l'ordre de lecture. */
|
|
37
|
+
function sousTitres(instructions, vus = []) {
|
|
38
|
+
for (const instruction of instructions) {
|
|
39
|
+
if (instruction.type === 'Element' && instruction.element === 'soustitre')
|
|
40
|
+
vus.push(instruction);
|
|
41
|
+
if (instruction.type === 'Evenement')
|
|
42
|
+
continue;
|
|
43
|
+
for (const cle of ['corps', 'alors', 'sinon']) {
|
|
44
|
+
const suite = instruction[cle];
|
|
45
|
+
if (Array.isArray(suite))
|
|
46
|
+
sousTitres(suite, vus);
|
|
47
|
+
}
|
|
48
|
+
if (instruction.type === 'Selon')
|
|
49
|
+
for (const cas of instruction.cas)
|
|
50
|
+
sousTitres(cas.corps, vus);
|
|
51
|
+
}
|
|
52
|
+
return vus;
|
|
53
|
+
}
|
|
54
|
+
/** Une « section » : son premier sous-titre la titre, les autres descendent d'un niveau au moins. */
|
|
55
|
+
function niveauxDeLaSection(corps, parent) {
|
|
56
|
+
const tous = sousTitres(corps);
|
|
57
|
+
const automatiques = new Set(tous.filter((t) => t.niveau === undefined));
|
|
58
|
+
niveauxDesSousTitres(corps, parent);
|
|
59
|
+
const [premier, ...autres] = tous;
|
|
60
|
+
if (!premier)
|
|
61
|
+
return;
|
|
62
|
+
const sousLaSection = Math.min((premier.niveau ?? 2) + 1, 6);
|
|
63
|
+
for (const titre of autres) {
|
|
64
|
+
if (automatiques.has(titre) && (titre.niveau ?? 2) < sousLaSection)
|
|
65
|
+
titre.niveau = sousLaSection;
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/analyse/noms.js
CHANGED
|
@@ -12,7 +12,7 @@ export function avertirNomsDuLangage(programme) {
|
|
|
12
12
|
const diagnostics = [];
|
|
13
13
|
const avertir = (nom, ligne) => {
|
|
14
14
|
const canonique = formeCanonique(nom);
|
|
15
|
-
if (canonique && MOTS_CONTEXTUELS.has(canonique)) {
|
|
15
|
+
if (canonique && canonique !== 'liste' && MOTS_CONTEXTUELS.has(canonique)) {
|
|
16
16
|
diagnostics.push({
|
|
17
17
|
gravite: 'avertissement',
|
|
18
18
|
message: `la variable « ${nom} » porte le nom d'un mot du langage ; c'est permis, mais cela peut prêter à confusion.`,
|
package/dist/analyse/site.js
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
|
+
import { langueDuMot } from '../i18n/langues.js';
|
|
1
2
|
/** Le texte d'un littéral sans « {…} », sinon `undefined`. */
|
|
2
3
|
export function texteLitteral(expression) {
|
|
3
4
|
if (expression?.type !== 'Chaine' || !expression.parties.every((p) => p.type === 'texte'))
|
|
4
5
|
return undefined;
|
|
5
6
|
return expression.parties.map((p) => p.valeur).join('');
|
|
6
7
|
}
|
|
7
|
-
/**
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* « si langue est anglais » : vrai si le test est vrai dans la langue `code`,
|
|
10
|
+
* faux sinon ; `undefined` pour un test qui ne porte pas sur la langue.
|
|
11
|
+
*/
|
|
12
|
+
function testDeLangue(test, code) {
|
|
13
|
+
if (test.type !== 'Comparaison' || test.gauche.type !== 'Reference' || test.gauche.nom !== 'langue' || test.droite?.type !== 'Reference')
|
|
14
|
+
return undefined;
|
|
15
|
+
if (test.operateur !== 'egal' && test.operateur !== 'different')
|
|
16
|
+
return undefined;
|
|
17
|
+
const langue = langueDuMot(test.droite.nom);
|
|
18
|
+
if (!langue)
|
|
19
|
+
return undefined;
|
|
20
|
+
return (langue.code === code) !== (test.operateur === 'different') !== Boolean(test.negation);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Le premier « titre » ou « texte » écrit dans la page, même dans un bloc de mise en page.
|
|
24
|
+
* Avec des langues, une branche « si langue est … » compte : celle de la langue de base.
|
|
25
|
+
*/
|
|
26
|
+
function premier(genre, instructions, langueDeBase) {
|
|
9
27
|
for (const instruction of instructions) {
|
|
10
28
|
if (instruction.type === 'Element' && instruction.element === genre)
|
|
11
29
|
return instruction.contenu;
|
|
12
|
-
|
|
13
|
-
|
|
30
|
+
const vraie = instruction.type === 'Condition' && langueDeBase !== undefined ? testDeLangue(instruction.test, langueDeBase) : undefined;
|
|
31
|
+
const corps = instruction.type === 'Bloc' ? instruction.corps : instruction.type === 'Condition' && vraie !== undefined ? (vraie ? instruction.alors : instruction.sinon) : undefined;
|
|
32
|
+
if (corps) {
|
|
33
|
+
const trouve = premier(genre, corps, langueDeBase);
|
|
14
34
|
if (trouve)
|
|
15
35
|
return trouve;
|
|
16
36
|
}
|
|
@@ -18,10 +38,10 @@ function premier(genre, instructions) {
|
|
|
18
38
|
return undefined;
|
|
19
39
|
}
|
|
20
40
|
/** Le titre et la description propres à la page : son aperçu, sinon son premier titre et son premier texte. */
|
|
21
|
-
export function apercuDeLaPage(page) {
|
|
41
|
+
export function apercuDeLaPage(page, langueDeBase) {
|
|
22
42
|
return {
|
|
23
|
-
titre: page.apercu?.titre ?? premier('titre', page.corps),
|
|
24
|
-
description: page.apercu?.description ?? premier('texte', page.corps),
|
|
43
|
+
titre: page.apercu?.titre ?? premier('titre', page.corps, langueDeBase),
|
|
44
|
+
description: page.apercu?.description ?? premier('texte', page.corps, langueDeBase),
|
|
25
45
|
};
|
|
26
46
|
}
|
|
27
47
|
export function analyserPublication(programme, diagnostics) {
|
|
@@ -43,10 +63,12 @@ export function analyserPublication(programme, diagnostics) {
|
|
|
43
63
|
avertir(`l'image de partage « ${site.image} » est sur un autre site : si elle disparaît, l'aperçu des liens du site la perd. Préférez un fichier du projet.`, site.lignes.image ?? site.ligne);
|
|
44
64
|
}
|
|
45
65
|
const titres = new Map();
|
|
66
|
+
const premiereLangue = programme.langues?.langues[0];
|
|
67
|
+
const langueDeBase = premiereLangue ? langueDuMot(premiereLangue.mot)?.code : undefined;
|
|
46
68
|
for (const page of programme.pages) {
|
|
47
69
|
if (page.nom === 'introuvable')
|
|
48
70
|
continue;
|
|
49
|
-
const { titre, description } = apercuDeLaPage(page);
|
|
71
|
+
const { titre, description } = apercuDeLaPage(page, langueDeBase);
|
|
50
72
|
const ligneApercu = page.apercu?.ligne ?? page.ligne;
|
|
51
73
|
const titreLu = texteLitteral(titre);
|
|
52
74
|
const descriptionLue = texteLitteral(description);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { analyser } from '../../lexer/lexer.js';
|
|
2
|
+
const echapper = (texte) => texte.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
3
|
+
/** Colore un exemple Humalis valide ; un exemple incomplet reste lisible sans couleur. */
|
|
4
|
+
export function colorerCodeHumalis(code, langage) {
|
|
5
|
+
if (langage !== 'humalis')
|
|
6
|
+
return echapper(code);
|
|
7
|
+
try {
|
|
8
|
+
const tokens = analyser(code);
|
|
9
|
+
const lignes = code.split('\n');
|
|
10
|
+
const debuts = [];
|
|
11
|
+
let offset = 0;
|
|
12
|
+
for (const ligne of lignes) {
|
|
13
|
+
debuts.push(offset);
|
|
14
|
+
offset += ligne.length + 1;
|
|
15
|
+
}
|
|
16
|
+
const plages = [];
|
|
17
|
+
for (const token of tokens) {
|
|
18
|
+
if (!['MOT_CLE', 'CHAINE', 'NOMBRE'].includes(token.type))
|
|
19
|
+
continue;
|
|
20
|
+
const debut = debuts[token.ligne - 1] + token.colonne - 1;
|
|
21
|
+
let fin = debut + (token.original ?? token.valeur).length;
|
|
22
|
+
if (token.type === 'CHAINE') {
|
|
23
|
+
fin = debut + 1;
|
|
24
|
+
while (fin < code.length) {
|
|
25
|
+
if (code[fin] === '"' && code[fin - 1] !== '\\') {
|
|
26
|
+
fin++;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
fin++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
plages.push({ debut, fin, classe: token.type === 'MOT_CLE' ? 'mot' : token.type === 'CHAINE' ? 'texte' : 'nombre' });
|
|
33
|
+
}
|
|
34
|
+
for (let index = 0; index < lignes.length; index++) {
|
|
35
|
+
const ligne = lignes[index];
|
|
36
|
+
for (let colonne = 0; colonne < ligne.length; colonne++) {
|
|
37
|
+
const debut = debuts[index] + colonne;
|
|
38
|
+
if (ligne[colonne] === '#' && !plages.some((p) => p.debut <= debut && debut < p.fin)) {
|
|
39
|
+
plages.push({ debut, fin: debuts[index] + ligne.length, classe: 'commentaire' });
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
plages.sort((a, b) => a.debut - b.debut);
|
|
45
|
+
let rendu = '';
|
|
46
|
+
let position = 0;
|
|
47
|
+
for (const plage of plages) {
|
|
48
|
+
if (plage.debut < position)
|
|
49
|
+
continue;
|
|
50
|
+
rendu += echapper(code.slice(position, plage.debut));
|
|
51
|
+
rendu += `<span class="humalis-code-${plage.classe}">${echapper(code.slice(plage.debut, plage.fin))}</span>`;
|
|
52
|
+
position = plage.fin;
|
|
53
|
+
}
|
|
54
|
+
return rendu + echapper(code.slice(position));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return echapper(code);
|
|
58
|
+
}
|
|
59
|
+
}
|