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.
@@ -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
  }
@@ -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') || estLeMot(suivant, 'vertical'))) ||
729
780
  ((genre === 'section' || GENRES_AVEC_TITRE.includes(genre)) && suivant.type === 'CHAINE'));
730
781
  }
731
782
  function analyserBlocMiseEnPage(flux) {
@@ -734,6 +785,16 @@ 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
+ // « menu vertical : » : les liens en colonne. Les deux se combinent, dans n'importe quel ordre.
790
+ let repliable = false;
791
+ let vertical = false;
792
+ while (genre === 'menu' && (estLeMot(flux.actuel(), 'repliable') || estLeMot(flux.actuel(), 'vertical'))) {
793
+ if (estLeMot(flux.avancer(), 'repliable'))
794
+ repliable = true;
795
+ else
796
+ vertical = true;
797
+ }
737
798
  // Le titre d'une section devient son premier élément : un « titre » comme les autres.
738
799
  const titre = [];
739
800
  let titreBloc;
@@ -769,7 +830,7 @@ function analyserBlocMiseEnPage(flux) {
769
830
  } while (flux.accepter('MOT_CLE', 'et'));
770
831
  }
771
832
  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 };
833
+ return { type: 'Bloc', genre, styles, classes, ...(titreBloc ? { titre: titreBloc } : {}), ...(ancre !== undefined ? { ancre } : {}), ...(nom !== undefined ? { nom } : {}), ...(repliable ? { repliable } : {}), ...(vertical ? { vertical } : {}), corps: [...titre, ...analyserBloc(flux)], ligne: debut.ligne };
773
834
  }
