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.
- package/LICENSE +85 -201
- package/LICENSE.en.md +86 -0
- package/NOTICE +6 -3
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/dist/analyse/accessibilite.js +13 -5
- package/dist/analyse/analyse.js +43 -3
- package/dist/analyse/ecran.js +1 -1
- 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 +33 -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 +7 -2
- package/dist/cibles/mobile/rendu.js +6 -3
- package/dist/cibles/statique/elements-js.js +1 -1
- package/dist/cibles/statique/page.js +99 -6
- package/dist/cibles/statique/projet.js +31 -0
- package/dist/cibles/statique/valeurs.js +5 -1
- package/dist/cibles/statique/widgets.js +1 -1
- package/dist/cibles/web/css-ecran.js +22 -1
- package/dist/cibles/web/ecran.js +161 -16
- package/dist/cibles/web/interactions.js +50 -3
- package/dist/cibles/web/projet.js +29 -2
- package/dist/cibles/web/theme.js +11 -2
- package/dist/cibles/web/valeurs.js +17 -0
- package/dist/cibles/web/widgets.js +1 -1
- package/dist/i18n/messages.js +2 -0
- package/dist/lexer/lexer.js +106 -3
- package/dist/navigateur.js +5274 -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/miseEnPage.js +1 -1
- package/dist/parser/parser.js +85 -3
- package/dist/parser/theme.js +10 -1
- package/dist/ri/construire.js +24 -1
- package/dist/vocabulaire/table-ecran.js +3 -3
- package/dist/vocabulaire/table-interactions.js +16 -1
- package/dist/vocabulaire/table.js +4 -4
- package/docs/guide.en.md +170 -2
- package/docs/guide.md +171 -1
- package/llms.txt +14 -1
- package/package.json +7 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { verificationsDuChamp, messageEnCode, libellesEnLangues } from '../../i18n/messages.js';
|
|
1
|
+
import { verificationsDuChamp, messageEnCode, libellesEnLangues, messageEnLangues } from '../../i18n/messages.js';
|
|
2
2
|
/**
|
|
3
3
|
* Un écran de la représentation intermédiaire → une page du site statique :
|
|
4
4
|
* son contenu de départ, écrit dans le HTML (décision T2), et son script,
|
|
@@ -9,6 +9,8 @@ import { verificationsDuChamp, messageEnCode, libellesEnLangues } from '../../i1
|
|
|
9
9
|
* script doivent donner exactement la même page.
|
|
10
10
|
*/
|
|
11
11
|
import { sousActions } from '../../ri/actions.js';
|
|
12
|
+
import { colorerCodeHumalis } from '../commun/colorerCode.js';
|
|
13
|
+
import { attributsFrappe } from '../commun/mouvement.js';
|
|
12
14
|
import { arbreWidget, arbreComposite, initiales } from './widgets.js';
|
|
13
15
|
import { iconeHtml, etoilesHtml } from './html-elements.js';
|
|
14
16
|
import { appelGraphique, htmlGraphique } from './graphique.js';
|
|
@@ -320,9 +322,13 @@ class GenerateurPage {
|
|
|
320
322
|
this.nomsPris.add(resultat);
|
|
321
323
|
return resultat;
|
|
322
324
|
}
|
|
325
|
+
/** « Copier » dans chaque langue du site, la langue de base (celle du HTML de départ) en premier. */
|
|
326
|
+
messageCopier() {
|
|
327
|
+
return (this.site.langues && messageEnLangues('Copier', this.site.langues.map((l) => l.code))) || [{ code: 'fr', texte: 'Copier' }];
|
|
328
|
+
}
|
|
323
329
|
// ─── La page ────────────────────────────────────────────────────────────
|
|
324
330
|
generer() {
|
|
325
|
-
const { titre, description } = titreEtDescription(this.ecran);
|
|
331
|
+
const { titre, description } = titreEtDescription(this.ecran, this.site.langues);
|
|
326
332
|
// Les fonctions et le dessin d'abord : on y lit les variables partagées
|
|
327
333
|
// que la page utilise vraiment.
|
|
328
334
|
const fonctions = new Ecrivain();
|
|
@@ -600,7 +606,7 @@ ${prise}${code}}
|
|
|
600
606
|
break;
|
|
601
607
|
case 'copie':
|
|
602
608
|
// « copier code » : le résultat est dit à la personne, sauf « sans message ».
|
|
603
|
-
e.ligne(`await copier(${this.enTexte(action.valeur)}${action.message === null ? ', null' : action.message === 'Copié' ? '' : `, ${chaine(action.message)}`});`);
|
|
609
|
+
e.ligne(`await copier(${this.enTexte(action.valeur)}${action.message === null ? ', null' : action.texte ? `, ${x(action.texte)}` : action.message === 'Copié' ? '' : `, ${chaine(action.message)}`});`);
|
|
604
610
|
break;
|
|
605
611
|
case 'partage': {
|
|
606
612
|
// La feuille de partage du système ; sans elle, le lien (ou le texte) est copié.
|
|
@@ -892,13 +898,40 @@ ${prise}${code}}
|
|
|
892
898
|
case 'graphique':
|
|
893
899
|
e.ligne(`${appelGraphique(noeud, (v) => expressionJs(v, this.contexte))},`);
|
|
894
900
|
break;
|
|
901
|
+
case 'codeDocumentation':
|
|
902
|
+
const frappe = attributsFrappe(noeud);
|
|
903
|
+
const marques = frappe ? `, 'data-humalis-frappe': '${frappe.frappe}', 'data-humalis-frappe-cle': '${frappe.cle}'` : '';
|
|
904
|
+
e.ligne(`creer('div', { class: 'humalis-bloc-code-conteneur' }, creer('pre', { class: 'humalis-bloc-code${frappe ? ` ${frappe.classe}` : ''}', 'data-langage': ${chaine(noeud.langage ?? '')}${marques} }, (() => { const code = document.createElement('code'); code.innerHTML = ${chaine(colorerCodeHumalis(noeud.code, noeud.langage))}; return code; })()), creer('button', { type: 'button', onclick: () => copier(${chaine(noeud.code)}) }, ${messageEnCode(this.messageCopier(), chaine)})),`);
|
|
905
|
+
break;
|
|
906
|
+
case 'tableauDocumentation': {
|
|
907
|
+
const cellule = (valeur, balise) => `creer('${balise}', {}, ${this.contenuAccentJs(valeur, 'texte')})`;
|
|
908
|
+
const rang = (valeurs, balise) => `creer('tr', {}, ${valeurs.map((v) => cellule(v, balise)).join(', ')})`;
|
|
909
|
+
e.ligne(`creer('div', { class: 'humalis-tableau-conteneur' }, creer('table', {}, creer('caption', {}, ${this.contenuAccentJs(noeud.legende, 'texte')}), creer('thead', {}, ${rang(noeud.colonnes, 'th')}), creer('tbody', {}, ${noeud.rangees.map((r) => rang(r, 'td')).join(', ')}))),`);
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
895
912
|
case 'bloc': {
|
|
913
|
+
if (noeud.genre === 'liste' || noeud.genre === 'listeNumerotee') {
|
|
914
|
+
const balise = noeud.genre === 'liste' ? 'ul' : 'ol';
|
|
915
|
+
e.ligne(`creer('${balise}', { class: 'humalis-liste' },`);
|
|
916
|
+
e.indenter(() => noeud.corps.forEach((enfant) => {
|
|
917
|
+
if (enfant.type === 'repetition') {
|
|
918
|
+
const debut = `...${expressionJs(enfant.liste, this.contexte)}.flatMap((${nomJs(enfant.variable)}) => `;
|
|
919
|
+
e.bloc(`${debut}[`, () => this.ecrireGroupe(e, enfant.corps, "creer('li', {}, ...", '),'), ']),');
|
|
920
|
+
}
|
|
921
|
+
else
|
|
922
|
+
this.ecrireGroupe(e, [enfant], "creer('li', {}, ...", '),');
|
|
923
|
+
}));
|
|
924
|
+
e.ligne('),');
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
896
927
|
if (COMPOSITES.has(noeud.genre)) {
|
|
897
928
|
this.ecrireEnfantJs(e, arbreComposite(this.aideComposite(noeud, this.environnement())));
|
|
898
929
|
break;
|
|
899
930
|
}
|
|
900
931
|
const balise = BALISES_BLOC[noeud.genre] ?? 'div';
|
|
901
|
-
|
|
932
|
+
// « menu repliable » : le bouton « Menu » en premier enfant (l'écouteur est dans humalis.js).
|
|
933
|
+
const bouton = estRepliable(noeud) ? `creer('button', { type: 'button', class: 'humalis-menu-bouton', 'aria-expanded': 'false' }, 'Menu'), ` : '';
|
|
934
|
+
this.ecrireGroupe(e, noeud.corps, `creer('${balise}', { class: ${chaine(classesDuBloc(noeud))} }, ${bouton}...`, '),');
|
|
902
935
|
// « arrive en bas de » ce bloc : la sentinelle observée et le vrai bouton « Voir plus », tant que la condition est vraie.
|
|
903
936
|
const bas = noeud.id !== undefined ? this.basDesBlocs.get(noeud.id) : undefined;
|
|
904
937
|
if (bas) {
|
|
@@ -963,7 +996,7 @@ ${prise}${code}}
|
|
|
963
996
|
const contenu = (lucideIcone ? `icone(${chaine(lucideIcone)}, 'humalis-icone'), ` : '') +
|
|
964
997
|
(this.textesChanges.has(noeud.id)
|
|
965
998
|
? `textes[${chaine(noeud.id)}] ?? ${contenuJs(noeud.contenu, this.contexte)}`
|
|
966
|
-
:
|
|
999
|
+
: this.contenuAccentJs(noeud.contenu, noeud.genre));
|
|
967
1000
|
switch (noeud.genre) {
|
|
968
1001
|
case 'titre':
|
|
969
1002
|
case 'soustitre':
|
|
@@ -1137,13 +1170,49 @@ ${prise}${code}}
|
|
|
1137
1170
|
case 'graphique':
|
|
1138
1171
|
lignes.push(htmlGraphique(noeud, (v) => this.initial(v, env), (v) => this.texteInitial(v, env)));
|
|
1139
1172
|
break;
|
|
1173
|
+
case 'codeDocumentation':
|
|
1174
|
+
const frappe = attributsFrappe(noeud);
|
|
1175
|
+
const marques = frappe ? ` data-humalis-frappe="${frappe.frappe}" data-humalis-frappe-cle="${frappe.cle}"` : '';
|
|
1176
|
+
lignes.push(`<div class="humalis-bloc-code-conteneur"><pre class="humalis-bloc-code${frappe ? ` ${frappe.classe}` : ''}" data-langage="${echapperTexte(noeud.langage ?? '')}"${marques}><code>${colorerCodeHumalis(noeud.code, noeud.langage)}</code></pre><button type="button">${echapperTexte(this.messageCopier()[0].texte)}</button></div>`);
|
|
1177
|
+
break;
|
|
1178
|
+
case 'tableauDocumentation': {
|
|
1179
|
+
const cellule = (v, balise) => `<${balise}>${this.contenuAccentHtml(v, 'texte', env)}</${balise}>`;
|
|
1180
|
+
lignes.push('<div class="humalis-tableau-conteneur"><table>');
|
|
1181
|
+
lignes.push(`<caption>${this.contenuAccentHtml(noeud.legende, 'texte', env)}</caption><thead><tr>${noeud.colonnes.map((v) => cellule(v, 'th')).join('')}</tr></thead><tbody>`);
|
|
1182
|
+
for (const rangee of noeud.rangees)
|
|
1183
|
+
lignes.push(`<tr>${rangee.map((v) => cellule(v, 'td')).join('')}</tr>`);
|
|
1184
|
+
lignes.push('</tbody></table></div>');
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1140
1187
|
case 'bloc': {
|
|
1188
|
+
if (noeud.genre === 'liste' || noeud.genre === 'listeNumerotee') {
|
|
1189
|
+
const balise = noeud.genre === 'liste' ? 'ul' : 'ol';
|
|
1190
|
+
lignes.push(`<${balise} class="humalis-liste">`);
|
|
1191
|
+
for (const enfant of noeud.corps) {
|
|
1192
|
+
if (enfant.type === 'repetition') {
|
|
1193
|
+
const valeurs = evaluer(enfant.liste, env);
|
|
1194
|
+
for (const valeur of Array.isArray(valeurs) ? valeurs : []) {
|
|
1195
|
+
lignes.push('<li>');
|
|
1196
|
+
this.ecrireGroupeHtml(enfant.corps, env.enfant().definir(enfant.variable, valeur), lignes);
|
|
1197
|
+
lignes.push('</li>');
|
|
1198
|
+
}
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
lignes.push('<li>');
|
|
1202
|
+
this.ecrireGroupeHtml([enfant], env, lignes);
|
|
1203
|
+
lignes.push('</li>');
|
|
1204
|
+
}
|
|
1205
|
+
lignes.push(`</${balise}>`);
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1141
1208
|
if (COMPOSITES.has(noeud.genre)) {
|
|
1142
1209
|
this.ecrireEnfantHtml(arbreComposite(this.aideComposite(noeud, env)), env, lignes);
|
|
1143
1210
|
break;
|
|
1144
1211
|
}
|
|
1145
1212
|
const balise = BALISES_BLOC[noeud.genre] ?? 'div';
|
|
1146
1213
|
lignes.push(`<${balise}${attributs([['class', classesDuBloc(noeud)]])}>`);
|
|
1214
|
+
if (estRepliable(noeud))
|
|
1215
|
+
lignes.push('<button type="button" class="humalis-menu-bouton" aria-expanded="false">Menu</button>');
|
|
1147
1216
|
this.ecrireGroupeHtml(noeud.corps, env, lignes);
|
|
1148
1217
|
lignes.push(`</${balise}>`);
|
|
1149
1218
|
const bas = noeud.id !== undefined ? this.basDesBlocs.get(noeud.id) : undefined;
|
|
@@ -1192,6 +1261,27 @@ ${prise}${code}}
|
|
|
1192
1261
|
return undefined;
|
|
1193
1262
|
}
|
|
1194
1263
|
}
|
|
1264
|
+
contenuAccentJs(valeur, genre) {
|
|
1265
|
+
if (valeur.type !== 'texte' || !['titre', 'soustitre', 'texte'].includes(genre))
|
|
1266
|
+
return contenuJs(valeur, this.contexte);
|
|
1267
|
+
const parties = (liste) => liste.map((p) => 'texte' in p
|
|
1268
|
+
? p.accent ? `creer('span', { class: 'humalis-accent' }, ${chaine(p.texte)})` : p.code ? `creer('code', { class: 'humalis-code-ligne' }, ${chaine(p.texte)})` : chaine(p.texte)
|
|
1269
|
+
: contenuJs({ type: 'texte', parties: [p] }, this.contexte)).join(', ');
|
|
1270
|
+
const accent = (liste) => liste.some((p) => 'texte' in p && (p.accent || p.code));
|
|
1271
|
+
if (!accent(valeur.parties) && !valeur.versions?.some((v) => accent(v.parties)))
|
|
1272
|
+
return contenuJs(valeur, this.contexte);
|
|
1273
|
+
if (valeur.versions)
|
|
1274
|
+
return `...(${valeur.versions.slice(1).map((v) => `langue() === ${chaine(v.code)} ? [${parties(v.parties)}] : `).join('')}[${parties(valeur.versions[0].parties)}])`;
|
|
1275
|
+
return parties(valeur.parties);
|
|
1276
|
+
}
|
|
1277
|
+
contenuAccentHtml(valeur, genre, env) {
|
|
1278
|
+
if (valeur.type !== 'texte' || !['titre', 'soustitre', 'texte'].includes(genre) || !valeur.parties.some((p) => 'texte' in p && (p.accent || p.code)))
|
|
1279
|
+
return echapperTexte(texteDe(valeur, env, this.contexte.champs));
|
|
1280
|
+
return valeur.parties.map((p) => {
|
|
1281
|
+
const texte = echapperTexte('texte' in p ? p.texte : texteDe({ type: 'texte', parties: [p] }, env, this.contexte.champs));
|
|
1282
|
+
return 'texte' in p && p.accent ? `<span class="humalis-accent">${texte}</span>` : 'texte' in p && p.code ? `<code class="humalis-code-ligne">${texte}</code>` : texte;
|
|
1283
|
+
}).join('');
|
|
1284
|
+
}
|
|
1195
1285
|
texteInitial(valeur, env) {
|
|
1196
1286
|
try {
|
|
1197
1287
|
return texteDe(valeur, env, this.contexte.champs);
|
|
@@ -1456,7 +1546,7 @@ ${prise}${code}}
|
|
|
1456
1546
|
const classes = this.classes(noeud) || undefined;
|
|
1457
1547
|
const commun = (avant = []) => attributs([...avant, ['data-humalis', noeud.id], ['class', classes], ...(noeud.annonce ? [['aria-live', 'polite'], ['aria-atomic', 'true']] : []), ...attributsRaccourci(noeud)]);
|
|
1458
1548
|
const lucideHtml = noeud.reglages?.icone ? chercherIcone(noeud.reglages.icone)?.lucide : undefined;
|
|
1459
|
-
const contenu = (lucideHtml ? iconeHtml(lucideHtml, 'humalis-icone') : '') +
|
|
1549
|
+
const contenu = (lucideHtml ? iconeHtml(lucideHtml, 'humalis-icone') : '') + this.contenuAccentHtml(noeud.contenu, noeud.genre, env);
|
|
1460
1550
|
switch (noeud.genre) {
|
|
1461
1551
|
case 'titre':
|
|
1462
1552
|
case 'soustitre': {
|
|
@@ -1485,6 +1575,8 @@ ${prise}${code}}
|
|
|
1485
1575
|
}
|
|
1486
1576
|
}
|
|
1487
1577
|
}
|
|
1578
|
+
/** « menu repliable » : un bouton « Menu » le replie sur un petit écran. */
|
|
1579
|
+
const estRepliable = (noeud) => noeud.options.some((o) => o.nom === 'menu' && o.valeur === 'repliable');
|
|
1488
1580
|
/** Les blocs d'éléments d'écran, décrits par widgets.ts. */
|
|
1489
1581
|
const COMPOSITES = new Set(['onglets', 'deroulant', 'accordeon', 'carrousel', 'filAriane']);
|
|
1490
1582
|
/** La balise de chaque bloc : une section, l'en-tête et le pied de la page, sinon une boîte. */
|
|
@@ -1512,6 +1604,7 @@ const OUTILS_PAGE = [
|
|
|
1512
1604
|
'afficherMessage',
|
|
1513
1605
|
'planifier',
|
|
1514
1606
|
'mouvementReduit',
|
|
1607
|
+
'themeSombre',
|
|
1515
1608
|
'copier',
|
|
1516
1609
|
'partager',
|
|
1517
1610
|
'derniereRequete',
|
|
@@ -133,6 +133,10 @@ ${fonctions.join('\n')}`);
|
|
|
133
133
|
const emploie = (motif) => scriptsDesPages.some((contenu) => motif.test(contenu));
|
|
134
134
|
const interactions = { pressePapier: emploie(/\b(copier|partager)\(/), fenetres: emploie(/\bfenetre\(/), questions: emploie(/\bconfirmer\(/), bas: emploie(/\bonbas:/) };
|
|
135
135
|
fichiers.set('humalis/humalis.js', humalisJs(options.source, pagesFichiers, serveur, session, bascule ? modeTheme ?? '' : undefined, usage, application.langues, interactions));
|
|
136
|
+
// « menu repliable » : un seul écouteur pour tous les menus, ceux du HTML de départ comme ceux redessinés.
|
|
137
|
+
if ([...fichiers.values()].some((contenu) => contenu.includes('humalis-menu-bouton'))) {
|
|
138
|
+
fichiers.set('humalis/humalis.js', `${fichiers.get('humalis/humalis.js')}\n${MENU_REPLIABLE_JS}`);
|
|
139
|
+
}
|
|
136
140
|
// L'observateur des animations « au defilement », seulement si une page en a : il pose humalis-js au chargement.
|
|
137
141
|
if (animeAuDefilement([...fichiers.values()])) {
|
|
138
142
|
fichiers.set('humalis/humalis.js', `${fichiers.get('humalis/humalis.js')}\n// Les animations « au defilement » attendent que l'élément entre à l'écran.\n${OBSERVATEUR_DEFILEMENT}\nobserverLeDefilement();\n`);
|
|
@@ -291,6 +295,7 @@ function humalisJs(source, pages, serveur, session, theme, elements, langues, in
|
|
|
291
295
|
'afficherMessage',
|
|
292
296
|
'planifier',
|
|
293
297
|
'mouvementReduit',
|
|
298
|
+
'themeSombre',
|
|
294
299
|
'cliquer',
|
|
295
300
|
'focaliser',
|
|
296
301
|
'retirerPremier',
|
|
@@ -544,6 +549,14 @@ ${interactions.fenetres ? ' // Une fenêtre fermée rend le focus à ce qui l
|
|
|
544
549
|
// Le réglage change pendant la visite : la page est redessinée.
|
|
545
550
|
animationsReduites?.addEventListener('change', () => actualiser());
|
|
546
551
|
|
|
552
|
+
/** « theme est sombre » : le thème affiché (choix du visiteur ou du projet, sinon le système). */
|
|
553
|
+
const schemaSombre = globalThis.matchMedia?.('(prefers-color-scheme: dark)');
|
|
554
|
+
const themeSombre = () => {
|
|
555
|
+
const choisi = document.documentElement.dataset.theme;
|
|
556
|
+
return choisi ? choisi === 'sombre' : (schemaSombre?.matches ?? false);
|
|
557
|
+
};
|
|
558
|
+
schemaSombre?.addEventListener('change', () => actualiser());
|
|
559
|
+
|
|
547
560
|
/** Les minuteries de la page existent tant que leur condition est vraie ; « arreter » les arrête pour de bon. */
|
|
548
561
|
function reglerMinuteries() {
|
|
549
562
|
for (const m of page.minuteries ?? []) {
|
|
@@ -1112,6 +1125,11 @@ const themeJs = (defaut) => `
|
|
|
1112
1125
|
const THEME_PAR_DEFAUT = ${defaut ? `'${defaut}'` : "''"};
|
|
1113
1126
|
|
|
1114
1127
|
function definirTheme(theme) {
|
|
1128
|
+
// « basculer theme » : l'inverse du thème affiché (choix du visiteur ou du projet, sinon le système).
|
|
1129
|
+
if (theme === 'bascule') {
|
|
1130
|
+
const affiche = document.documentElement.dataset.theme || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'sombre' : 'clair');
|
|
1131
|
+
theme = affiche === 'sombre' ? 'clair' : 'sombre';
|
|
1132
|
+
}
|
|
1115
1133
|
try {
|
|
1116
1134
|
if (theme === 'automatique') localStorage.removeItem('${CLE_THEME}');
|
|
1117
1135
|
else localStorage.setItem('${CLE_THEME}', theme);
|
|
@@ -1121,5 +1139,18 @@ const themeJs = (defaut) => `
|
|
|
1121
1139
|
const choisi = theme === 'automatique' ? THEME_PAR_DEFAUT : theme;
|
|
1122
1140
|
if (choisi) document.documentElement.dataset.theme = choisi;
|
|
1123
1141
|
else delete document.documentElement.dataset.theme;
|
|
1142
|
+
// « theme est sombre » est relu : la page est redessinée.
|
|
1143
|
+
actualiser();
|
|
1124
1144
|
}
|
|
1125
1145
|
`;
|
|
1146
|
+
/** « menu repliable » : le bouton « Menu » ouvre et ferme les liens ; un lien choisi referme le menu. */
|
|
1147
|
+
const MENU_REPLIABLE_JS = `// Les menus repliables : le bouton « Menu » d'un petit écran.
|
|
1148
|
+
document.addEventListener('click', (evenement) => {
|
|
1149
|
+
const cible = evenement.target instanceof Element ? evenement.target : null;
|
|
1150
|
+
const menu = cible?.closest('.humalis-menu-repliable');
|
|
1151
|
+
if (!menu || !cible.closest('.humalis-menu-bouton, a')) return;
|
|
1152
|
+
const ouvrir = cible.closest('.humalis-menu-bouton') !== null && !menu.hasAttribute('data-ouvert');
|
|
1153
|
+
menu.toggleAttribute('data-ouvert', ouvrir);
|
|
1154
|
+
menu.querySelector(':scope > .humalis-menu-bouton')?.setAttribute('aria-expanded', String(ouvrir));
|
|
1155
|
+
});
|
|
1156
|
+
`;
|
|
@@ -133,6 +133,9 @@ export function expressionJs(valeur, contexte) {
|
|
|
133
133
|
case 'mouvementReduit':
|
|
134
134
|
// « réduire les animations » du système (humalis.js), relu à chaque dessin.
|
|
135
135
|
return 'mouvementReduit()';
|
|
136
|
+
case 'themeSombre':
|
|
137
|
+
// Le thème affiché (humalis.js), relu à chaque dessin.
|
|
138
|
+
return 'themeSombre()';
|
|
136
139
|
case 'etatRequete':
|
|
137
140
|
case 'erreurRequete':
|
|
138
141
|
// Les sources ne sont pas encore produites pour le site statique : l'analyse de la cible les refuse avant.
|
|
@@ -371,8 +374,9 @@ export function evaluer(valeur, env) {
|
|
|
371
374
|
}
|
|
372
375
|
case 'utilisateur':
|
|
373
376
|
case 'etatRequete':
|
|
374
|
-
// Le HTML de départ n'écrit aucun mouvement.
|
|
377
|
+
// Le HTML de départ n'écrit aucun mouvement, et ne connaît pas le thème du visiteur.
|
|
375
378
|
case 'mouvementReduit':
|
|
379
|
+
case 'themeSombre':
|
|
376
380
|
return false;
|
|
377
381
|
case 'erreurRequete':
|
|
378
382
|
return '';
|
|
@@ -81,7 +81,7 @@ export function arbreWidget(genre, a) {
|
|
|
81
81
|
throw new Error('le choix d’un fichier n’est pas encore produit pour le site statique');
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
/** Les initiales d'un nom : «
|
|
84
|
+
/** Les initiales d'un nom : « Camille Martin » donne « CM » (le même calcul que humalis.initiales). */
|
|
85
85
|
export function initiales(nom) {
|
|
86
86
|
const mots = nom.trim().split(/\s+/).filter(Boolean);
|
|
87
87
|
return ((mots[0]?.[0] ?? '') + (mots.length > 1 ? (mots[mots.length - 1][0] ?? '') : '')).toUpperCase();
|
|
@@ -34,9 +34,30 @@ export function reglagesUtilises(codes) {
|
|
|
34
34
|
export function cssEcran(codes) {
|
|
35
35
|
const utilises = reglagesUtilises(codes);
|
|
36
36
|
const regles = [];
|
|
37
|
+
if (codes.some((c) => c.includes('humalis-accent')))
|
|
38
|
+
regles.push('.humalis-accent { color: var(--humalis-marque, currentColor); }');
|
|
39
|
+
if (codes.some((c) => c.includes('humalis-code-ligne')))
|
|
40
|
+
regles.push('.humalis-code-ligne { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; padding: 0.1em 0.25em; border-radius: 0.2em; background: rgb(127 127 127 / 0.12); }');
|
|
41
|
+
if (codes.some((c) => c.includes('humalis-bloc-code')))
|
|
42
|
+
regles.push('.humalis-bloc-code-conteneur { position: relative; max-width: 100%; overflow-x: auto; } .humalis-bloc-code { overflow-x: auto; padding: 1rem; border-radius: 0.5rem; background: rgb(127 127 127 / 0.12); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; white-space: pre; } .humalis-bloc-code-conteneur button { position: absolute; top: 0.5rem; right: 0.5rem; }');
|
|
43
|
+
if (codes.some((c) => c.includes('humalis-code-mot')))
|
|
44
|
+
regles.push('.humalis-code-mot { color: #7348c3; font-weight: 600; } .humalis-code-texte { color: #147d50; } .humalis-code-commentaire { color: #68717d; } .humalis-code-nombre { color: #b25215; }');
|
|
45
|
+
if (codes.some((c) => c.includes('humalis-tableau-conteneur')))
|
|
46
|
+
regles.push('.humalis-tableau-conteneur { max-width: 100%; overflow-x: auto; } .humalis-tableau-conteneur table { border-collapse: collapse; width: 100%; } .humalis-tableau-conteneur th, .humalis-tableau-conteneur td { border: 1px solid rgb(127 127 127 / 0.3); padding: 0.5rem; text-align: left; } .humalis-tableau-conteneur caption { text-align: left; font-weight: 700; margin-bottom: 0.5rem; }');
|
|
47
|
+
if (codes.some((c) => c.includes('humalis-liste')))
|
|
48
|
+
regles.push('.humalis-liste { padding-inline-start: 1.5rem; } .humalis-liste > li { margin-block: 0.35rem; }');
|
|
37
49
|
// L'en-tête et le pied communs : hors de la page, sur toute la largeur.
|
|
38
|
-
if (codes.some((c) => /humalis-entete\b/.test(c)))
|
|
50
|
+
if (codes.some((c) => /humalis-entete\b/.test(c))) {
|
|
39
51
|
regles.push('.humalis-entete { padding: 0.75rem 1rem; border-bottom: 1px solid rgb(138 138 144 / 0.4); }');
|
|
52
|
+
// « Aller au contenu » : hors de l'écran, il apparaît quand le clavier l'atteint.
|
|
53
|
+
regles.push('.humalis-aller-au-contenu { position: absolute; left: 0.5rem; top: -4rem; z-index: 1000; padding: 0.5rem 1rem; border-radius: 0.5rem; background: Canvas; color: CanvasText; }');
|
|
54
|
+
regles.push('.humalis-aller-au-contenu:focus { top: 0.5rem; }');
|
|
55
|
+
}
|
|
56
|
+
// « menu repliable » : sur un petit écran, les liens attendent le bouton « Menu ».
|
|
57
|
+
if (codes.some((c) => c.includes('humalis-menu-repliable'))) {
|
|
58
|
+
regles.push('.humalis-menu-bouton { display: none; }');
|
|
59
|
+
regles.push('@media (max-width: 720px) { .humalis-menu-repliable { flex-direction: column; align-items: stretch; } .humalis-menu-bouton { display: inline-flex; align-self: flex-start; } .humalis-menu-repliable:not([data-ouvert]) > :not(.humalis-menu-bouton) { display: none; } }');
|
|
60
|
+
}
|
|
40
61
|
if (codes.some((c) => /humalis-pied\b/.test(c)))
|
|
41
62
|
regles.push('.humalis-pied { padding: 1.5rem 1rem; margin-top: 2rem; border-top: 1px solid rgb(138 138 144 / 0.4); }');
|
|
42
63
|
if (utilises.size === 0 && regles.length === 0)
|
package/dist/cibles/web/ecran.js
CHANGED
|
@@ -2,6 +2,7 @@ import { verificationsDuChamp, messageEnCode, libellesEnLangues } from '../../i1
|
|
|
2
2
|
import { titreComplet, adresseImage, sansBarreFinale } from '../publication.js';
|
|
3
3
|
import { sousActions } from '../../ri/actions.js';
|
|
4
4
|
import { nomDeLaCopie, estFichierLocal } from '../../ri/medias.js';
|
|
5
|
+
import { estCombine } from '../../ri/types.js';
|
|
5
6
|
import { simplifier } from '../../analyse/elements.js';
|
|
6
7
|
import { lireCible } from '../../analyse/navigation.js';
|
|
7
8
|
import { Ecrivain } from './ecrivain.js';
|
|
@@ -10,6 +11,8 @@ import { WIDGETS_WEB } from './widgets.js';
|
|
|
10
11
|
import { baliseGraphique } from './graphique.js';
|
|
11
12
|
import { typeTS, typeDe, expression, contenuJSX, condition, ficheAvec } from './valeurs.js';
|
|
12
13
|
import { importsDesInteractions, decrireTouches } from './interactions.js';
|
|
14
|
+
import { colorerCodeHumalis } from '../commun/colorerCode.js';
|
|
15
|
+
import { attributsFrappe } from '../commun/mouvement.js';
|
|
13
16
|
/** Les sortes de champ où l'on écrit une seule ligne : la touche Entrée y envoie le formulaire. */
|
|
14
17
|
const UNE_LIGNE = new Set(['texte', 'email', 'telephone', 'nombre', 'date', 'heure', 'motDePasse', 'adresseWeb']);
|
|
15
18
|
/** Un texte écrit en clair dans du JSX : tel quel, ou entre accolades s'il contient un caractère du JSX. */
|
|
@@ -152,7 +155,9 @@ export const RENDU_WEB = {
|
|
|
152
155
|
ouvrir: (genre, options, classes, ancre) => {
|
|
153
156
|
const toutes = [`humalis-${genre}`, ...options.map((o) => `humalis-${o.nom}-${o.valeur}`), ...classes];
|
|
154
157
|
const balise = genre === 'section' ? 'section' : genre === 'menu' ? 'nav' : 'div';
|
|
155
|
-
|
|
158
|
+
// « menu repliable » : le bouton « Menu » du petit écran (src/humalis/menu.ts).
|
|
159
|
+
const repliable = options.some((o) => o.nom === 'menu' && o.valeur === 'repliable');
|
|
160
|
+
return `<${balise} className="${toutes.join(' ')}"${ancre !== undefined ? ` id=${JSON.stringify(ancre)}` : ''}${repliable ? ' onClick={menuRepliable}' : ''}>${repliable ? '<button type="button" className="humalis-menu-bouton" aria-expanded="false">Menu</button>' : ''}`;
|
|
156
161
|
},
|
|
157
162
|
fermer: (genre) => (genre === 'section' ? '</section>' : genre === 'menu' ? '</nav>' : '</div>'),
|
|
158
163
|
},
|
|
@@ -204,6 +209,7 @@ class GenerateurEcran {
|
|
|
204
209
|
chemins;
|
|
205
210
|
application;
|
|
206
211
|
parametres;
|
|
212
|
+
listeEnCours;
|
|
207
213
|
contexte = { types: new Map(), champs: new Map(), sortes: new Map() };
|
|
208
214
|
/** Les champs qui ont une sorte ou des options, et le libellé de chaque champ (pour les messages). */
|
|
209
215
|
saisies = new Map();
|
|
@@ -325,7 +331,7 @@ class GenerateurEcran {
|
|
|
325
331
|
for (const g of this.ecran.gestionnaires)
|
|
326
332
|
this.enregistrer(g, []);
|
|
327
333
|
const composant = pascal(this.ecran.nom);
|
|
328
|
-
const { titre, description } = titreEtDescription(this.ecran);
|
|
334
|
+
const { titre, description } = titreEtDescription(this.ecran, this.application.langues);
|
|
329
335
|
// Un composant reçoit ses paramètres comme des propriétés React, et ne
|
|
330
336
|
// change pas le titre de la page qui l'affiche.
|
|
331
337
|
const signature = this.parametres
|
|
@@ -392,7 +398,9 @@ class GenerateurEcran {
|
|
|
392
398
|
const lues = [...new Set([...code.matchAll(/\bactuel\.current\.(\w+)/g)].map((m) => m[1]))];
|
|
393
399
|
code = code.replace(' /*ACTUEL*/\n', lues.length > 0 ? ` const actuel = useActuel({ ${lues.join(', ')} });\n` : '');
|
|
394
400
|
// « mouvement est reduit » : suivi en direct (src/humalis/temps.ts).
|
|
395
|
-
code = code.replace(' /*MOUVEMENT*/\n', /\bmouvementReduit\b/.test(code) ? ' const mouvementReduit = useMouvementReduit();\n' : '')
|
|
401
|
+
code = code.replace(' /*MOUVEMENT*/\n', (/\bmouvementReduit\b/.test(code) ? ' const mouvementReduit = useMouvementReduit();\n' : '') +
|
|
402
|
+
// « theme est sombre » : le thème affiché, suivi en direct.
|
|
403
|
+
(/\bthemeSombre\b/.test(code) ? ' const themeSombre = useThemeSombre();\n' : ''));
|
|
396
404
|
// Sur le téléphone, « confirmer » vient d'un crochet : la question est une fenêtre de l'application.
|
|
397
405
|
code = code.replace(' /*CONFIRMER*/\n', this.rendu.confirmationParCrochet && /\bconfirmer\(/.test(code) ? ' const confirmer = useConfirmer();\n' : '');
|
|
398
406
|
code = code.replace(' /*PLANIFIER*/\n', (/\bplanifier\(/.test(code) ? ' const planifier = usePlanifier();\n' : '') + (/\bderniereRequete\(/.test(code) ? ' const derniereRequete = useDerniere();\n' : ''));
|
|
@@ -407,13 +415,16 @@ class GenerateurEcran {
|
|
|
407
415
|
}
|
|
408
416
|
/** Le premier titre (ou texte) de la page, s'il a des versions : il suit la langue (« t({ fr, en }) »). */
|
|
409
417
|
traduit(genre) {
|
|
410
|
-
const valeur = this.application.langues ? premiereValeur(genre, this.ecran.vue) : undefined;
|
|
418
|
+
const valeur = this.application.langues ? premiereValeur(genre, this.ecran.vue, this.application.langues) : undefined;
|
|
411
419
|
return valeur?.type === 'texte' && valeur.versions ? expression(valeur, this.contexte) : undefined;
|
|
412
420
|
}
|
|
413
421
|
// ─── Recensement : champs, cibles des événements, besoins ───────────────
|
|
414
422
|
recenser(noeuds, boucles, formulaire) {
|
|
415
423
|
for (const noeud of noeuds) {
|
|
416
424
|
switch (noeud.type) {
|
|
425
|
+
case 'codeDocumentation':
|
|
426
|
+
this.besoins.messages = true;
|
|
427
|
+
break;
|
|
417
428
|
case 'element':
|
|
418
429
|
if (noeud.genre === 'champ') {
|
|
419
430
|
const libelle = texteDeBase(noeud.contenu) ?? noeud.id;
|
|
@@ -834,8 +845,8 @@ class GenerateurEcran {
|
|
|
834
845
|
const code = expression(valeur, this.contexte);
|
|
835
846
|
return complet && site?.nom ? `${code} + ${suffixe()}` : code;
|
|
836
847
|
};
|
|
837
|
-
const titreCode = enCode(apercu?.titre ?? premiereValeur('titre', this.ecran.vue), true) ?? titreLitteral(titre);
|
|
838
|
-
const descriptionCode = enCode(apercu?.description ?? premiereValeur('texte', this.ecran.vue)) ??
|
|
848
|
+
const titreCode = enCode(apercu?.titre ?? premiereValeur('titre', this.ecran.vue, this.application.langues), true) ?? titreLitteral(titre);
|
|
849
|
+
const descriptionCode = enCode(apercu?.description ?? premiereValeur('texte', this.ecran.vue, this.application.langues)) ??
|
|
839
850
|
(site?.descriptionLangues ? messageEnCode(site.descriptionLangues, chaine) : chaine(site?.description ?? description));
|
|
840
851
|
const chemin = this.chemins.get(this.ecran.nom) ?? `/${simplifier(this.ecran.nom)}`;
|
|
841
852
|
const image = apercu?.image ?? (site?.image !== undefined ? { type: 'texte', parties: [{ texte: site.image }] } : undefined);
|
|
@@ -1017,7 +1028,7 @@ class GenerateurEcran {
|
|
|
1017
1028
|
break;
|
|
1018
1029
|
case 'copie':
|
|
1019
1030
|
// « copier code » : le résultat est dit à la personne, sauf « sans message ».
|
|
1020
|
-
e.ligne(`await copier(${this.enTexte(action.valeur)}${action.message === null ? '' : `, afficherMessage${action.message === 'Copié' ? '' : `, ${chaine(action.message)}`}`});`);
|
|
1031
|
+
e.ligne(`await copier(${this.enTexte(action.valeur)}${action.message === null ? '' : `, afficherMessage${action.texte ? `, ${x(action.texte)}` : action.message === 'Copié' ? '' : `, ${chaine(action.message)}`}`});`);
|
|
1021
1032
|
break;
|
|
1022
1033
|
case 'partage': {
|
|
1023
1034
|
// La feuille de partage du système ; sans elle, le lien (ou le texte) est copié.
|
|
@@ -1437,6 +1448,50 @@ class GenerateurEcran {
|
|
|
1437
1448
|
ecrireVue(e, noeuds) {
|
|
1438
1449
|
for (const noeud of noeuds) {
|
|
1439
1450
|
switch (noeud.type) {
|
|
1451
|
+
case 'tableauDocumentation':
|
|
1452
|
+
if (this.application.rendu) {
|
|
1453
|
+
const mobile = (valeur) => contenuJSX(valeur, this.contexte)
|
|
1454
|
+
.replace(/<code className="humalis-code-ligne">/g, '<Text style={{ fontFamily: "monospace" }}>')
|
|
1455
|
+
.replace(/<span className="humalis-accent">/g, '<Text style={s.accent}>')
|
|
1456
|
+
.replace(/<\/(code|span)>/g, '</Text>');
|
|
1457
|
+
e.ligne('<ScrollView horizontal><View>');
|
|
1458
|
+
e.ligne(`<Text>${mobile(noeud.legende)}</Text>`);
|
|
1459
|
+
for (const ligne of [noeud.colonnes, ...noeud.rangees]) {
|
|
1460
|
+
e.ligne('<View style={{ flexDirection: "row" }}>');
|
|
1461
|
+
for (const cellule of ligne)
|
|
1462
|
+
e.ligne(`<Text style={{ minWidth: 120 }}>${mobile(cellule)}</Text>`);
|
|
1463
|
+
e.ligne('</View>');
|
|
1464
|
+
}
|
|
1465
|
+
e.ligne('</View></ScrollView>');
|
|
1466
|
+
}
|
|
1467
|
+
else {
|
|
1468
|
+
e.ligne('<div className="humalis-tableau-conteneur"><table><caption>');
|
|
1469
|
+
e.ligne(contenuJSX(noeud.legende, this.contexte));
|
|
1470
|
+
e.ligne('</caption><thead><tr>');
|
|
1471
|
+
for (const cellule of noeud.colonnes)
|
|
1472
|
+
e.ligne(`<th scope="col">${contenuJSX(cellule, this.contexte)}</th>`);
|
|
1473
|
+
e.ligne('</tr></thead><tbody>');
|
|
1474
|
+
for (const ligne of noeud.rangees) {
|
|
1475
|
+
e.ligne('<tr>');
|
|
1476
|
+
for (const cellule of ligne)
|
|
1477
|
+
e.ligne(`<td>${contenuJSX(cellule, this.contexte)}</td>`);
|
|
1478
|
+
e.ligne('</tr>');
|
|
1479
|
+
}
|
|
1480
|
+
e.ligne('</tbody></table></div>');
|
|
1481
|
+
}
|
|
1482
|
+
break;
|
|
1483
|
+
case 'codeDocumentation':
|
|
1484
|
+
if (this.application.rendu)
|
|
1485
|
+
e.ligne(`<ScrollView horizontal><Text style={{ fontFamily: 'monospace' }}>{${chaine(noeud.code)}}</Text></ScrollView>`);
|
|
1486
|
+
else {
|
|
1487
|
+
const copie = this.application.langues ? `t({ fr: 'Copié', en: 'Copied' })` : `'Copié'`;
|
|
1488
|
+
const libelle = this.application.langues ? `{t({ fr: 'Copier', en: 'Copy' })}` : 'Copier';
|
|
1489
|
+
const frappe = attributsFrappe(noeud);
|
|
1490
|
+
const animation = frappe ? ` ${frappe.classe}` : '';
|
|
1491
|
+
const marques = frappe ? ` data-humalis-frappe="${frappe.frappe}" data-humalis-frappe-cle="${frappe.cle}"` : '';
|
|
1492
|
+
e.ligne(`<div className="humalis-bloc-code-conteneur"><pre className="humalis-bloc-code${animation}" data-langage=${chaine(noeud.langage ?? '')}${marques}><code dangerouslySetInnerHTML={{ __html: ${chaine(colorerCodeHumalis(noeud.code, noeud.langage))} }} /></pre><button type="button" onClick={() => void copier(${chaine(noeud.code)}, afficherMessage, ${copie})}>${libelle}</button></div>`);
|
|
1493
|
+
}
|
|
1494
|
+
break;
|
|
1440
1495
|
case 'element':
|
|
1441
1496
|
this.ecrireElement(e, noeud);
|
|
1442
1497
|
break;
|
|
@@ -1500,7 +1555,13 @@ class GenerateurEcran {
|
|
|
1500
1555
|
e.ligne(`{${liste}.map((${variable}, ${index}) => (`);
|
|
1501
1556
|
e.indenter(() => {
|
|
1502
1557
|
e.ligne(`<Fragment key={${index}}>`);
|
|
1503
|
-
e.indenter(() =>
|
|
1558
|
+
e.indenter(() => {
|
|
1559
|
+
if (this.listeEnCours)
|
|
1560
|
+
e.ligne(this.application.rendu ? `<View style={{ flexDirection: "row" }}><Text>${this.listeEnCours === 'liste' ? '•' : `{${index} + 1}.`}</Text>` : '<li>');
|
|
1561
|
+
this.ecrireVue(e, contenu);
|
|
1562
|
+
if (this.listeEnCours)
|
|
1563
|
+
e.ligne(this.application.rendu ? '</View>' : '</li>');
|
|
1564
|
+
});
|
|
1504
1565
|
e.ligne('</Fragment>');
|
|
1505
1566
|
});
|
|
1506
1567
|
e.ligne('))}');
|
|
@@ -1541,6 +1602,40 @@ class GenerateurEcran {
|
|
|
1541
1602
|
break;
|
|
1542
1603
|
}
|
|
1543
1604
|
case 'bloc':
|
|
1605
|
+
if (noeud.genre === 'liste' || noeud.genre === 'listeNumerotee') {
|
|
1606
|
+
if (this.application.rendu) {
|
|
1607
|
+
e.ligne('<View>');
|
|
1608
|
+
e.indenter(() => noeud.corps.forEach((enfant, index) => {
|
|
1609
|
+
if (enfant.type === 'repetition') {
|
|
1610
|
+
this.listeEnCours = noeud.genre;
|
|
1611
|
+
this.ecrireVue(e, [enfant]);
|
|
1612
|
+
this.listeEnCours = undefined;
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
e.ligne('<View style={{ flexDirection: "row" }}>');
|
|
1616
|
+
e.ligne(`<Text>${noeud.genre === 'liste' ? '•' : `${index + 1}.`}</Text>`);
|
|
1617
|
+
this.ecrireVue(e, [enfant]);
|
|
1618
|
+
e.ligne('</View>');
|
|
1619
|
+
}));
|
|
1620
|
+
e.ligne('</View>');
|
|
1621
|
+
break;
|
|
1622
|
+
}
|
|
1623
|
+
const balise = noeud.genre === 'liste' ? 'ul' : 'ol';
|
|
1624
|
+
e.ligne(`<${balise} className="humalis-liste">`);
|
|
1625
|
+
e.indenter(() => noeud.corps.forEach((enfant) => {
|
|
1626
|
+
if (enfant.type === 'repetition') {
|
|
1627
|
+
this.listeEnCours = noeud.genre;
|
|
1628
|
+
this.ecrireVue(e, [enfant]);
|
|
1629
|
+
this.listeEnCours = undefined;
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
e.ligne('<li>');
|
|
1633
|
+
e.indenter(() => this.ecrireVue(e, [enfant]));
|
|
1634
|
+
e.ligne('</li>');
|
|
1635
|
+
}));
|
|
1636
|
+
e.ligne(`</${balise}>`);
|
|
1637
|
+
break;
|
|
1638
|
+
}
|
|
1544
1639
|
if (COMPOSITES.has(noeud.genre)) {
|
|
1545
1640
|
this.ecrireComposite(e, noeud);
|
|
1546
1641
|
break;
|
|
@@ -1926,24 +2021,72 @@ function modifiees(actions, noms = new Set()) {
|
|
|
1926
2021
|
}
|
|
1927
2022
|
return noms;
|
|
1928
2023
|
}
|
|
1929
|
-
/**
|
|
1930
|
-
|
|
2024
|
+
/**
|
|
2025
|
+
* « si langue est anglais » : la branche montrée dans la langue `code` ;
|
|
2026
|
+
* rien pour une condition qui ne porte pas sur la langue.
|
|
2027
|
+
*/
|
|
2028
|
+
function brancheDeLangue(noeud, code) {
|
|
2029
|
+
if (noeud.type !== 'condition')
|
|
2030
|
+
return undefined;
|
|
2031
|
+
const test = noeud.test;
|
|
2032
|
+
if (estCombine(test))
|
|
2033
|
+
return undefined;
|
|
2034
|
+
if (test.gauche.type !== 'variable' || test.gauche.nom !== 'langue' || (test.operateur !== 'egal' && test.operateur !== 'different'))
|
|
2035
|
+
return undefined;
|
|
2036
|
+
const visee = test.droite && texteDeBase(test.droite);
|
|
2037
|
+
if (visee === undefined)
|
|
2038
|
+
return undefined;
|
|
2039
|
+
const vraie = (visee === code) !== (test.operateur === 'different') !== Boolean(test.negation);
|
|
2040
|
+
return vraie ? noeud.alors : noeud.sinon;
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* La première valeur d'un élément de ce genre, affiché sans condition (même
|
|
2044
|
+
* dans un bloc de mise en page), dans la langue `code` : avec des langues,
|
|
2045
|
+
* une branche « si langue est … » compte.
|
|
2046
|
+
*/
|
|
2047
|
+
function premiereValeurEn(genre, noeuds, code) {
|
|
1931
2048
|
for (const n of noeuds) {
|
|
1932
2049
|
if (n.type === 'element' && n.genre === genre)
|
|
1933
2050
|
return n.contenu;
|
|
1934
|
-
|
|
1935
|
-
|
|
2051
|
+
const corps = n.type === 'bloc' ? n.corps : code !== undefined ? brancheDeLangue(n, code) : undefined;
|
|
2052
|
+
if (corps) {
|
|
2053
|
+
const trouvee = premiereValeurEn(genre, corps, code);
|
|
1936
2054
|
if (trouvee)
|
|
1937
2055
|
return trouvee;
|
|
1938
2056
|
}
|
|
1939
2057
|
}
|
|
1940
2058
|
return undefined;
|
|
1941
2059
|
}
|
|
2060
|
+
/**
|
|
2061
|
+
* La première valeur d'un élément de ce genre. Avec des langues, celle de la
|
|
2062
|
+
* langue de base ; si les branches « si langue est … » en montrent une autre
|
|
2063
|
+
* dans une autre langue, elle devient la version de cette langue.
|
|
2064
|
+
*/
|
|
2065
|
+
function premiereValeur(genre, noeuds, langues) {
|
|
2066
|
+
if (!langues || langues.length === 0)
|
|
2067
|
+
return premiereValeurEn(genre, noeuds);
|
|
2068
|
+
const [base, ...autres] = langues;
|
|
2069
|
+
const valeur = premiereValeurEn(genre, noeuds, base.code);
|
|
2070
|
+
if (valeur?.type !== 'texte')
|
|
2071
|
+
return valeur;
|
|
2072
|
+
const versions = autres.flatMap(({ code }) => {
|
|
2073
|
+
const autre = premiereValeurEn(genre, noeuds, code);
|
|
2074
|
+
if (autre === valeur || autre?.type !== 'texte')
|
|
2075
|
+
return [];
|
|
2076
|
+
return [{ code, parties: autre.versions?.find((v) => v.code === code)?.parties ?? autre.parties }];
|
|
2077
|
+
});
|
|
2078
|
+
if (versions.length === 0)
|
|
2079
|
+
return valeur;
|
|
2080
|
+
// Les versions écrites avec « en anglais » restent, celles des branches les complètent.
|
|
2081
|
+
const ecrites = (valeur.versions ?? []).filter((v) => v.code !== base.code && !versions.some((b) => b.code === v.code));
|
|
2082
|
+
return { ...valeur, versions: [{ code: base.code, parties: valeur.parties }, ...ecrites, ...versions] };
|
|
2083
|
+
}
|
|
1942
2084
|
/**
|
|
1943
2085
|
* Le titre et la description de la page (décision W7) : le premier titre
|
|
1944
2086
|
* et le premier texte écrits en clair, à défaut le nom de la page.
|
|
1945
2087
|
*/
|
|
1946
|
-
export function titreEtDescription(ecran) {
|
|
2088
|
+
export function titreEtDescription(ecran, langues) {
|
|
2089
|
+
const base = langues?.[0]?.code;
|
|
1947
2090
|
const premier = (genre, noeuds) => {
|
|
1948
2091
|
for (const n of noeuds) {
|
|
1949
2092
|
if (n.type === 'element' && n.genre === genre) {
|
|
@@ -1951,9 +2094,11 @@ export function titreEtDescription(ecran) {
|
|
|
1951
2094
|
if (texte)
|
|
1952
2095
|
return texte;
|
|
1953
2096
|
}
|
|
1954
|
-
// Toujours affiché : le contenu d'un bloc de mise en page compte aussi
|
|
1955
|
-
|
|
1956
|
-
|
|
2097
|
+
// Toujours affiché : le contenu d'un bloc de mise en page compte aussi, et
|
|
2098
|
+
// avec des langues, la branche « si langue est … » de la langue de base.
|
|
2099
|
+
const corps = n.type === 'bloc' ? n.corps : base !== undefined ? brancheDeLangue(n, base) : undefined;
|
|
2100
|
+
if (corps) {
|
|
2101
|
+
const texte = premier(genre, corps);
|
|
1957
2102
|
if (texte)
|
|
1958
2103
|
return texte;
|
|
1959
2104
|
}
|