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.
@@ -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
- if (titres === 0)
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 ['clair', 'sombre']) {
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;
@@ -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 ['clair', 'sombre']) {
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;
@@ -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
- /** Les textes que la personne voit : contenus d'éléments, titres de blocs, messages, aperçu. */
59
- function textesVisibles(instructions, vus) {
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
- textesVisibles(enfants, vus);
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) {
@@ -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 = ['clair', 'sombre'].find((m) => contraste(couleur[m], FOND_PAGE[m]) < 3);
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
+ }
@@ -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.`,
@@ -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
- /** Le premier « titre » ou « texte » écrit dans la page, même dans un bloc de mise en page. */
8
- function premier(genre, instructions) {
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
- if (instruction.type === 'Bloc') {
13
- const trouve = premier(genre, instruction.corps);
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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
+ }