774
835
  function lireNombre(flux, description) {
775
836
  const negatif = flux.accepter('PONCTUATION', '-');
@@ -936,7 +997,7 @@ function analyserAction(flux) {
936
997
  // « ajouter nom à panier » : « à » et « de » sont des mots de liaison
937
998
  // déjà écartés par le lexer ; « dans » reste permis.
938
999
  if (debut.valeur === 'ajouter' || debut.valeur === 'retirer') {
939
- const valeur = analyserExpression(flux);
1000
+ const valeur = analyserOperande(flux);
940
1001
  flux.accepter('MOT_CLE', 'dans');
941
1002
  const { nom } = flux.attendreNom('le nom de la liste');
942
1003
  return {
@@ -1018,7 +1079,7 @@ function analyserAction(flux) {
1018
1079
  type: 'Affectation',
1019
1080
  nom,
1020
1081
  ...(champs.length > 0 ? { champs } : {}),
1021
- valeur: analyserExpression(flux),
1082
+ valeur: analyserOperande(flux),
1022
1083
  ligne: debut.ligne,
1023
1084
  };
1024
1085
  }
@@ -1154,6 +1215,17 @@ function analyserComparaison(flux) {
1154
1215
  flux.avancer();
1155
1216
  return { type: 'Comparaison', gauche: { type: 'MouvementReduit', ligne: debut.ligne }, operateur: pas ? 'faux' : 'vrai', ligne: debut.ligne };
1156
1217
  }
1218
+ // « theme est sombre », « theme est clair » (et « theme n'est pas sombre ») : le thème affiché.
1219
+ if (estLeMot(flux.actuel(), 'theme') &&
1220
+ flux.regarder(1).type === 'MOT_CLE' &&
1221
+ flux.regarder(1).valeur === 'est' &&
1222
+ (estLeMot(flux.regarder(2 + pas), 'sombre') || estLeMot(flux.regarder(2 + pas), 'clair'))) {
1223
+ const debut = flux.avancer();
1224
+ for (let i = 0; i < 1 + pas; i++)
1225
+ flux.avancer();
1226
+ const sombre = estLeMot(flux.avancer(), 'sombre');
1227
+ return { type: 'Comparaison', gauche: { type: 'ThemeSombre', ligne: debut.ligne }, operateur: sombre !== Boolean(pas) ? 'vrai' : 'faux', ligne: debut.ligne };
1228
+ }
1157
1229
  const gauche = analyserOperande(flux);
1158
1230
  // « panier contient "Livre" », « nom contient "Liv" ».
1159
1231
  if (estLeMot(flux.actuel(), 'contient')) {
@@ -1285,6 +1357,14 @@ function analyserApresEst(flux, gauche) {
1285
1357
  return { type: 'Comparaison', gauche, operateur, droite, ligne: gauche.ligne };
1286
1358
  }
1287
1359
  /** Variable ou champ de formulaire (« champ "email" »). */
1360
+ /** « code … : » est un bloc de code quand le lexer a recopié ses lignes (CODE_BRUT) juste après les deux-points. */
1361
+ function estBlocDeCode(flux) {
1362
+ for (let decalage = 1; decalage <= 7; decalage++) {
1363
+ if (flux.regarder(decalage).valeur === ':')
1364
+ return flux.regarder(decalage + 2).type === 'CODE_BRUT' || flux.regarder(decalage + 1).type === 'CODE_BRUT';
1365
+ }
1366
+ return false;
1367
+ }
1288
1368
  export function analyserOperande(flux) {
1289
1369
  if (flux.verifier('MOT_CLE', 'champ') && flux.regarder(1).type === 'CHAINE') {
1290
1370
  const debut = flux.avancer();
@@ -1531,6 +1611,14 @@ function analyserValeur(flux) {
1531
1611
  flux.avancer();
1532
1612
  // « "Bienvenue" en anglais "Welcome" » : les versions écrites à côté.
1533
1613
  const versions = lireVersions(flux);
1614
+ // « "GitHub" sans traduction » : le même texte dans toutes les langues (un nom propre).
1615
+ if (estLeMot(flux.actuel(), 'sans') && estLeMot(flux.regarder(1), 'traduction')) {
1616
+ const sans = flux.avancer();
1617
+ flux.avancer();
1618
+ if (versions)
1619
+ throw new ErreurSyntaxique('ce texte a une version traduite et « sans traduction » : gardez l’un ou l’autre.', sans.ligne);
1620
+ return { type: 'Chaine', parties: token.parties ?? [], sansTraduction: true, ligne: token.ligne };
1621
+ }
1534
1622
  return { type: 'Chaine', parties: token.parties ?? [], ...(versions ? { versions } : {}), ligne: token.ligne };
1535
1623
  }
1536
1624
  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,12 @@ 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' });
694
+ // « menu vertical » : la colonne que lit le CSS.
695
+ if (instruction.vertical)
696
+ options.push({ nom: 'menu', valeur: 'vertical' });
677
697
  const noeud = {
678
698
  type: 'bloc',
679
699
  genre: instruction.genre,
@@ -795,6 +815,10 @@ function traduireValeur(expression, analyse) {
795
815
  const parties = (liste) => liste.map((p) => {
796
816
  if (p.type === 'texte')
797
817
  return { texte: p.valeur };
818
+ if (p.type === 'accent')
819
+ return { texte: p.valeur, accent: true };
820
+ if (p.type === 'code')
821
+ return { texte: p.valeur, code: true };
798
822
  const format = analyse.formats.get(p);
799
823
  const pluriel = analyse.pluriels.get(p);
800
824
  return {
@@ -892,6 +916,8 @@ function traduireValeur(expression, analyse) {
892
916
  return { type: 'erreurRequete', variable: expression.variable };
893
917
  case 'MouvementReduit':
894
918
  return { type: 'mouvementReduit' };
919
+ case 'ThemeSombre':
920
+ return { type: 'themeSombre' };
895
921
  }
896
922
  }
897
923
  /** Une durée écrite en chiffres, en millisecondes (l'analyse a refusé les autres là où il le faut). */
@@ -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,95 @@ 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. In a `section`, the first subtitle is
191
+ the section's title, even when it sits in its own block; every other subtitle of
192
+ the section goes down one level. `avec niveau` (with level) still lets you
193
+ choose.
194
+
195
+ ```humalis
196
+ page "home" :
197
+ titre "Home"
198
+ section :
199
+ soustitre "Our pages"
200
+ grille :
201
+ bloc :
202
+ soustitre "Examples"
203
+ texte "Six apps to try."
204
+ ```
205
+
206
+ In a `titre`, `soustitre`, or `texte`, `{accent "…"}` highlights a few words
207
+ with the brand color. Screen readers and the browser tab still get plain text.
208
+
209
+ ```humalis
210
+ page "offer" :
211
+ titre "A {accent "simple"} offer"
212
+ texte "Price: {accent "free"} forever"
213
+ ```
214
+
215
+ ### Lists
216
+
217
+ `liste :` makes bullets; `liste numerotee :` numbers each item. Each instruction in the block becomes one item.
218
+
219
+ ```humalis
220
+ page "guide" :
221
+ liste :
222
+ texte "Install Node.js"
223
+ texte "Create the project"
224
+ liste numerotee :
225
+ texte "Write the file"
226
+ texte "Compile"
227
+ ```
228
+
229
+ ### Code within a sentence
230
+
231
+ In text, `{code "…"}` displays a short piece of code in a monospace font.
232
+
233
+ ```humalis
234
+ page "guide" :
235
+ texte "Write {code "titre"} to add a heading."
236
+ ```
237
+
238
+ ### Code block
239
+
240
+ `code language :` copies the indented lines without interpreting them; the language is optional.
241
+
242
+ ```humalis
243
+ page "guide" :
244
+ code humalis :
245
+ page "home" :
246
+ titre "Welcome"
247
+ ```
248
+
249
+ `avec animation frappe` ("with typing animation") writes the code letter by
250
+ letter, with a cursor; `avec animation frappe au defilement` waits until the
251
+ block scrolls into view. The real code stays in place while it types (screen
252
+ readers, Copy button), and it shows at once if the person asked to reduce
253
+ motion. On a phone, the code shows in full.
254
+
255
+ ```humalis
256
+ page "home" :
257
+ code humalis avec animation frappe :
258
+ page "shop" :
259
+ titre "Our products"
260
+ bouton "Order"
261
+ ```
262
+
263
+ ### Table
264
+
265
+ A table has a caption, columns, and rows of equal width. A translation may follow each cell.
266
+
267
+ ```humalis
268
+ langues :
269
+ francais
270
+ anglais
271
+
272
+ page "guide" :
273
+ tableau "Commands" en anglais "Commands" :
274
+ colonnes "Command", "Role" en anglais "Role"
275
+ rangee "humalis compile", "builds the project" en anglais "builds the project"
276
+ ```
277
+
181
278
  ### Image
182
279
 
183
280
  ```humalis
@@ -391,10 +488,35 @@ a light (`clair`) and dark (`sombre`) value; `police titres` is the headings
391
488
  font. Humalis checks contrast: a text colour too pale for its background is
392
489
  reported at compile time.
393
490
 
491
+ A font can be shipped with the site, without relying on an outside service:
492
+ `police titres "Space Grotesk" depuis "./polices/space-grotesk.woff2"` (`.woff2`,
493
+ `.woff`, `.ttf` or `.otf` files).
494
+
495
+ Visitors can also switch themes. `basculer theme` ("toggle theme") switches to
496
+ the opposite of the theme being shown, and `si theme est sombre :` (or `clair`)
497
+ reacts to the theme on screen:
498
+
499
+ ```humalis
500
+ theme :
501
+ mode automatique
502
+
503
+ page "home" :
504
+ titre "Home"
505
+ bouton "Change theme"
506
+ quand l'utilisateur clique sur "Change theme" :
507
+ basculer theme
508
+ si theme est sombre :
509
+ texte "Good evening"
510
+ sinon :
511
+ texte "Good day"
512
+ ```
513
+
394
514
  ### Shared header and footer
395
515
 
396
516
  `entete :` (header) and `pied :` (footer) appear on every page. A page can
397
- leave them out with `sans entete` or `sans pied`.
517
+ leave them out with `sans entete` or `sans pied`. With a header, Humalis adds a
518
+ "Skip to content" link on its own, hidden until the keyboard reaches it: it lets
519
+ keyboard users jump past the header.
398
520
 
399
521
  ```humalis
400
522
  entete avec colle :
@@ -488,7 +610,32 @@ page "menu" :
488
610
  ```
489
611
 
490
612
  `fil d'Ariane` is a breadcrumb. In a `menu`, the link to the current page is
491
- marked as active.
613
+ marked as active. The links of a menu wrap when space runs out. A `menu
614
+ vertical :` stacks its links in a column and never starts a second column: a
615
+ table of contents with a limited height scrolls. It combines with `repliable`
616
+ (`menu vertical repliable :`).
617
+
618
+ ```humalis
619
+ page "guide" :
620
+ titre "The guide"
621
+ menu vertical :
622
+ lien "Install" vers "guide"
623
+ lien "Pages" vers "guide"
624
+ ```
625
+
626
+ A `menu repliable :` (collapsible menu) folds behind a burger button (named
627
+ "Menu" for screen readers) on a small screen (phone); the icon turns into a cross
628
+ while the menu is open. Choosing a link, tapping outside or pressing Escape
629
+ closes it. On a large screen it stays open:
630
+
631
+ ```humalis
632
+ page "home" :
633
+ menu repliable :
634
+ lien "Home" vers "home"
635
+ lien "The menu" vers "home"
636
+ lien "Book" vers "home"
637
+ titre "Home"
638
+ ```
492
639
 
493
640
  ---
494
641
 
@@ -535,6 +682,18 @@ Animations: `apparition` (fade in), `montee` (slide up), `agrandissement`
535
682
  `agrandi` (grow), `leve` (lift). Humalis respects the person's "reduce motion"
536
683
  setting.
537
684
 
685
+ On a block, `et en cascade` ("and in a cascade") makes its children appear one
686
+ after another:
687
+
688
+ ```humalis
689
+ page "home" :
690
+ titre "Our dishes"
691
+ grille avec animation montee au defilement et en cascade :
692
+ texte "Soup of the day"
693
+ texte "Risotto"
694
+ texte "Apple pie"
695
+ ```
696
+
538
697
  ---
539
698
 
540
699
  ## 9. Variables and calculations
@@ -959,8 +1118,10 @@ Actions are written inside an event.
959
1118
  | `afficher erreur "…"` | shows an error message |
960
1119
  | `montrer "name"`, `cacher "name"` | shows or hides an element |
961
1120
  | `changer x par …` | changes a variable |
1121
+ | `changer x par champ "Label"` | reads a field's current value into a variable |
962
1122
  | `changer texte de "name" par "…"` | changes an element's text |
963
1123
  | `ajouter … à list`, `retirer … de list` | changes a list |
1124
+ | `ajouter champ "Label" à list` | adds a field's current value to a list |
964
1125
  | `rediriger vers "page"` | goes to a page |
965
1126
  | `revenir vers "page"` | goes back, or to that page |
966
1127
  | `envoyer requete vers "…"` | calls a server (see [the server](#22-connecting-pages-to-the-server)) |
@@ -981,6 +1142,22 @@ page "thanks" :
981
1142
  titre "Thank you!"
982
1143
  ```
983
1144
 
1145
+ 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.
1146
+
1147
+ ```humalis
1148
+ page "tasks" :
1149
+ variable tasks = ["Buy bread"]
1150
+ variable latest = ""
1151
+ champ "New task"
1152
+ bouton "Add"
1153
+ quand l'utilisateur clique sur "Add" :
1154
+ ajouter champ "New task" à tasks
1155
+ changer latest par champ "New task"
1156
+ texte "Latest added: {latest}"
1157
+ pour chaque task dans tasks :
1158
+ texte task
1159
+ ```
1160
+
984
1161
  ---
985
1162
 
986
1163
  ## 16. Messages, dialogs and questions
@@ -1617,9 +1794,13 @@ page "catalogue" :
1617
1794
 
1618
1795
  - `langues :` lists the languages (`francais`, `anglais`…).
1619
1796
  - `en francais "…"` gives another language's version of a text.
1797
+ - `sans traduction` ("without translation") keeps the same text in every
1798
+ language, for a proper noun: `lien "GitHub" sans traduction vers "https://github.com"`.
1620
1799
  - `traductions francais :` translates a repeated text once.
1621
1800
  - `selecteur de langue` shows the language switcher; `changer langue par
1622
1801
  anglais` and `si langue est anglais :` are used in events.
1802
+ - The message of `copier` is translated like any other text:
1803
+ `copier code avec message "Copied" en francais "Copié"`.
1623
1804
  - Humalis reports at compile time every text that lacks its translation.
1624
1805
  Humalis's own messages (required field, loading…) follow the language.
1625
1806