humalis 1.0.3 → 1.1.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.
Files changed (47) hide show
  1. package/LICENSE +85 -201
  2. package/LICENSE.en.md +86 -0
  3. package/NOTICE +6 -3
  4. package/README.en.md +1 -1
  5. package/README.md +1 -1
  6. package/dist/analyse/accessibilite.js +13 -5
  7. package/dist/analyse/analyse.js +43 -3
  8. package/dist/analyse/ecran.js +1 -1
  9. package/dist/analyse/langues.js +30 -10
  10. package/dist/analyse/medias.js +4 -1
  11. package/dist/analyse/navigation.js +35 -0
  12. package/dist/analyse/niveaux.js +33 -0
  13. package/dist/analyse/noms.js +1 -1
  14. package/dist/analyse/site.js +30 -8
  15. package/dist/cibles/commun/colorerCode.js +59 -0
  16. package/dist/cibles/commun/mouvement.js +110 -4
  17. package/dist/cibles/mobile/projet.js +7 -2
  18. package/dist/cibles/mobile/rendu.js +6 -3
  19. package/dist/cibles/statique/elements-js.js +1 -1
  20. package/dist/cibles/statique/page.js +99 -6
  21. package/dist/cibles/statique/projet.js +31 -0
  22. package/dist/cibles/statique/valeurs.js +5 -1
  23. package/dist/cibles/statique/widgets.js +1 -1
  24. package/dist/cibles/web/css-ecran.js +22 -1
  25. package/dist/cibles/web/ecran.js +161 -16
  26. package/dist/cibles/web/interactions.js +50 -3
  27. package/dist/cibles/web/projet.js +29 -2
  28. package/dist/cibles/web/theme.js +11 -2
  29. package/dist/cibles/web/valeurs.js +17 -0
  30. package/dist/cibles/web/widgets.js +1 -1
  31. package/dist/i18n/messages.js +2 -0
  32. package/dist/lexer/lexer.js +106 -3
  33. package/dist/navigateur.js +5274 -0
  34. package/dist/parser/interactions.js +6 -2
  35. package/dist/parser/langues.js +1 -1
  36. package/dist/parser/medias.js +6 -1
  37. package/dist/parser/miseEnPage.js +1 -1
  38. package/dist/parser/parser.js +85 -3
  39. package/dist/parser/theme.js +10 -1
  40. package/dist/ri/construire.js +24 -1
  41. package/dist/vocabulaire/table-ecran.js +3 -3
  42. package/dist/vocabulaire/table-interactions.js +16 -1
  43. package/dist/vocabulaire/table.js +4 -4
  44. package/docs/guide.en.md +170 -2
  45. package/docs/guide.md +171 -1
  46. package/llms.txt +14 -1
  47. package/package.json +7 -4
@@ -1,5 +1,6 @@
1
1
  import { normaliser } from '../lexer/motsCles.js';
2
2
  import { ErreurSyntaxique } from './erreurs.js';
3
+ import { lireVersions } from './langues.js';
3
4
  /** Vrai si le token est ce mot, écrit comme mot-clé ou comme nom (sans accent ni majuscule). */
