humalis 1.3.1 → 1.4.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.
@@ -19,16 +19,9 @@ export function colorerCodeHumalis(code, langage) {
19
19
  continue;
20
20
  const debut = debuts[token.ligne - 1] + token.colonne - 1;
21
21
  let fin = debut + (token.original ?? token.valeur).length;
22
- if (token.type === 'CHAINE') {
23
- fin = debut + 1;
24
- while (fin < code.length) {
25
- if (code[fin] === '"' && code[fin - 1] !== '\\') {
26
- fin++;
27
- break;
28
- }
29
- fin++;
30
- }
31
- }
22
+ // Un texte s'arrête où le lexer l'a fermé (le code entre accents graves et {code "…"} compris).
23
+ if (token.type === 'CHAINE' && token.longueur !== undefined)
24
+ fin = debut + token.longueur;
32
25
  plages.push({ debut, fin, classe: token.type === 'MOT_CLE' ? 'mot' : token.type === 'CHAINE' ? 'texte' : 'nombre' });
33
26
  }
34
27
  for (let index = 0; index < lignes.length; index++) {
@@ -286,6 +286,13 @@ export const ENTETE_WEB = `// Produit par Humalis : la hauteur de l'en-tête, po
286
286
  ${SUIVI_ENTETE.replace('function suivreLaHauteurDeLEntete() {', 'export function suivreLaHauteurDeLEntete(): void {').replace("const entete = () => document.querySelector(", "const entete = () => document.querySelector<HTMLElement>(")}
287
287
  `;
288
288
  /** Le prérendu capture la page après le script : il retire les marques du mouvement (rien de caché sans JavaScript). */
289
- export const NETTOYAGE_PRERENDU = ` // Le script du mouvement marque la page (humalis-js, data-humalis-vu) : le HTML prérendu ne garde aucune de ces marques.
290
- transformHtml: (html) => html.replace(/(<html[^>]*class="[^"]*)\\bhumalis-js\\b/, '$1').replace(/ data-humalis-vu=""/g, '').replace(/ data-humalis-tape="[a-z-]*"/g, '').replace(/<code class="humalis-frappe-copie"[\\s\\S]*?<\\/code>/g, ''),
289
+ /**
290
+ * Le nettoyage du HTML prérendu. Les préchargements que Vite ajoute en chargeant une page
291
+ * (« modulepreload » avec as="script", adresse complète) ne servent à rien dans la photo : le
292
+ * script principal les redemande. Avec le mouvement, le script marque aussi la page
293
+ * (humalis-js, data-humalis-vu) : le HTML prérendu ne garde aucune de ces marques.
294
+ */
295
+ export const nettoyagePrerendu = (mouvement) => ` // La photo de chaque page ne garde que ce que le navigateur doit lire au départ.
296
+ transformHtml: (html) => html.replace(/<link rel="modulepreload" as="script"[^>]*>/g, '')${mouvement ? NETTOYAGE_MOUVEMENT : ''},
291
297
  `;
298
+ const NETTOYAGE_MOUVEMENT = `.replace(/(<html[^>]*class="[^"]*)\\bhumalis-js\\b/, '$1').replace(/ data-humalis-vu=""/g, '').replace(/ data-humalis-tape="[a-z-]*"/g, '').replace(/<code class="humalis-frappe-copie"[\\s\\S]*?<\\/code>/g, '')`;
@@ -142,10 +142,11 @@ export function cssEcran(codes) {
142
142
  break;
143
143
  case 'fixe':
144
144
  // « entete avec fixe » : l'en-tête ne bouge pas ; sur le web, la racine devient la zone qui
145
- // défile, sous l'en-tête ; sur le statique, la page laisse la place de l'en-tête.
145
+ // défile, sous l'en-tête (sans glisser de côté : un décor qui dépasse ne la déplace pas) ;
146
+ // sur le statique, la page laisse la place de l'en-tête.
146
147
  regles.push(`header${classe} { position: fixed; top: 0; left: 0; right: 0; z-index: 50; background: var(--humalis-page); }`);
147
148
  regles.push('html:has(#racine > .humalis-fixe-oui), html:has(#racine > .humalis-fixe-oui) body { height: 100%; overflow: hidden; }');
148
- regles.push('#racine:has(> .humalis-fixe-oui) { position: fixed; top: var(--humalis-hauteur-entete, 0px); left: 0; right: 0; bottom: 0; overflow-y: auto; overscroll-behavior: contain; }');
149
+ regles.push('#racine:has(> .humalis-fixe-oui) { position: fixed; top: var(--humalis-hauteur-entete, 0px); left: 0; right: 0; bottom: 0; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; }');
149
150
  regles.push('body:has(header.humalis-fixe-oui) { padding-top: var(--humalis-hauteur-entete, 0px); }');
150
151
  break;
151
152
  }
@@ -3,7 +3,7 @@ import { robotsTxt, manifeste, fichiersDuSite, balisesIcone, estExterne } from '
3
3
  import { sousActions } from '../../ri/actions.js';
4
4
  import { cssEcran } from './css-ecran.js';
5
5
  import { cssElements } from './css-elements.js';
6
- import { cssMouvement, animeAuDefilement, MOUVEMENT_WEB, NETTOYAGE_PRERENDU, ENTETE_WEB, suitLEntete } from '../commun/mouvement.js';
6
+ import { cssMouvement, animeAuDefilement, MOUVEMENT_WEB, nettoyagePrerendu, ENTETE_WEB, suitLEntete } from '../commun/mouvement.js';
7
7
  import { fichiersDesElements, dependancesDesElements, dependancesDeDeveloppement } from './widgets.js';
8
8
  import { fichiersGraphique, cssGraphique } from './graphique.js';
9
9
  import { cssTheme, changeLeTheme, modeParDefaut, themeTs, SCRIPT_THEME } from './theme.js';
@@ -437,7 +437,7 @@ ${lignesRoutes}
437
437
  waitFor: 'meta[name="description"]',
438
438
  // Les adresses des « envoyer requete » du programme, neutralisées pendant le prérendu.
439
439
  apiPatterns: [${motifs}],
440
- ${mouvement ? NETTOYAGE_PRERENDU : ''}};
440
+ ${nettoyagePrerendu(mouvement)}};
441
441
  `;
