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
|
@@ -39,12 +39,15 @@ export function fichiersDesInteractions(codes, plateforme = 'web') {
|
|
|
39
39
|
fichiers.set('voir-plus.ts', plateforme === 'web' ? VOIR_PLUS_TS : VOIR_PLUS_MOBILE);
|
|
40
40
|
if (emploie('deplacer'))
|
|
41
41
|
fichiers.set('deplacer.ts', plateforme === 'web' ? DEPLACER_TS : DEPLACER_MOBILE);
|
|
42
|
+
if (emploie('menu'))
|
|
43
|
+
fichiers.set('menu.ts', MENU_TS);
|
|
42
44
|
return fichiers;
|
|
43
45
|
}
|
|
44
46
|
/** Les outils de chaque fichier, pour écrire les imports d'une page. */
|
|
45
47
|
export const OUTILS_INTERACTIONS = {
|
|
46
|
-
temps: ['useActuel', 'useMinuterie', 'usePlanifier', 'useMouvementReduit', 'useQuandChange', 'useQuandPause', 'useDerniere'],
|
|
48
|
+
temps: ['useActuel', 'useMinuterie', 'usePlanifier', 'useMouvementReduit', 'useThemeSombre', 'useQuandChange', 'useQuandPause', 'useDerniere'],
|
|
47
49
|
'presse-papier': ['copier', 'partager', 'adresseAbsolue'],
|
|
50
|
+
menu: ['menuRepliable'],
|
|
48
51
|
clavier: ['useRaccourcis', 'correspond', 'cliquer', 'focaliser'],
|
|
49
52
|
confirmation: ['confirmer', 'useConfirmer'],
|
|
50
53
|
fenetre: ['Fenetre'],
|
|
@@ -57,7 +60,9 @@ export function importsDesInteractions(code, dossier, outils = OUTILS_INTERACTIO
|
|
|
57
60
|
for (const [fichier, noms] of Object.entries(outils)) {
|
|
58
61
|
// « confirmer » vient de useConfirmer() quand la page le déclare (le téléphone).
|
|
59
62
|
const declares = new Set([...code.matchAll(/\bconst (\w+) = use\w+\(/g)].map((m) => m[1]));
|
|
60
|
-
|
|
63
|
+
// Fenetre est un composant ; menuRepliable est passé tel quel à onClick.
|
|
64
|
+
const motif = (n) => (n === 'Fenetre' ? '<Fenetre\\b' : n === 'menuRepliable' ? '\\{menuRepliable\\}' : `\\b${n}\\(`);
|
|
65
|
+
const employes = noms.filter((n) => !declares.has(n) && new RegExp(motif(n)).test(code));
|
|
61
66
|
if (employes.length)
|
|
62
67
|
lignes.push(`import { ${employes.join(', ')} } from '${dossier}/${fichier}';`);
|
|
63
68
|
}
|
|
@@ -71,7 +76,7 @@ export function importsDesInteractions(code, dossier, outils = OUTILS_INTERACTIO
|
|
|
71
76
|
function tempsTs(plateforme) {
|
|
72
77
|
const web = plateforme === 'web';
|
|
73
78
|
return `import { useCallback, useEffect, useLayoutEffect, useRef, useState${web ? ', useSyncExternalStore' : ''} } from 'react';
|
|
74
|
-
${web ? '' : "import { AccessibilityInfo, AppState } from 'react-native';\n"}
|
|
79
|
+
${web ? '' : "import { AccessibilityInfo, AppState, useColorScheme } from 'react-native';\n"}
|
|
75
80
|
/**
|
|
76
81
|
* Les dernières valeurs de la page, pour ce qui s'exécute plus tard (une
|
|
77
82
|
* minuterie lancée par un clic, le bouton d'un message) : sans cela, il
|
|
@@ -195,6 +200,28 @@ export function useMouvementReduit(): boolean {
|
|
|
195
200
|
// Le prérendu n'écrit aucun mouvement.
|
|
196
201
|
() => false,
|
|
197
202
|
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** « theme est sombre » : le thème affiché (choix du visiteur ou du projet, sinon le système), suivi en direct. */
|
|
206
|
+
export function useThemeSombre(): boolean {
|
|
207
|
+
return useSyncExternalStore(
|
|
208
|
+
(changement) => {
|
|
209
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
210
|
+
media.addEventListener('change', changement);
|
|
211
|
+
const observateur = new MutationObserver(changement);
|
|
212
|
+
observateur.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
213
|
+
return () => {
|
|
214
|
+
media.removeEventListener('change', changement);
|
|
215
|
+
observateur.disconnect();
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
() => {
|
|
219
|
+
const choisi = document.documentElement.dataset.theme;
|
|
220
|
+
return choisi ? choisi === 'sombre' : window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
221
|
+
},
|
|
222
|
+
// Le prérendu ne connaît pas le thème du visiteur.
|
|
223
|
+
() => false,
|
|
224
|
+
);
|
|
198
225
|
}`
|
|
199
226
|
: `/** « mouvement est reduit » : la personne a demandé à son téléphone de réduire les animations, suivi en direct. */
|
|
200
227
|
export function useMouvementReduit(): boolean {
|
|
@@ -205,6 +232,11 @@ export function useMouvementReduit(): boolean {
|
|
|
205
232
|
return () => abonnement.remove();
|
|
206
233
|
}, []);
|
|
207
234
|
return reduit;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** « theme est sombre » : le thème affiché par le téléphone, suivi en direct. */
|
|
238
|
+
export function useThemeSombre(): boolean {
|
|
239
|
+
return useColorScheme() === 'dark';
|
|
208
240
|
}`}
|
|
209
241
|
|
|
210
242
|
/**
|
|
@@ -1041,3 +1073,18 @@ const CSS_DEPLACER = `.humalis-liste-deplacable { list-style: none; margin: 0; p
|
|
|
1041
1073
|
.humalis-insertion-avant { box-shadow: 0 -3px 0 var(--humalis-bleu); }
|
|
1042
1074
|
.humalis-insertion-apres { box-shadow: 0 3px 0 var(--humalis-bleu); }
|
|
1043
1075
|
${CSS_CACHE}`;
|
|
1076
|
+
/** `src/humalis/menu.ts` : « menu repliable », le bouton « Menu » d'un petit écran. */
|
|
1077
|
+
const MENU_TS = `/**
|
|
1078
|
+
* « menu repliable » : le bouton « Menu » ouvre et ferme les liens (visible
|
|
1079
|
+
* seulement sur un petit écran, par le CSS) ; un lien choisi referme le menu.
|
|
1080
|
+
*/
|
|
1081
|
+
export function menuRepliable(evenement: { target: EventTarget; currentTarget: EventTarget }): void {
|
|
1082
|
+
const menu = evenement.currentTarget as HTMLElement;
|
|
1083
|
+
const cible = evenement.target as Element;
|
|
1084
|
+
const bouton = menu.querySelector(':scope > .humalis-menu-bouton');
|
|
1085
|
+
const ouvrir = cible.closest('.humalis-menu-bouton') !== null && !menu.hasAttribute('data-ouvert');
|
|
1086
|
+
if (!ouvrir && cible.closest('.humalis-menu-bouton, a') === null) return;
|
|
1087
|
+
menu.toggleAttribute('data-ouvert', ouvrir);
|
|
1088
|
+
bouton?.setAttribute('aria-expanded', String(ouvrir));
|
|
1089
|
+
}
|
|
1090
|
+
`;
|
|
@@ -98,6 +98,9 @@ export function genererProjetWeb(application, options) {
|
|
|
98
98
|
}
|
|
99
99
|
if (dispositions.entete || dispositions.pied)
|
|
100
100
|
fichiers.set('src/Disposition.tsx', dispositionTsx(dispositions, application));
|
|
101
|
+
// Avec un en-tête, le clavier peut sauter directement au contenu de la page.
|
|
102
|
+
if (dispositions.entete)
|
|
103
|
+
fichiers.set('src/humalis/aller-au-contenu.tsx', ALLER_AU_CONTENU);
|
|
101
104
|
const pages = application.ecrans.map((ecran) => ({ ecran, page: genererEcran(ecran, chemins, contexte) }));
|
|
102
105
|
session ||= pages.some(({ page }) => page.session);
|
|
103
106
|
// Une page réservée demande la session, même si elle ne la lit pas elle-même.
|
|
@@ -433,6 +436,24 @@ ${mouvement ? NETTOYAGE_PRERENDU : ''}};
|
|
|
433
436
|
`;
|
|
434
437
|
}
|
|
435
438
|
/** Disposition.tsx : l'en-tête et le pied communs autour des pages, qui restent montés quand on change de page. */
|
|
439
|
+
/** `src/humalis/aller-au-contenu.tsx` : le premier arrêt du clavier, qui saute l'en-tête. */
|
|
440
|
+
const ALLER_AU_CONTENU = `/** Le lien « Aller au contenu » : caché tant qu'il n'a pas le focus du clavier, il saute l'en-tête. */
|
|
441
|
+
export default function AllerAuContenu() {
|
|
442
|
+
return (
|
|
443
|
+
<a
|
|
444
|
+
className="humalis-aller-au-contenu"
|
|
445
|
+
href="#contenu"
|
|
446
|
+
onClick={(evenement) => {
|
|
447
|
+
const contenu = document.querySelector('main');
|
|
448
|
+
if (!contenu) return;
|
|
449
|
+
evenement.preventDefault();
|
|
450
|
+
contenu.setAttribute('tabindex', '-1');
|
|
451
|
+
contenu.focus();
|
|
452
|
+
}}
|
|
453
|
+
>Aller au contenu</a>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
`;
|
|
436
457
|
function dispositionTsx(d, application) {
|
|
437
458
|
const balise = (genre, composant) => {
|
|
438
459
|
const dispo = application.disposition[genre];
|
|
@@ -441,12 +462,12 @@ function dispositionTsx(d, application) {
|
|
|
441
462
|
return `{${genre} && (\n <${ouvre} className="${classes}">\n <${composant} />\n </${ferme}>\n )}`;
|
|
442
463
|
};
|
|
443
464
|
return `import { Outlet } from 'react-router';
|
|
444
|
-
${d.entete ? `import ${d.entete} from './composants/${d.entete}';\n` : ''}${d.pied ? `import ${d.pied} from './composants/${d.pied}';\n` : ''}
|
|
465
|
+
${d.entete ? `import AllerAuContenu from './humalis/aller-au-contenu';\nimport ${d.entete} from './composants/${d.entete}';\n` : ''}${d.pied ? `import ${d.pied} from './composants/${d.pied}';\n` : ''}
|
|
445
466
|
/** L'en-tête et le pied communs à toutes les pages : montés une fois, ils restent là quand on change de page. */
|
|
446
467
|
export default function Disposition({ entete = true, pied = true }: { entete?: boolean; pied?: boolean }) {
|
|
447
468
|
return (
|
|
448
469
|
<>
|
|
449
|
-
${d.entete ? balise('entete', d.entete) : '{entete && null}'}
|
|
470
|
+
${d.entete ? `{entete && <AllerAuContenu />}\n ${balise('entete', d.entete)}` : '{entete && null}'}
|
|
450
471
|
<Outlet />
|
|
451
472
|
${d.pied ? balise('pied', d.pied) : '{pied && null}'}
|
|
452
473
|
</>
|
|
@@ -512,6 +533,12 @@ ${session ? "import { Session } from './humalis/session';\n" : ''}${partage ? "i
|
|
|
512
533
|
${dispositions.entete || dispositions.pied ? "import Disposition from './Disposition';\n" : ''}${imports}
|
|
513
534
|
import './humalis/styles.css';
|
|
514
535
|
${mouvement ? "import { observerLeDefilement } from './humalis/mouvement';\n" : ''}${feuilles.map((f) => `import './styles/${f}';\n`).join('')}
|
|
536
|
+
// Un hébergeur statique sert /documentation/index.html pour /documentation/ :
|
|
537
|
+
// le routeur lit l'adresse sans « index.html », comme le dossier.
|
|
538
|
+
if (/\\/index\\.html$/.test(location.pathname)) {
|
|
539
|
+
history.replaceState(history.state, '', location.pathname.replace(/index\\.html$/, '') + location.search + location.hash);
|
|
540
|
+
}
|
|
541
|
+
|
|
515
542
|
createRoot(document.getElementById('racine')!).render(
|
|
516
543
|
<StrictMode>
|
|
517
544
|
<BrowserRouter>
|
package/dist/cibles/web/theme.js
CHANGED
|
@@ -102,14 +102,23 @@ export const SCRIPT_THEME = `try{var t=localStorage.getItem('${CLE_THEME}');if(t
|
|
|
102
102
|
/** `src/humalis/theme.ts` du projet web : « changer theme par … ». */
|
|
103
103
|
export function themeTs(defaut) {
|
|
104
104
|
return `/** Le thème choisi par le visiteur (« changer theme par sombre »), gardé dans le navigateur. */
|
|
105
|
-
export type Theme = 'clair' | 'sombre' | 'automatique';
|
|
105
|
+
export type Theme = 'clair' | 'sombre' | 'automatique' | 'bascule';
|
|
106
106
|
|
|
107
107
|
const CLE = '${CLE_THEME}';
|
|
108
108
|
/** Le mode du projet quand le visiteur n'a rien choisi (bloc « theme : »). */
|
|
109
109
|
const PAR_DEFAUT: 'clair' | 'sombre' | undefined = ${defaut ? `'${defaut}'` : 'undefined'};
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
/** Le thème affiché : le choix du visiteur ou du projet, sinon celui du système. */
|
|
112
|
+
function themeAffiche(): 'clair' | 'sombre' {
|
|
113
|
+
const choisi = document.documentElement.dataset.theme;
|
|
114
|
+
if (choisi === 'clair' || choisi === 'sombre') return choisi;
|
|
115
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'sombre' : 'clair';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function definirTheme(demande: Theme): void {
|
|
112
119
|
const racine = document.documentElement;
|
|
120
|
+
// « basculer theme » : l'inverse de ce qu'on voit.
|
|
121
|
+
const theme = demande === 'bascule' ? (themeAffiche() === 'sombre' ? 'clair' : 'sombre') : demande;
|
|
113
122
|
try {
|
|
114
123
|
if (theme === 'automatique') localStorage.removeItem(CLE);
|
|
115
124
|
else localStorage.setItem(CLE, theme);
|
|
@@ -38,6 +38,7 @@ export function typeDe(valeur, contexte) {
|
|
|
38
38
|
case 'utilisateur':
|
|
39
39
|
case 'etatRequete':
|
|
40
40
|
case 'mouvementReduit':
|
|
41
|
+
case 'themeSombre':
|
|
41
42
|
return { genre: 'booleen' };
|
|
42
43
|
case 'erreurRequete':
|
|
43
44
|
return { genre: 'texte' };
|
|
@@ -150,6 +151,9 @@ export function expression(valeur, contexte) {
|
|
|
150
151
|
case 'mouvementReduit':
|
|
151
152
|
// « réduire les animations » du système, suivi en direct (src/humalis/temps.ts).
|
|
152
153
|
return 'mouvementReduit';
|
|
154
|
+
case 'themeSombre':
|
|
155
|
+
// Le thème affiché, suivi en direct (src/humalis/temps.ts).
|
|
156
|
+
return 'themeSombre';
|
|
153
157
|
case 'etatRequete':
|
|
154
158
|
// L'état de la requête de la variable (src/humalis/serveur.ts) : chargement, erreur ou introuvable.
|
|
155
159
|
return `${camel(valeur.variable)}Req.statut === '${valeur.etat}'`;
|
|
@@ -187,6 +191,19 @@ function texteTS(parties, contexte, langue) {
|
|
|
187
191
|
}
|
|
188
192
|
/** Le contenu d'un élément dans du JSX : `Bienvenue`, `{nom}`, `{panier.join(', ')}`. */
|
|
189
193
|
export function contenuJSX(valeur, contexte) {
|
|
194
|
+
if (valeur.type === 'texte' && (valeur.parties.some((p) => 'texte' in p && (p.accent || p.code)) || valeur.versions?.some((v) => v.parties.some((p) => 'texte' in p && (p.accent || p.code))))) {
|
|
195
|
+
const enfants = (parties) => parties.map((p) => {
|
|
196
|
+
if ('texte' in p)
|
|
197
|
+
return p.accent
|
|
198
|
+
? `<span className="humalis-accent">{${chaine(p.texte)}}</span>`
|
|
199
|
+
: p.code ? `<code className="humalis-code-ligne">{${chaine(p.texte)}}</code>`
|
|
200
|
+
: `{${chaine(p.texte)}}`;
|
|
201
|
+
return contenuJSX({ type: 'texte', parties: [p] }, contexte);
|
|
202
|
+
}).join('');
|
|
203
|
+
if (valeur.versions)
|
|
204
|
+
return valeur.versions.map((v) => `{langue === ${chaine(v.code)} && <>${enfants(v.parties)}</>}`).join('');
|
|
205
|
+
return enfants(valeur.parties);
|
|
206
|
+
}
|
|
190
207
|
if (valeur.type === 'texte' && !valeur.versions && valeur.parties.every((p) => 'texte' in p)) {
|
|
191
208
|
const texte = valeur.parties.map((p) => ('texte' in p ? p.texte : '')).join('');
|
|
192
209
|
// Les caractères qui ont un sens en JSX passent par une chaîne.
|
|
@@ -185,7 +185,7 @@ export function dependancesDeDeveloppement(codes) {
|
|
|
185
185
|
}
|
|
186
186
|
const AVATAR_TSX = `import { useState } from 'react';
|
|
187
187
|
|
|
188
|
-
/** Les initiales d'un nom : «
|
|
188
|
+
/** Les initiales d'un nom : « Camille Martin » donne « CM ». */
|
|
189
189
|
function initiales(nom: string): string {
|
|
190
190
|
const mots = nom.trim().split(/\\s+/).filter(Boolean);
|
|
191
191
|
return ((mots[0]?.[0] ?? '') + (mots.length > 1 ? (mots[mots.length - 1][0] ?? '') : '')).toUpperCase();
|
package/dist/i18n/messages.js
CHANGED
|
@@ -65,8 +65,10 @@ export const MESSAGES = [
|
|
|
65
65
|
{ fr: "Choisissez d'abord un fichier.", en: 'Choose a file first.' },
|
|
66
66
|
{ fr: 'AAAA-MM-JJ', en: 'YYYY-MM-DD' },
|
|
67
67
|
{ fr: 'Fermer le message', en: 'Close the message' },
|
|
68
|
+
{ fr: 'Aller au contenu', en: 'Skip to content' },
|
|
68
69
|
{ fr: 'Fermer', en: 'Close' },
|
|
69
70
|
{ fr: 'Copié', en: 'Copied' },
|
|
71
|
+
{ fr: 'Copier', en: 'Copy' },
|
|
70
72
|
{ fr: 'Copie impossible.', en: 'Could not copy.' },
|
|
71
73
|
{ fr: 'Lien copié : il ne reste qu’à le coller.', en: 'Link copied: paste it wherever you like.' },
|
|
72
74
|
{ fr: 'Texte copié : il ne reste qu’à le coller.', en: 'Text copied: paste it wherever you like.' },
|
package/dist/lexer/lexer.js
CHANGED
|
@@ -30,6 +30,23 @@ function motColle(ligne, debut, premier) {
|
|
|
30
30
|
}
|
|
31
31
|
return null;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Le guillemet qui ferme un {code "…"} ou un {accent "…"} commencé à `debut` : le premier « "} »
|
|
35
|
+
* où les accolades du contenu sont équilibrées ({code "{accent "…"}"}), sinon le premier « "} »
|
|
36
|
+
* ({code "{"}). -1 s'il n'y en a pas.
|
|
37
|
+
*/
|
|
38
|
+
function finDuMorceau(texte, debut) {
|
|
39
|
+
let profondeur = 0;
|
|
40
|
+
for (let j = debut; j < texte.length; j++) {
|
|
41
|
+
if (texte.startsWith('"}', j) && profondeur <= 0)
|
|
42
|
+
return j;
|
|
43
|
+
if (texte[j] === '{')
|
|
44
|
+
profondeur++;
|
|
45
|
+
if (texte[j] === '}')
|
|
46
|
+
profondeur--;
|
|
47
|
+
}
|
|
48
|
+
return texte.indexOf('"}', debut);
|
|
49
|
+
}
|
|
33
50
|
function decouperChaine(valeur, ligne, colonne) {
|
|
34
51
|
const parties = [];
|
|
35
52
|
let tampon = '';
|
|
@@ -42,6 +59,13 @@ function decouperChaine(valeur, ligne, colonne) {
|
|
|
42
59
|
}
|
|
43
60
|
i++;
|
|
44
61
|
let nomVariable = '';
|
|
62
|
+
// {code "…"} et {accent "…"} : le morceau va jusqu'au premier « "} ».
|
|
63
|
+
const morceau = /^(code|accent)\s*"/.exec(valeur.slice(i));
|
|
64
|
+
const finMorceau = morceau ? finDuMorceau(valeur, i + morceau[0].length) : -1;
|
|
65
|
+
if (finMorceau >= 0) {
|
|
66
|
+
nomVariable = valeur.slice(i, finMorceau + 1);
|
|
67
|
+
i = finMorceau + 1;
|
|
68
|
+
}
|
|
45
69
|
while (i < valeur.length && valeur[i] !== '}') {
|
|
46
70
|
nomVariable += valeur[i];
|
|
47
71
|
i++;
|
|
@@ -50,7 +74,21 @@ function decouperChaine(valeur, ligne, colonne) {
|
|
|
50
74
|
throw new ErreurLexicale('accolade non fermée dans la chaîne : il manque }', ligne, colonne);
|
|
51
75
|
}
|
|
52
76
|
i++;
|
|
53
|
-
|
|
77
|
+
const accent = /^accent\b(.*)$/s.exec(nomVariable.trim());
|
|
78
|
+
if (accent) {
|
|
79
|
+
const contenu = /^\s*"([\s\S]*)"\s*$/.exec(accent[1]);
|
|
80
|
+
if (!contenu)
|
|
81
|
+
throw new ErreurLexicale('accent attend un texte entre guillemets : {accent "mot"}.', ligne, colonne);
|
|
82
|
+
parties.push({ type: 'accent', valeur: contenu[1] });
|
|
83
|
+
}
|
|
84
|
+
else if (/^code\s+\S/s.test(nomVariable.trim())) {
|
|
85
|
+
const contenu = /^code\s+"([\s\S]*)"\s*$/.exec(nomVariable.trim());
|
|
86
|
+
if (!contenu)
|
|
87
|
+
throw new ErreurLexicale('code attend un texte entre guillemets : {code "titre"}.', ligne, colonne);
|
|
88
|
+
parties.push({ type: 'code', valeur: contenu[1] });
|
|
89
|
+
}
|
|
90
|
+
else
|
|
91
|
+
parties.push({ type: 'variable', valeur: nomVariable.trim() });
|
|
54
92
|
continue;
|
|
55
93
|
}
|
|
56
94
|
tampon += valeur[i];
|
|
@@ -68,10 +106,52 @@ export function analyser(source) {
|
|
|
68
106
|
for (let numLigne = 0; numLigne < lignes.length; numLigne++) {
|
|
69
107
|
const ligne = lignes[numLigne];
|
|
70
108
|
let i = 0;
|
|
109
|
+
// « code humalis : », « code : », et l'animation : « code humalis avec animation frappe au defilement : ».
|
|
110
|
+
const codeBrut = /^( *)(code)(?: +(?!avec\b)([\p{L}][\p{L}0-9_]*))?( +avec +animation +frappe(?: +au +d[eé]filement)?)? *: *$/u.exec(ligne);
|
|
111
|
+
if (codeBrut) {
|
|
112
|
+
const contenu = [];
|
|
113
|
+
const indentation = codeBrut[1].length;
|
|
114
|
+
if (indentation % 4 !== 0)
|
|
115
|
+
throw new ErreurLexicale('indentation incohérente : un multiple de 4 est attendu', numLigne + 1, 1);
|
|
116
|
+
const niveau = indentation / 4;
|
|
117
|
+
const courant = pile[pile.length - 1];
|
|
118
|
+
if (niveau > courant + 1)
|
|
119
|
+
throw new ErreurLexicale('indentation incohérente : un seul niveau à la fois', numLigne + 1, 1);
|
|
120
|
+
if (niveau === courant + 1) {
|
|
121
|
+
pile.push(niveau);
|
|
122
|
+
tokens.push({ type: 'INDENT', valeur: '', ligne: numLigne + 1, colonne: 1 });
|
|
123
|
+
}
|
|
124
|
+
else
|
|
125
|
+
while (pile.length > 1 && pile[pile.length - 1] > niveau) {
|
|
126
|
+
pile.pop();
|
|
127
|
+
tokens.push({ type: 'DEDENT', valeur: '', ligne: numLigne + 1, colonne: 1 });
|
|
128
|
+
}
|
|
129
|
+
if (pile[pile.length - 1] !== niveau)
|
|
130
|
+
throw new ErreurLexicale('indentation incohérente : ce niveau ne correspond à aucun bloc ouvert', numLigne + 1, 1);
|
|
131
|
+
while (numLigne + 1 < lignes.length) {
|
|
132
|
+
const suivante = lignes[numLigne + 1];
|
|
133
|
+
const marge = suivante.length - suivante.trimStart().length;
|
|
134
|
+
if (suivante.trim() !== '' && marge <= indentation)
|
|
135
|
+
break;
|
|
136
|
+
contenu.push(lignes[++numLigne]);
|
|
137
|
+
}
|
|
138
|
+
while (contenu.length && contenu[contenu.length - 1].trim() === '')
|
|
139
|
+
contenu.pop();
|
|
140
|
+
const marge = Math.min(...contenu.filter((l) => l.trim() !== '').map((l) => l.length - l.trimStart().length));
|
|
141
|
+
const brut = contenu.map((l) => l.slice(Math.min(marge, l.length - l.trimStart().length))).join('\n');
|
|
142
|
+
const ligneDebut = numLigne - contenu.length + 1;
|
|
143
|
+
tokens.push({ type: 'MOT_CLE', valeur: 'code', ligne: ligneDebut, colonne: indentation + 1, reserve: false, original: 'code' }, ...(codeBrut[3] ? [{ type: 'IDENTIFIANT', valeur: codeBrut[3], ligne: ligneDebut, colonne: indentation + 6 }] : []), ...(codeBrut[4] ? codeBrut[4].trim().split(/ +/).map((mot) => ({ type: 'IDENTIFIANT', valeur: mot, ligne: ligneDebut, colonne: ligne.indexOf(codeBrut[4]) + 1 })) : []), { type: 'PONCTUATION', valeur: ':', ligne: ligneDebut, colonne: ligne.indexOf(':') + 1 }, { type: 'FIN_LIGNE', valeur: '', ligne: ligneDebut, colonne: ligne.length + 1 }, { type: 'CODE_BRUT', valeur: brut, ligne: ligneDebut + 1, colonne: 1 }, { type: 'FIN_LIGNE', valeur: '', ligne: numLigne + 1, colonne: 1 });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
71
146
|
// « style : » au début d'une ligne, hors de tout bloc : les lignes
|
|
72
147
|
// indentées qui suivent sont du CSS, recopié tel quel. Il contient des
|
|
73
148
|
// {, des : et des # qui ne doivent pas être lus comme du Humalis.
|
|
74
|
-
if (
|
|
149
|
+
if (/^style\s*:\s*$/i.test(ligne)) {
|
|
150
|
+
// Juste après un bloc de haut de fichier (« theme : »), ce bloc se ferme ici.
|
|
151
|
+
while (pile.length > 1) {
|
|
152
|
+
pile.pop();
|
|
153
|
+
tokens.push({ type: 'DEDENT', valeur: '', ligne: numLigne + 1, colonne: 1 });
|
|
154
|
+
}
|
|
75
155
|
const debut = numLigne;
|
|
76
156
|
const contenu = [];
|
|
77
157
|
while (numLigne + 1 < lignes.length && (lignes[numLigne + 1].trim() === '' || /^\s/.test(lignes[numLigne + 1]))) {
|
|
@@ -141,7 +221,30 @@ export function analyser(source) {
|
|
|
141
221
|
const debut = i;
|
|
142
222
|
i++;
|
|
143
223
|
let valeur = '';
|
|
144
|
-
|
|
224
|
+
let profondeur = 0;
|
|
225
|
+
while (i < ligne.length && (ligne[i] !== '"' || (profondeur > 0 && ligne.indexOf('}', i + 1) >= 0))) {
|
|
226
|
+
// {code "…"} et {accent "…"} : recopiés tels quels jusqu'au premier « "} »
|
|
227
|
+
// (accolades, guillemets et antislashs compris).
|
|
228
|
+
const morceau = /^\{(code|accent)\s*"/.exec(ligne.slice(i));
|
|
229
|
+
const fin = morceau ? finDuMorceau(ligne, i + morceau[0].length) : -1;
|
|
230
|
+
if (fin >= 0) {
|
|
231
|
+
valeur += ligne.slice(i, fin + 2);
|
|
232
|
+
i = fin + 2;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// \" écrit un guillemet dans le texte ; tout autre antislash reste tel quel
|
|
236
|
+
// (« \\ » aussi, en entier : un texte peut finir par lui, comme avant).
|
|
237
|
+
if (ligne[i] === '\\' && ligne[i + 1] === '\\') {
|
|
238
|
+
valeur += '\\\\';
|
|
239
|
+
i += 2;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (ligne[i] === '\\' && ligne[i + 1] === '"')
|
|
243
|
+
i++;
|
|
244
|
+
if (ligne[i] === '{')
|
|
245
|
+
profondeur++;
|
|
246
|
+
if (ligne[i] === '}' && profondeur > 0)
|
|
247
|
+
profondeur--;
|
|
145
248
|
valeur += ligne[i];
|
|
146
249
|
i++;
|
|
147
250
|
}
|