4
5
  function estLeMot(token, mot) {
5
6
  return (token.type === 'MOT_CLE' || token.type === 'IDENTIFIANT') && normaliser(token.original ?? token.valeur) === mot;
@@ -122,8 +123,11 @@ function analyserCopier(flux, outils) {
122
123
  if (flux.verifier('MOT_CLE', 'avec') && estLeMot(flux.regarder(1), 'message')) {
123
124
  flux.avancer();
124
125
  flux.avancer();
125
- const message = flux.attendre('CHAINE', undefined, 'le texte du message entre guillemets (copier code avec message "Code copié")').valeur;
126
- return { type: 'Copier', valeur, message, ligne: debut.ligne };
126
+ const jeton = flux.attendre('CHAINE', undefined, 'le texte du message entre guillemets (copier code avec message "Code copié")');
127
+ // « avec message "Copié" en anglais "Copied" » : les versions écrites à côté.
128
+ const versions = lireVersions(flux);
129
+ const texte = versions ? { texte: { type: 'Chaine', parties: jeton.parties ?? [], versions, ligne: jeton.ligne } } : {};
130
+ return { type: 'Copier', valeur, message: jeton.valeur, ...texte, ligne: debut.ligne };
127
131
  }
128
132
  if (estLeMot(flux.actuel(), 'sans') && estLeMot(flux.regarder(1), 'message')) {
129
133
  flux.avancer();
@@ -85,5 +85,5 @@ export function estSelecteurLangue(flux) {
85
85
  }
86
86
  /** Le texte écrit, interpolations comprises (« {n article} dans le panier ») : la clé d'une traduction. */
87
87
  export function texteSource(parties) {
88
- return parties.map((p) => (p.type === 'texte' ? p.valeur : `{${p.valeur}}`)).join('');
88
+ return parties.map((p) => (p.type === 'variable' ? `{${p.valeur}}` : p.valeur)).join('');
89
89
  }
@@ -16,7 +16,12 @@ export function analyserOptionMedia(flux, nom) {
16
16
  const valeur = flux.avancer().valeur;
17
17
  if (estLeMot(flux.actuel(), 'defilement')) {
18
18
  flux.avancer();
19
- return { propriete: 'animation', valeur, defilement: true };
19
+ // « … au defilement et en cascade » : les enfants du bloc, l'un après l'autre.
20
+ const enCascade = flux.actuel().valeur === 'et' && estLeMot(flux.regarder(1), 'en') && estLeMot(flux.regarder(2), 'cascade');
21
+ if (enCascade)
22
+ for (let i = 0; i < 3; i++)
23
+ flux.avancer();
24
+ return { propriete: 'animation', valeur, defilement: true, ...(enCascade ? { cascade: true } : {}) };
20
25
  }
21
26
  return { propriete: 'animation', valeur };
22
27
  }
@@ -30,7 +30,7 @@ export function analyserStylePropre(flux, propriete) {
30
30
  return { propriete: nom, valeur: '5' };
31
31
  // « photo "{p.photo}" » : un texte, avec ses {variables}.
32
32
  if (nom === 'photo') {
33
- const jeton = flux.attendre('CHAINE', undefined, 'l’image entre guillemets : avec photo "emch.jpg"');
33
+ const jeton = flux.attendre('CHAINE', undefined, 'l’image entre guillemets : avec photo "camille.jpg"');
34
34
  return { propriete: nom, valeur: jeton.valeur, parties: jeton.parties ?? [] };
35
35
  }
36
36
  const media = analyserOptionMedia(flux, nom);
@@ -398,6 +398,56 @@ function analyserBloc(flux) {
398
398
  return instructions;
399
399
  }
400
400
  function analyserInstruction(flux) {
401
+ if (estLeMot(flux.actuel(), 'tableau') && (flux.regarder(1).type === 'CHAINE' || flux.regarder(1).valeur === ':')) {
402
+ const debut = flux.avancer();
403
+ const legende = flux.verifier('PONCTUATION', ':') ? undefined : analyserValeur(flux);
404
+ flux.attendre('PONCTUATION', ':', 'deux-points après la légende du tableau');
405
+ flux.sauterFinsDeLigne();
406
+ const colonnes = [];
407
+ const rangees = [];
408
+ if (flux.accepter('INDENT')) {
409
+ flux.sauterFinsDeLigne();
410
+ while (!flux.verifier('DEDENT') && !flux.estFini()) {
411
+ const genre = flux.avancer();
412
+ if (!estLeMot(genre, 'colonnes') && !estLeMot(genre, 'rangee'))
413
+ throw new ErreurSyntaxique('un tableau contient « colonnes » et « rangee ».', genre.ligne);
414
+ const cellules = [analyserValeur(flux)];
415
+ while (flux.accepter('PONCTUATION', ','))
416
+ cellules.push(analyserValeur(flux));
417
+ if (estLeMot(genre, 'colonnes'))
418
+ colonnes.push(...cellules);
419
+ else
420
+ rangees.push(cellules);
421
+ flux.sauterFinsDeLigne();
422
+ }
423
+ flux.accepter('DEDENT');
424
+ }
425
+ return { type: 'TableauDocumentation', ...(legende ? { legende } : {}), colonnes, rangees, ligne: debut.ligne };
426
+ }
427
+ if (estLeMot(flux.actuel(), 'liste') && (flux.regarder(1).valeur === ':' || (estLeMot(flux.regarder(1), 'numerotee') && flux.regarder(2).valeur === ':'))) {
428
+ const debut = flux.avancer();
429
+ const numerotee = estLeMot(flux.actuel(), 'numerotee');
430
+ if (numerotee)
431
+ flux.avancer();
432
+ flux.attendre('PONCTUATION', ':', 'deux-points après « liste »');
433
+ return { type: 'Bloc', genre: numerotee ? 'listeNumerotee' : 'liste', styles: [], classes: [], corps: analyserBloc(flux), ligne: debut.ligne };
434
+ }
435
+ if (estLeMot(flux.actuel(), 'code') && estBlocDeCode(flux)) {
436
+ const debut = flux.avancer();
437
+ const langage = flux.verifier('PONCTUATION', ':') || estLeMot(flux.actuel(), 'avec') ? undefined : flux.avancer().valeur;
438
+ // « avec animation frappe » (au chargement), « … au defilement » (quand le bloc arrive à l'écran).
439
+ let frappe;
440
+ if (estLeMot(flux.actuel(), 'avec')) {
441
+ flux.avancer(); // avec
442
+ flux.avancer(); // animation
443
+ flux.avancer(); // frappe
444
+ frappe = estLeMot(flux.actuel(), 'au') ? (flux.avancer(), flux.avancer(), 'defilement') : 'chargement';
445
+ }
446
+ flux.attendre('PONCTUATION', ':', 'deux-points après « code »');
447
+ flux.accepter('FIN_LIGNE');
448
+ const code = flux.attendre('CODE_BRUT', undefined, 'les lignes du bloc de code').valeur;
449
+ return { type: 'CodeDocumentation', ...(langage ? { langage } : {}), ...(frappe ? { frappe } : {}), code, ligne: debut.ligne };
450
+ }
401
451
  if (estLeMot(flux.actuel(), 'en') && estLeMot(flux.regarder(1), 'supposant')) {
402
452
  const debut = flux.avancer();
403
453
  for (const motAttendu of ['supposant', 'que', 'ca', 'marche']) {
@@ -726,6 +776,7 @@ function estBlocMiseEnPage(flux) {
726
776
  const suivant = flux.regarder(1);
727
777
  return ((suivant.type === 'PONCTUATION' && suivant.valeur === ':') ||
728
778
  (suivant.type === 'MOT_CLE' && suivant.valeur === 'avec') ||
779
+ (genre === 'menu' && estLeMot(suivant, 'repliable')) ||
729
780
  ((genre === 'section' || GENRES_AVEC_TITRE.includes(genre)) && suivant.type === 'CHAINE'));
730
781
  }
731
782
  function analyserBlocMiseEnPage(flux) {
@@ -734,6 +785,10 @@ function analyserBlocMiseEnPage(flux) {
734
785
  if (filAriane)
735
786
  flux.avancer();
736
787
  const genre = filAriane ? 'filAriane' : GENRES_BLOC.find((g) => estLeMot(debut, g));
788
+ // « menu repliable : » : sur un petit écran, un bouton « Menu » ouvre et ferme les liens.
789
+ const repliable = genre === 'menu' && estLeMot(flux.actuel(), 'repliable');
790
+ if (repliable)
791
+ flux.avancer();
737
792
  // Le titre d'une section devient son premier élément : un « titre » comme les autres.
738
793
  const titre = [];
739
794
  let titreBloc;
@@ -769,7 +824,7 @@ function analyserBlocMiseEnPage(flux) {
769
824
  } while (flux.accepter('MOT_CLE', 'et'));
770
825
  }
771
826
  flux.attendre('PONCTUATION', ':', `deux-points à la fin de la ligne « ${genre} », puis son contenu en dessous, décalé`);
772
- return { type: 'Bloc', genre, styles, classes, ...(titreBloc ? { titre: titreBloc } : {}), ...(ancre !== undefined ? { ancre } : {}), ...(nom !== undefined ? { nom } : {}), corps: [...titre, ...analyserBloc(flux)], ligne: debut.ligne };
827
+ return { type: 'Bloc', genre, styles, classes, ...(titreBloc ? { titre: titreBloc } : {}), ...(ancre !== undefined ? { ancre } : {}), ...(nom !== undefined ? { nom } : {}), ...(repliable ? { repliable } : {}), corps: [...titre, ...analyserBloc(flux)], ligne: debut.ligne };
773
828
  }
774
829
  function lireNombre(flux, description) {
775
830
  const negatif = flux.accepter('PONCTUATION', '-');
@@ -936,7 +991,7 @@ function analyserAction(flux) {
936
991
  // « ajouter nom à panier » : « à » et « de » sont des mots de liaison
937
992
  // déjà écartés par le lexer ; « dans » reste permis.
938
993
  if (debut.valeur === 'ajouter' || debut.valeur === 'retirer') {
939
- const valeur = analyserExpression(flux);
994
+ const valeur = analyserOperande(flux);
940
995
  flux.accepter('MOT_CLE', 'dans');
941
996
  const { nom } = flux.attendreNom('le nom de la liste');
942
997
  return {
@@ -1018,7 +1073,7 @@ function analyserAction(flux) {
1018
1073
  type: 'Affectation',
1019
1074
  nom,
1020
1075
  ...(champs.length > 0 ? { champs } : {}),
1021
- valeur: analyserExpression(flux),
1076
+ valeur: analyserOperande(flux),
1022
1077
  ligne: debut.ligne,
1023
1078
  };
1024
1079
  }
@@ -1154,6 +1209,17 @@ function analyserComparaison(flux) {
1154
1209
  flux.avancer();
1155
1210
  return { type: 'Comparaison', gauche: { type: 'MouvementReduit', ligne: debut.ligne }, operateur: pas ? 'faux' : 'vrai', ligne: debut.ligne };
1156
1211
  }
1212
+ // « theme est sombre », « theme est clair » (et « theme n'est pas sombre ») : le thème affiché.
1213
+ if (estLeMot(flux.actuel(), 'theme') &&
1214
+ flux.regarder(1).type === 'MOT_CLE' &&
1215
+ flux.regarder(1).valeur === 'est' &&
1216
+ (estLeMot(flux.regarder(2 + pas), 'sombre') || estLeMot(flux.regarder(2 + pas), 'clair'))) {
1217
+ const debut = flux.avancer();
1218
+ for (let i = 0; i < 1 + pas; i++)
1219
+ flux.avancer();
1220
+ const sombre = estLeMot(flux.avancer(), 'sombre');
1221
+ return { type: 'Comparaison', gauche: { type: 'ThemeSombre', ligne: debut.ligne }, operateur: sombre !== Boolean(pas) ? 'vrai' : 'faux', ligne: debut.ligne };
1222
+ }
1157
1223
  const gauche = analyserOperande(flux);
1158
1224
  // « panier contient "Livre" », « nom contient "Liv" ».
1159
1225
  if (estLeMot(flux.actuel(), 'contient')) {
@@ -1285,6 +1351,14 @@ function analyserApresEst(flux, gauche) {
1285
1351
  return { type: 'Comparaison', gauche, operateur, droite, ligne: gauche.ligne };
1286
1352
  }
1287
1353
  /** Variable ou champ de formulaire (« champ "email" »). */
1354
+ /** « code … : » est un bloc de code quand le lexer a recopié ses lignes (CODE_BRUT) juste après les deux-points. */
1355
+ function estBlocDeCode(flux) {
1356
+ for (let decalage = 1; decalage <= 7; decalage++) {
1357
+ if (flux.regarder(decalage).valeur === ':')
1358
+ return flux.regarder(decalage + 2).type === 'CODE_BRUT' || flux.regarder(decalage + 1).type === 'CODE_BRUT';
1359
+ }
1360
+ return false;
1361
+ }
1288
1362
  export function analyserOperande(flux) {
1289
1363
  if (flux.verifier('MOT_CLE', 'champ') && flux.regarder(1).type === 'CHAINE') {
1290
1364
  const debut = flux.avancer();
@@ -1531,6 +1605,14 @@ function analyserValeur(flux) {
1531
1605
  flux.avancer();
1532
1606
  // « "Bienvenue" en anglais "Welcome" » : les versions écrites à côté.
1533
1607
  const versions = lireVersions(flux);
1608
+ // « "GitHub" sans traduction » : le même texte dans toutes les langues (un nom propre).
1609
+ if (estLeMot(flux.actuel(), 'sans') && estLeMot(flux.regarder(1), 'traduction')) {
1610
+ const sans = flux.avancer();
1611
+ flux.avancer();
1612
+ if (versions)
1613
+ throw new ErreurSyntaxique('ce texte a une version traduite et « sans traduction » : gardez l’un ou l’autre.', sans.ligne);
1614
+ return { type: 'Chaine', parties: token.parties ?? [], sansTraduction: true, ligne: token.ligne };
1615
+ }
1534
1616
  return { type: 'Chaine', parties: token.parties ?? [], ...(versions ? { versions } : {}), ligne: token.ligne };
1535
1617
  }
1536
1618
  if (token.type === 'NOMBRE') {
@@ -107,14 +107,23 @@ export function analyserTheme(flux) {
107
107
  flux.accepter('DEDENT');
108
108
  return theme;
109
109
  }
110
- /** « changer theme par sombre » (ou par "sombre") : le choix du visiteur, gardé par son appareil. */
110
+ /** « basculer theme » : le thème inverse de celui qu'on voit. */
111
+ const estBasculerTheme = (flux) => estLeMot(flux.actuel(), 'basculer') && estLeMot(flux.regarder(1), 'theme');
112
+ /** « changer theme par sombre » (ou par "sombre") : le choix du visiteur, gardé par son appareil ; ou « basculer theme ». */
111
113
  export function estChangerTheme(flux) {
114
+ if (estBasculerTheme(flux))
115
+ return true;
112
116
  if (!(flux.verifier('MOT_CLE', 'changer') && estLeMot(flux.regarder(1), 'theme') && flux.regarder(2).valeur === 'par'))
113
117
  return false;
114
118
  const valeur = flux.regarder(3);
115
119
  return valeur.type === 'CHAINE' || (estMot(valeur) && MODES.includes(normaliser(valeur.valeur)));
116
120
  }
117
121
  export function analyserChangerTheme(flux) {
122
+ if (estBasculerTheme(flux)) {
123
+ const debut = flux.avancer();
124
+ const mot = flux.avancer();
125
+ return { type: 'Action', action: 'changerTheme', arguments: [{ type: 'Chaine', parties: [{ type: 'texte', valeur: 'bascule' }], ligne: mot.ligne }], ligne: debut.ligne };
126
+ }
118
127
  const debut = flux.avancer();
119
128
  flux.avancer();
120
129
  flux.avancer();
@@ -330,6 +330,12 @@ class Construction {
330
330
  case 'Bloc':
331
331
  noeuds.push(instruction.genre === 'carte' ? this.carte(instruction, gestionnaires) : this.bloc(instruction, gestionnaires));
332
332
  break;
333
+ case 'CodeDocumentation':
334
+ noeuds.push({ type: 'codeDocumentation', ...(instruction.langage ? { langage: instruction.langage } : {}), ...(instruction.frappe ? { frappe: instruction.frappe } : {}), code: instruction.code, ligne: instruction.ligne });
335
+ break;
336
+ case 'TableauDocumentation':
337
+ noeuds.push({ type: 'tableauDocumentation', legende: this.valeur(instruction.legende), colonnes: instruction.colonnes.map((c) => this.valeur(c)), rangees: instruction.rangees.map((r) => r.map((c) => this.valeur(c))), ligne: instruction.ligne });
338
+ break;
333
339
  case 'Formulaire':
334
340
  noeuds.push({
335
341
  type: 'formulaire',
@@ -578,7 +584,12 @@ class Construction {
578
584
  });
579
585
  break;
580
586
  case 'Copier':
581
- actions.push({ type: 'copie', valeur: this.valeur(instruction.valeur), message: instruction.message === undefined ? 'Copié' : instruction.message });
587
+ actions.push({
588
+ type: 'copie',
589
+ valeur: this.valeur(instruction.valeur),
590
+ message: instruction.message === undefined ? 'Copié' : instruction.message,
591
+ ...(instruction.texte ? { texte: this.valeur(instruction.texte) } : {}),
592
+ });
582
593
  break;
583
594
  case 'Partager': {
584
595
  const lien = instruction.lien;
@@ -648,6 +659,9 @@ class Construction {
648
659
  // « animation montee au defilement » : la marque que lit l'observateur du défilement.
649
660
  if (s.defilement)
650
661
  options.push({ nom: 'animation', valeur: 'defilement' });
662
+ // « … et en cascade » : les enfants du bloc s'animent, l'un après l'autre.
663
+ if (s.cascade)
664
+ options.push({ nom: 'animation', valeur: 'cascade' });
651
665
  }
652
666
  return { options, reglages: reglages };
653
667
  }
@@ -674,6 +688,9 @@ class Construction {
674
688
  /** Un bloc de mise en page, ou un bloc d'éléments d'écran avec son état caché. */
675
689
  bloc(instruction, gestionnaires, groupe) {
676
690
  const { options, reglages } = this.separer(instruction.styles);
691
+ // « menu repliable » : la marque que lisent le CSS et le bouton « Menu ».
692
+ if (instruction.repliable)
693
+ options.push({ nom: 'menu', valeur: 'repliable' });
677
694
  const noeud = {
678
695
  type: 'bloc',
679
696
  genre: instruction.genre,
@@ -795,6 +812,10 @@ function traduireValeur(expression, analyse) {
795
812
  const parties = (liste) => liste.map((p) => {
796
813
  if (p.type === 'texte')
797
814
  return { texte: p.valeur };
815
+ if (p.type === 'accent')
816
+ return { texte: p.valeur, accent: true };
817
+ if (p.type === 'code')
818
+ return { texte: p.valeur, code: true };
798
819
  const format = analyse.formats.get(p);
799
820
  const pluriel = analyse.pluriels.get(p);
800
821
  return {
@@ -892,6 +913,8 @@ function traduireValeur(expression, analyse) {
892
913
  return { type: 'erreurRequete', variable: expression.variable };
893
914
  case 'MouvementReduit':
894
915
  return { type: 'mouvementReduit' };
916
+ case 'ThemeSombre':
917
+ return { type: 'themeSombre' };
895
918
  }
896
919
  }
897
920
  /** Une durée écrite en chiffres, en millisecondes (l'analyse a refusé les autres là où il le faut). */
@@ -238,8 +238,8 @@ export const ENTREES_ECRAN = [
238
238
  }),
239
239
  elementEcran({
240
240
  mot: 'avatar',
241
- description: 'Le visage d’une personne : sa photo (« avec photo "emch.jpg" ») ou, sans photo, ses initiales sur un fond de couleur.',
242
- exemple: PAGE(' avatar "Emch Kongo"\n avatar "Le Fleuve" avec photo "logo.png" et taille grande'),
241
+ description: 'Le visage d’une personne : sa photo (« avec photo "camille.jpg" ») ou, sans photo, ses initiales sur un fond de couleur.',
242
+ exemple: PAGE(' avatar "Camille Martin"\n avatar "Le Fleuve" avec photo "logo.png" et taille grande'),
243
243
  web: '<span role="img" aria-label="…"> avec <img> ou les initiales',
244
244
  mobile: '<View> avec <Image> ou <Text>',
245
245
  statique: '<span class="humalis-avatar" role="img">',
@@ -366,7 +366,7 @@ export const ENTREES_ECRAN = [
366
366
  optionEcran('description', 'Le texte lu par un lecteur d’écran quand l’élément n’a pas de texte visible : « avec description "Panier" ».', PAGE(' icone "panier" avec description "Panier"'), ['icone', 'avatar', 'progression', 'etoiles', 'compteur', 'qr', 'chargement', 'badge', 'image', 'audio']),
367
367
  optionEcran('decoratif', 'Une image purement décorative, ignorée par le lecteur d’écran : « avec decoratif ».', PAGE(' titre "Accueil"\n image "filet.png" avec decoratif'), ['image', 'icone']),
368
368
  optionEcran('niveau', 'Le niveau d’un sous-titre, de 2 à 6 : « avec niveau 3 ».', PAGE(' titre "Accueil"\n soustitre "Détails" avec niveau 3'), ['soustitre']),
369
- optionEcran('photo', 'L’image d’un avatar : « avec photo "emch.jpg" », ou « avec photo "{p.photo}" » pour une image qui vient d’une donnée.', PAGE(' avatar "Emch Kongo" avec photo "emch.jpg"'), ['avatar']),
369
+ optionEcran('photo', 'L’image d’un avatar : « avec photo "camille.jpg" », ou « avec photo "{p.photo}" » pour une image qui vient d’une donnée.', PAGE(' avatar "Camille Martin" avec photo "camille.jpg"'), ['avatar']),
370
370
  optionEcran('modifiable', 'Des étoiles sur lesquelles on clique pour changer la variable : « etoiles note avec modifiable ».', PAGE(' variable note = 3\n etoiles note avec modifiable'), ['etoiles']),
371
371
  optionEcran('ouvert', 'Un onglet ou un deroulant ouvert au départ : « deroulant "Détails" avec ouvert : ».', PAGE(' deroulant "Détails" avec ouvert :\n texte "a"'), ['onglet', 'deroulant']),
372
372
  optionEcran('plusieurs', 'Un accordéon dont plusieurs deroulant peuvent être ouverts en même temps.', PAGE(' accordeon avec plusieurs :\n deroulant "A" :\n texte "a"\n deroulant "B" :\n texte "b"'), ['accordeon']),
@@ -109,7 +109,7 @@ export const ENTREES_INTERACTIONS = [
109
109
  concept: 'action.copier',
110
110
  cibles: ecran('await copier(texte, afficherMessage) : navigator.clipboard.writeText, repli execCommand (src/humalis/presse-papier.ts)', 'await copier(texte, afficherMessage) : Clipboard.setStringAsync d’expo-clipboard', "await copier(texte) : navigator.clipboard.writeText, repli execCommand (humalis.js)"),
111
111
  couvre: ['web:navigator.clipboard.writeText', 'expo:expo-clipboard'],
112
- description: 'Dans un événement : « copier code » met le texte dans le presse-papier, puis affiche « Copié » (ou « Copie impossible »). « avec message "Code copié" » change le texte, « sans message » le tait.',
112
+ description: 'Dans un événement : « copier code » met le texte dans le presse-papier, puis affiche « Copié » (ou « Copie impossible »). « avec message "Code copié" » change le texte (et « en anglais "Code copied" » le traduit), « sans message » le tait.',
113
113
  exemple: AU_CLIC(' copier code', ' variable code = "REY-2026"\n'),
114
114
  }),
115
115
  mot({
@@ -262,4 +262,19 @@ export const ENTREES_INTERACTIONS = [
262
262
  statut: 'supporte',
263
263
  etape: 9,
264
264
  },
265
+ {
266
+ mot: 'theme sombre',
267
+ accentue: 'thème sombre',
268
+ variantes: ['theme clair'],
269
+ categorie: 'comparaison',
270
+ concept: 'condition.themeSombre',
271
+ // Comme « mouvement est reduit » : une tournure lue dans une condition, jamais un mot-clé du lexer.
272
+ contextes: ['session'],
273
+ cibles: ecran("useThemeSombre() : data-theme de <html>, sinon matchMedia('(prefers-color-scheme: dark)'), suivi en direct", "useThemeSombre() : useColorScheme() === 'dark'", 'humalis.themeSombre() : data-theme de <html>, sinon le système, la page redessinée au changement'),
274
+ couvre: [],
275
+ description: 'La condition « theme est sombre » est vraie quand le thème affiché est sombre (choix du visiteur ou du projet, sinon le système) ; « theme est clair » et « theme n’est pas sombre » sont son contraire. « basculer theme » passe au thème inverse. Le HTML écrit d’avance la considère fausse.',
276
+ exemple: 'theme :\n mode automatique\n\npage "accueil" :\n titre "Accueil"\n bouton "Changer de thème"\n quand l\'utilisateur clique sur "Changer de thème" :\n basculer theme\n si theme est sombre :\n texte "Bonne soirée"',
277
+ statut: 'supporte',
278
+ etape: 9,
279
+ },
265
280
  ];
@@ -2169,8 +2169,8 @@ export const VOCABULAIRE = [
2169
2169
  cibles: ecran('<ul> et ses <li>', 'FlatList', '<ul>'),
2170
2170
  couvre: ['html:ul', 'html:ol', 'html:li', 'html:menu', 'rn:FlatList'],
2171
2171
  description: 'Une liste à puces (ou numérotée). Ailleurs, « liste » reste un nom de variable possible.',
2172
- exemple: PAGE(' variable produits = ["Livre"]\n liste produits'),
2173
- statut: 'prevu',
2172
+ exemple: PAGE(' liste :\n texte "Livre"'),
2173
+ statut: 'supporte',
2174
2174
  etape: 1,
2175
2175
  note: 'L’élément n’est pas encore pris en charge. Comme type de champ d’une donnée du serveur, « liste de texte » marche déjà.',
2176
2176
  },
@@ -2183,8 +2183,8 @@ export const VOCABULAIRE = [
2183
2183
  cibles: ecran('<table> et ses lignes', 'une liste de lignes', '<table>'),
2184
2184
  couvre: ['html:table', 'html:thead', 'html:tbody', 'html:tr', 'html:td', 'html:th', 'html:caption'],
2185
2185
  description: 'Un tableau, décrit par ses colonnes et la liste qu’il affiche.',
2186
- exemple: PAGE(' tableau produits :\n colonne "Nom" : nom'),
2187
- statut: 'prevu',
2186
+ exemple: PAGE(' tableau "Produits" :\n colonnes "Nom", "Prix"\n rangee "Livre", "10"'),
2187
+ statut: 'supporte',
2188
2188
  etape: 1,
2189
2189
  },
2190
2190
  {
package/docs/guide.en.md CHANGED
@@ -103,6 +103,14 @@ Text that is displayed goes between quotes: `titre "Welcome"`. Without quotes,
103
103
  it is a variable name: `texte firstname` shows what the variable `firstname`
104
104
  contains.
105
105
 
106
+ To put a quote inside a text, write `\"`:
107
+
108
+ ```humalis
109
+ page "quote" :
110
+ titre "A quote"
111
+ texte "She said \"hello\" as she came in."
112
+ ```
113
+
106
114
  ### Accents are allowed, never required
107
115
 
108
116
  `si age est supérieur ou égal à 18` and `si age est superieur ou egal a 18`
@@ -178,6 +186,93 @@ page "menu" :
178
186
  Keep the levels in order, without skipping: screen readers use them to move
179
187
  through the page.
180
188
 
189
+ Inside a block placed under a subtitle, a `soustitre` goes down one level on its
190
+ own: a card in a section becomes level 3. `avec niveau` (with level) still lets
191
+ you choose.
192
+
193
+ ```humalis
194
+ page "home" :
195
+ titre "Home"
196
+ section :
197
+ soustitre "Our pages"
198
+ grille :
199
+ bloc :
200
+ soustitre "Examples"
201
+ texte "Six apps to try."
202
+ ```
203
+
204
+ In a `titre`, `soustitre`, or `texte`, `{accent "…"}` highlights a few words
205
+ with the brand color. Screen readers and the browser tab still get plain text.
206
+
207
+ ```humalis
208
+ page "offer" :
209
+ titre "A {accent "simple"} offer"
210
+ texte "Price: {accent "free"} forever"
211
+ ```
212
+
213
+ ### Lists
214
+
215
+ `liste :` makes bullets; `liste numerotee :` numbers each item. Each instruction in the block becomes one item.
216
+
217
+ ```humalis
218
+ page "guide" :
219
+ liste :
220
+ texte "Install Node.js"
221
+ texte "Create the project"
222
+ liste numerotee :
223
+ texte "Write the file"
224
+ texte "Compile"
225
+ ```
226
+
227
+ ### Code within a sentence
228
+
229
+ In text, `{code "…"}` displays a short piece of code in a monospace font.
230
+
231
+ ```humalis
232
+ page "guide" :
233
+ texte "Write {code "titre"} to add a heading."
234
+ ```
235
+
236
+ ### Code block
237
+
238
+ `code language :` copies the indented lines without interpreting them; the language is optional.
239
+
240
+ ```humalis
241
+ page "guide" :
242
+ code humalis :
243
+ page "home" :
244
+ titre "Welcome"
245
+ ```
246
+
247
+ `avec animation frappe` ("with typing animation") writes the code letter by
248
+ letter, with a cursor; `avec animation frappe au defilement` waits until the
249
+ block scrolls into view. The real code stays in place while it types (screen
250
+ readers, Copy button), and it shows at once if the person asked to reduce
251
+ motion. On a phone, the code shows in full.
252
+
253
+ ```humalis
254
+ page "home" :
255
+ code humalis avec animation frappe :
256
+ page "shop" :
257
+ titre "Our products"
258
+ bouton "Order"
259
+ ```
260
+
261
+ ### Table
262
+
263
+ A table has a caption, columns, and rows of equal width. A translation may follow each cell.
264
+
265
+ ```humalis
266
+ langues :
267
+ francais
268
+ anglais
269
+
270
+ page "guide" :
271
+ tableau "Commands" en anglais "Commands" :
272
+ colonnes "Command", "Role" en anglais "Role"
273
+ rangee "humalis compile", "builds the project" en anglais "builds the project"
274
+ ```
275
+
181
276
  ### Image
182
277
 
183
278
  ```humalis
@@ -391,10 +486,35 @@ a light (`clair`) and dark (`sombre`) value; `police titres` is the headings
391
486
  font. Humalis checks contrast: a text colour too pale for its background is
392
487
  reported at compile time.
393
488
 
489
+ A font can be shipped with the site, without relying on an outside service:
490
+ `police titres "Space Grotesk" depuis "./polices/space-grotesk.woff2"` (`.woff2`,
491
+ `.woff`, `.ttf` or `.otf` files).
492
+
493
+ Visitors can also switch themes. `basculer theme` ("toggle theme") switches to
494
+ the opposite of the theme being shown, and `si theme est sombre :` (or `clair`)
495
+ reacts to the theme on screen:
496
+
497
+ ```humalis
498
+ theme :
499
+ mode automatique
500
+
501
+ page "home" :
502
+ titre "Home"
503
+ bouton "Change theme"
504
+ quand l'utilisateur clique sur "Change theme" :
505
+ basculer theme
506
+ si theme est sombre :
507
+ texte "Good evening"
508
+ sinon :
509
+ texte "Good day"
510
+ ```
511
+
394
512
  ### Shared header and footer
395
513
 
396
514
  `entete :` (header) and `pied :` (footer) appear on every page. A page can
397
- leave them out with `sans entete` or `sans pied`.
515
+ leave them out with `sans entete` or `sans pied`. With a header, Humalis adds a
516
+ "Skip to content" link on its own, hidden until the keyboard reaches it: it lets
517
+ keyboard users jump past the header.
398
518
 
399
519
  ```humalis
400
520
  entete avec colle :
@@ -488,7 +608,21 @@ page "menu" :
488
608
  ```
489
609
 
490
610
  `fil d'Ariane` is a breadcrumb. In a `menu`, the link to the current page is
491
- marked as active.
611
+ marked as active. The links of a menu wrap when space runs out. A menu that
612
+ your CSS turns into a column (`flex-direction: column`) keeps that wrapping: add
613
+ `flex-wrap: nowrap` so it scrolls instead of starting a second column.
614
+
615
+ A `menu repliable :` (collapsible menu) folds behind a "Menu" button on a small
616
+ screen (phone); choosing a link closes it. On a large screen it stays open:
617
+
618
+ ```humalis
619
+ page "home" :
620
+ menu repliable :
621
+ lien "Home" vers "home"
622
+ lien "The menu" vers "home"
623
+ lien "Book" vers "home"
624
+ titre "Home"
625
+ ```
492
626
 
493
627
  ---
494
628
 
@@ -535,6 +669,18 @@ Animations: `apparition` (fade in), `montee` (slide up), `agrandissement`
535
669
  `agrandi` (grow), `leve` (lift). Humalis respects the person's "reduce motion"
536
670
  setting.
537
671
 
672
+ On a block, `et en cascade` ("and in a cascade") makes its children appear one
673
+ after another:
674
+
675
+ ```humalis
676
+ page "home" :
677
+ titre "Our dishes"
678
+ grille avec animation montee au defilement et en cascade :
679
+ texte "Soup of the day"
680
+ texte "Risotto"
681
+ texte "Apple pie"
682
+ ```
683
+
538
684
  ---
539
685
 
540
686
  ## 9. Variables and calculations
@@ -959,8 +1105,10 @@ Actions are written inside an event.
959
1105
  | `afficher erreur "…"` | shows an error message |
960
1106
  | `montrer "name"`, `cacher "name"` | shows or hides an element |
961
1107
  | `changer x par …` | changes a variable |
1108
+ | `changer x par champ "Label"` | reads a field's current value into a variable |
962
1109
  | `changer texte de "name" par "…"` | changes an element's text |
963
1110
  | `ajouter … à list`, `retirer … de list` | changes a list |
1111
+ | `ajouter champ "Label" à list` | adds a field's current value to a list |
964
1112
  | `rediriger vers "page"` | goes to a page |
965
1113
  | `revenir vers "page"` | goes back, or to that page |
966
1114
  | `envoyer requete vers "…"` | calls a server (see [the server](#22-connecting-pages-to-the-server)) |
@@ -981,6 +1129,22 @@ page "thanks" :
981
1129
  titre "Thank you!"
982
1130
  ```
983
1131
 
1132
+ Inside an action, `champ "Label"` reads what the person entered when they click. A number field yields a number, a checkbox yields true or false, and other fields yield text. Use the base language label even when the field has a translation.
1133
+
1134
+ ```humalis
1135
+ page "tasks" :
1136
+ variable tasks = ["Buy bread"]
1137
+ variable latest = ""
1138
+ champ "New task"
1139
+ bouton "Add"
1140
+ quand l'utilisateur clique sur "Add" :
1141
+ ajouter champ "New task" à tasks
1142
+ changer latest par champ "New task"
1143
+ texte "Latest added: {latest}"
1144
+ pour chaque task dans tasks :
1145
+ texte task
1146
+ ```
1147
+
984
1148
  ---
985
1149
 
986
1150
  ## 16. Messages, dialogs and questions
@@ -1617,9 +1781,13 @@ page "catalogue" :
1617
1781
 
1618
1782
  - `langues :` lists the languages (`francais`, `anglais`…).
1619
1783
  - `en francais "…"` gives another language's version of a text.
1784
+ - `sans traduction` ("without translation") keeps the same text in every
1785
+ language, for a proper noun: `lien "GitHub" sans traduction vers "https://github.com"`.
1620
1786
  - `traductions francais :` translates a repeated text once.
1621
1787
  - `selecteur de langue` shows the language switcher; `changer langue par
1622
1788
  anglais` and `si langue est anglais :` are used in events.
1789
+ - The message of `copier` is translated like any other text:
1790
+ `copier code avec message "Copied" en francais "Copié"`.
1623
1791
  - Humalis reports at compile time every text that lacks its translation.
1624
1792
  Humalis's own messages (required field, loading…) follow the language.
1625
1793