442
442
  }
443
443
  /** Disposition.tsx : l'en-tête et le pied communs autour des pages, qui restent montés quand on change de page. */
@@ -512,7 +512,21 @@ function lignesDeRoute(r, toutes, niveau, cheminParent) {
512
512
  return lignes;
513
513
  }
514
514
  function mainTsx(routes, feuilles, partage, session, introuvable, dispositions = {}, defilement = false, mouvement = false, entete = false) {
515
- const imports = routes.map((r) => `import ${r.composant} from './pages/${r.composant}';`).join('\n');
515
+ // Chaque page dans son propre fichier, chargé à sa première visite ; celle de l'adresse ouverte
516
+ // (et son parent, pour une sous-page) est chargée avant l'affichage, pour ne jamais montrer d'écran vide.
517
+ const aDesFils = (r) => routes.some((f) => f.parent === r.nom);
518
+ const imports = routes.length === 0 ? '' : `const chargeurs = {
519
+ ${routes.map((r) => ` ${r.composant}: () => import('./pages/${r.composant}'),`).join('\n')}
520
+ };
521
+ const adresses: [keyof typeof chargeurs, string, boolean][] = [
522
+ ${routes.map((r) => ` [${JSON.stringify(r.composant)}, ${JSON.stringify(r.chemin)}, ${aDesFils(r)}],`).join('\n')}
523
+ ];
524
+ const ouvertes = new Map<string, ComponentType>();
525
+ for (const [nom, chemin, parent] of adresses) {
526
+ if (matchPath({ path: chemin, end: !parent }, location.pathname)) ouvertes.set(nom, (await chargeurs[nom]()).default);
527
+ }
528
+ const page = (nom: keyof typeof chargeurs): ComponentType => ouvertes.get(nom) ?? lazy(chargeurs[nom]);
529
+ ${routes.map((r) => `const ${r.composant} = page(${JSON.stringify(r.composant)});`).join('\n')}`;
516
530
  // Les pages qui ont le même en-tête et le même pied partagent une disposition ; une sous-page suit son parent.
517
531
  const groupes = new Map();
518
532
  for (const r of routes.filter((r) => r.parent === undefined)) {
@@ -530,25 +544,26 @@ function mainTsx(routes, feuilles, partage, session, introuvable, dispositions =
530
544
  for (const nom of [...enveloppes].reverse())
531
545
  routesTsx = `<${nom}>\n${routesTsx.replace(/^/gm, ' ')}\n</${nom}>`;
532
546
  routesTsx = routesTsx.replace(/^/gm, ' ');
533
- return `import { StrictMode } from 'react';
547
+ return `import { ${routes.length === 0 ? '' : 'lazy, '}StrictMode, Suspense${routes.length === 0 ? '' : ', type ComponentType'} } from 'react';
534
548
  import { createRoot } from 'react-dom/client';
535
- import { BrowserRouter, Route, Routes } from 'react-router';
549
+ import { BrowserRouter, Route, Routes${routes.length === 0 ? '' : ', matchPath'} } from 'react-router';
536
550
  import { Messages } from './humalis/messages';${defilement ? "\nimport { Defilement } from './humalis/navigation';" : ''}
537
551
  ${session ? "import { Session } from './humalis/session';\n" : ''}${partage ? "import { Partage } from './humalis/partage';\n" : ''}${routes.some((r) => r.reservee) ? "import { Reservee } from './humalis/reservee';\n" : ''}${introuvable ? `import ${introuvable} from './pages/${introuvable}';` : "import Introuvable from './Introuvable';"}
538
- ${dispositions.entete || dispositions.pied ? "import Disposition from './Disposition';\n" : ''}${imports}
539
- import './humalis/styles.css';
552
+ ${dispositions.entete || dispositions.pied ? "import Disposition from './Disposition';\n" : ''}import './humalis/styles.css';
540
553
  ${mouvement ? "import { observerLeDefilement } from './humalis/mouvement';\n" : ''}${entete ? "import { suivreLaHauteurDeLEntete } from './humalis/entete';\n" : ''}${feuilles.map((f) => `import './styles/${f}';\n`).join('')}
541
554
  // Un hébergeur statique sert /documentation/index.html pour /documentation/ :
542
555
  // le routeur lit l'adresse sans « index.html », comme le dossier.
543
556
  if (/\\/index\\.html$/.test(location.pathname)) {
544
557
  history.replaceState(history.state, '', location.pathname.replace(/index\\.html$/, '') + location.search + location.hash);
545
558
  }
546
-
559
+ ${imports ? `\n${imports}\n` : ''}
547
560
  createRoot(document.getElementById('racine')!).render(
548
561
  <StrictMode>
549
562
  <BrowserRouter>
550
563
  ${defilement ? ' <Defilement />\n' : ''} <Messages>
551
- ${routesTsx}
564
+ <Suspense fallback={null}>
565
+ ${routesTsx.replace(/^/gm, ' ')}
566
+ </Suspense>
552
567
  </Messages>
553
568
  </BrowserRouter>
554
569
  </StrictMode>,
@@ -30,6 +30,14 @@ function motColle(ligne, debut, premier) {
30
30
  }
31
31
  return null;
32
32
  }
33
+ /**
34
+ * L'accent grave qui ferme un `code` commencé à `debut` (juste après le premier), sur la
35
+ * même ligne et après au moins un caractère ; -1 sinon : un accent grave seul reste un caractère.
36
+ */
37
+ function finDuCode(texte, debut) {
38
+ const fin = texte.indexOf('`', debut);
39
+ return fin > debut ? fin : -1;
40
+ }
33
41
  /**
34
42
  * Le guillemet qui ferme un {code "…"} ou un {accent "…"} commencé à `debut` : le premier « "} »
35
43
  * où les accolades du contenu sont équilibrées ({code "{accent "…"}"}), sinon le premier « "} »
@@ -52,6 +60,17 @@ function decouperChaine(valeur, ligne, colonne) {
52
60
  let tampon = '';
53
61
  let i = 0;
54
62
  while (i < valeur.length) {
63
+ // `titre "Bonjour"` : le code d'une phrase, recopié tel quel, comme {code "…"}.
64
+ const finCode = valeur[i] === '`' ? finDuCode(valeur, i + 1) : -1;
65
+ if (finCode >= 0) {
66
+ if (tampon !== '') {
67
+ parties.push({ type: 'texte', valeur: tampon });
68
+ tampon = '';
69
+ }
70
+ parties.push({ type: 'code', valeur: valeur.slice(i + 1, finCode) });
71
+ i = finCode + 1;
72
+ continue;
73
+ }
55
74
  if (valeur[i] === '{') {
56
75
  if (tampon !== '') {
57
76
  parties.push({ type: 'texte', valeur: tampon });
@@ -223,6 +242,13 @@ export function analyser(source) {
223
242
  let valeur = '';
224
243
  let profondeur = 0;
225
244
  while (i < ligne.length && (ligne[i] !== '"' || (profondeur > 0 && ligne.indexOf('}', i + 1) >= 0))) {
245
+ // `…` : le code d'une phrase, recopié tel quel jusqu'à l'accent grave qui le ferme.
246
+ const finCode = ligne[i] === '`' ? finDuCode(ligne, i + 1) : -1;
247
+ if (finCode >= 0) {
248
+ valeur += ligne.slice(i, finCode + 1);
249
+ i = finCode + 1;
250
+ continue;
251
+ }
226
252
  // {code "…"} et {accent "…"} : recopiés tels quels jusqu'au premier « "} »
227
253
  // (accolades, guillemets et antislashs compris).
228
254
  const morceau = /^\{(code|accent)\s*"/.exec(ligne.slice(i));
@@ -257,6 +283,7 @@ export function analyser(source) {
257
283
  valeur,
258
284
  ligne: numLigne + 1,
259
285
  colonne: debut + 1,
286
+ longueur: i - debut,
260
287
  parties: decouperChaine(valeur, numLigne + 1, debut + 1),
261
288
  });
262
289
  continue;