humalis 1.2.0 → 1.3.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/README.en.md +8 -1
- package/README.md +8 -1
- package/dist/analyse/analyse.js +6 -7
- package/dist/analyse/css.js +28 -0
- package/dist/cibles/commun/mouvement.js +29 -0
- package/dist/cibles/commun/reperes.js +70 -0
- package/dist/cibles/mobile/projet.js +6 -0
- package/dist/cibles/statique/page.js +25 -11
- package/dist/cibles/statique/projet.js +6 -1
- package/dist/cibles/web/css-ecran.js +19 -1
- package/dist/cibles/web/ecran.js +11 -5
- package/dist/cibles/web/langue.js +3 -0
- package/dist/cibles/web/projet.js +10 -5
- package/dist/cli.js +7 -2
- package/dist/fichiers.js +21 -1
- package/dist/navigateur.js +90 -62
- package/dist/outils/developpement.js +3 -1
- package/dist/parser/miseEnPage.js +1 -1
- package/dist/vocabulaire/table-ecran.js +6 -0
- package/docs/guide.en.md +40 -0
- package/docs/guide.md +38 -0
- package/llms.txt +2 -1
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
<p align="center"><a href="README.md">Français</a> · <strong>English</strong></p>
|
|
6
6
|
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/humalis"><img src="https://img.shields.io/npm/v/humalis" alt="npm"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Humalis%201.0-c1e928" alt="Licence"></a>
|
|
10
|
+
<a href="https://github.com/emch04/framework"><img src="https://img.shields.io/badge/server-Astratra-6f42c1" alt="Astratra"></a>
|
|
11
|
+
<a href="https://hub.xerj.org/contributors/emch04"><img src="https://hub.xerj.org/contributors/emch04/badge.svg" alt="XERJ Corpus Hub contributor"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
7
14
|
# Humalis
|
|
8
15
|
|
|
9
16
|
**One language, in French, to build a complete application**: a website, a mobile app, a desktop app and its server, all from the same file.
|
|
@@ -59,7 +66,7 @@ humalis compile my-project.humalis --cible serveur --sortie my-project-server
|
|
|
59
66
|
|---|---|
|
|
60
67
|
| `web` | a React + Vite project, prerendered (one HTML page per address) |
|
|
61
68
|
| `statique` | static HTML, CSS and JavaScript pages, no build step |
|
|
62
|
-
| `serveur` | a Node server with the Astratra packs (login, permissions, sessions) |
|
|
69
|
+
| `serveur` | a Node server with the [Astratra](https://github.com/emch04/framework) packs (login, permissions, sessions) |
|
|
63
70
|
| `mobile` | an Expo app (iPhone, Android) |
|
|
64
71
|
| `desktop` | an Electron app (Mac, Windows, Linux) |
|
|
65
72
|
|
package/README.md
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
<p align="center"><strong>Français</strong> · <a href="README.en.md">English</a></p>
|
|
6
6
|
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/humalis"><img src="https://img.shields.io/npm/v/humalis" alt="npm"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/licence-Humalis%201.0-c1e928" alt="Licence"></a>
|
|
10
|
+
<a href="https://github.com/emch04/framework"><img src="https://img.shields.io/badge/serveur-Astratra-6f42c1" alt="Astratra"></a>
|
|
11
|
+
<a href="https://hub.xerj.org/contributors/emch04"><img src="https://hub.xerj.org/contributors/emch04/badge.svg" alt="XERJ Corpus Hub contributor"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
7
14
|
# Humalis
|
|
8
15
|
|
|
9
16
|
**Un seul langage, en français, pour créer une application complète** : un site web, une application mobile, une application de bureau et son serveur, à partir du même fichier.
|
|
@@ -57,7 +64,7 @@ humalis compile mon-projet.humalis --cible serveur --sortie mon-projet-serveur
|
|
|
57
64
|
|---|---|
|
|
58
65
|
| `web` | un projet React + Vite, prérendu (une page HTML par adresse) |
|
|
59
66
|
| `statique` | des pages HTML, CSS et JavaScript, sans construction |
|
|
60
|
-
| `serveur` | un serveur Node avec les packs Astratra (connexion, droits, sessions) |
|
|
67
|
+
| `serveur` | un serveur Node avec les packs [Astratra](https://github.com/emch04/framework) (connexion, droits, sessions) |
|
|
61
68
|
| `mobile` | une application Expo (iPhone, Android) |
|
|
62
69
|
| `desktop` | une application Electron (Mac, Windows, Linux) |
|
|
63
70
|
|
package/dist/analyse/analyse.js
CHANGED
|
@@ -55,7 +55,7 @@ function modesVus(programme) {
|
|
|
55
55
|
return ['clair', 'sombre'];
|
|
56
56
|
return JSON.stringify(programme).includes('"action":"changerTheme"') ? ['clair', 'sombre'] : [mode];
|
|
57
57
|
}
|
|
58
|
-
export function analyserSens(programme) {
|
|
58
|
+
export function analyserSens(programme, classesCss) {
|
|
59
59
|
const resultat = {
|
|
60
60
|
diagnostics: [],
|
|
61
61
|
elements: new Map(),
|
|
@@ -105,11 +105,10 @@ export function analyserSens(programme) {
|
|
|
105
105
|
filtresDesPages.clear();
|
|
106
106
|
for (const p of programme.pages)
|
|
107
107
|
filtresDesPages.set(p.nom, p.filtres ?? []);
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
const classes = programme.fichiersStyle.length > 0
|
|
108
|
+
// Un fichier CSS déclaré mais illisible rend la vérification des noms incertaine.
|
|
109
|
+
const classes = classesCss ?? (programme.fichiersStyle.length > 0
|
|
111
110
|
? undefined
|
|
112
|
-
: new Set(programme.styles.flatMap((b) => [...b.css.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)].map((m) => m[1])));
|
|
111
|
+
: new Set(programme.styles.flatMap((b) => [...b.css.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)].map((m) => m[1]))));
|
|
113
112
|
// Les variables du haut du fichier sont partagées entre les pages : on
|
|
114
113
|
// les analyse une fois, puis chaque page les voit (portée « application »).
|
|
115
114
|
const globale = new AnalysePage({ type: 'Page', nom: '', parametres: [], corps: [], ligne: 0 }, nomsPages, resultat, classes);
|
|
@@ -935,7 +934,7 @@ class AnalysePage {
|
|
|
935
934
|
}
|
|
936
935
|
for (const classe of element.classes) {
|
|
937
936
|
if (this.classesDefinies && !this.classesDefinies.has(classe)) {
|
|
938
|
-
this.avertissement(`
|
|
937
|
+
this.avertissement(`le style « ${classe} » n'est défini dans aucun CSS du projet.${proposerNom(classe, this.classesDefinies)}`, element.ligne);
|
|
939
938
|
}
|
|
940
939
|
}
|
|
941
940
|
verifierElementEcran(element, this.signaler());
|
|
@@ -1110,7 +1109,7 @@ class AnalysePage {
|
|
|
1110
1109
|
}
|
|
1111
1110
|
for (const classe of bloc.classes) {
|
|
1112
1111
|
if (this.classesDefinies && !this.classesDefinies.has(classe)) {
|
|
1113
|
-
this.avertissement(`
|
|
1112
|
+
this.avertissement(`le style « ${classe} » n'est défini dans aucun CSS du projet.${proposerNom(classe, this.classesDefinies)}`, bloc.ligne);
|
|
1114
1113
|
}
|
|
1115
1114
|
}
|
|
1116
1115
|
verifierBlocEcran(bloc, this.pileBlocs[this.pileBlocs.length - 1], this.signaler());
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Repère les classes dans les sélecteurs et les déclarations qui forcent les styles Humalis. */
|
|
2
|
+
export function verifierCss(css, premiereLigne, fichier) {
|
|
3
|
+
const classes = new Set();
|
|
4
|
+
const diagnostics = [];
|
|
5
|
+
const sansCommentaires = css.replace(/\/\*[\s\S]*?\*\//g, (commentaire) => commentaire.replace(/[^\n]/g, ' '));
|
|
6
|
+
const regles = /([^{}]+)\{([^{}]*)\}/g;
|
|
7
|
+
for (const regle of sansCommentaires.matchAll(regles)) {
|
|
8
|
+
const selecteur = regle[1];
|
|
9
|
+
if (selecteur.trimStart().startsWith('@'))
|
|
10
|
+
continue;
|
|
11
|
+
const noms = [...selecteur.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)].map((m) => m[1]);
|
|
12
|
+
for (const nom of noms)
|
|
13
|
+
classes.add(nom);
|
|
14
|
+
const humalis = noms.filter((nom) => nom.startsWith('humalis-'));
|
|
15
|
+
if (humalis.length === 0)
|
|
16
|
+
continue;
|
|
17
|
+
for (const declaration of regle[2].matchAll(/(?:^|;)\s*([\w-]+)\s*:\s*([^;]*!\s*important)\s*(?=;|$)/gi)) {
|
|
18
|
+
const avant = sansCommentaires.slice(0, regle.index + selecteur.length + 1 + (declaration.index ?? 0));
|
|
19
|
+
const ligne = premiereLigne + (avant.match(/\n/g)?.length ?? 0);
|
|
20
|
+
for (const nom of humalis)
|
|
21
|
+
diagnostics.push({
|
|
22
|
+
gravite: 'avertissement', ligne, ...(fichier ? { fichier } : {}),
|
|
23
|
+
message: `le CSS écrase « ${nom} » avec !important (${declaration[1]}) : les réglages Humalis de cet élément (remplissage, largeur…) ne s'appliqueront plus. Retirez !important, ou visez votre propre classe avec « avec style ».`,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { classes, diagnostics };
|
|
28
|
+
}
|
|
@@ -256,6 +256,35 @@ ${OBSERVATEUR_DEFILEMENT.replace('function observerLeDefilement() {', 'export fu
|
|
|
256
256
|
.replace('const morceaux = [];', 'const morceaux: [Text, string][] = [];')
|
|
257
257
|
.replace('const noeud = parcours.currentNode;', 'const noeud = parcours.currentNode as Text;')}
|
|
258
258
|
`;
|
|
259
|
+
/**
|
|
260
|
+
* La hauteur de l'en-tête fixe ou collé, gardée dans --humalis-hauteur-entete : « entete avec fixe »
|
|
261
|
+
* place la zone qui défile dessous, « hauteur ecran » s'en sert pour remplir l'écran.
|
|
262
|
+
*/
|
|
263
|
+
export const SUIVI_ENTETE = `function suivreLaHauteurDeLEntete() {
|
|
264
|
+
const entete = () => document.querySelector('header.humalis-fixe-oui, header.humalis-colle-haut');
|
|
265
|
+
const regler = () => {
|
|
266
|
+
const e = entete();
|
|
267
|
+
document.documentElement.style.setProperty('--humalis-hauteur-entete', e ? e.offsetHeight + 'px' : '0px');
|
|
268
|
+
};
|
|
269
|
+
regler();
|
|
270
|
+
addEventListener('resize', regler);
|
|
271
|
+
if (!('ResizeObserver' in window)) return;
|
|
272
|
+
const observateur = new ResizeObserver(regler);
|
|
273
|
+
const suivre = () => {
|
|
274
|
+
const e = entete();
|
|
275
|
+
if (e) observateur.observe(e);
|
|
276
|
+
regler();
|
|
277
|
+
};
|
|
278
|
+
suivre();
|
|
279
|
+
// L'en-tête peut apparaître après le chargement (le web le dessine après le script).
|
|
280
|
+
new MutationObserver(suivre).observe(document.body, { childList: true, subtree: true });
|
|
281
|
+
}`;
|
|
282
|
+
/** Vrai si une page a besoin de la hauteur de l'en-tête. */
|
|
283
|
+
export const suitLEntete = (codes) => codes.some((c) => c.includes('humalis-fixe-oui') || c.includes('humalis-hauteur-ecran'));
|
|
284
|
+
/** src/humalis/entete.ts du web et du desktop : le même script, typé. */
|
|
285
|
+
export const ENTETE_WEB = `// Produit par Humalis : la hauteur de l'en-tête, pour « entete avec fixe » et « hauteur ecran ».
|
|
286
|
+
${SUIVI_ENTETE.replace('function suivreLaHauteurDeLEntete() {', 'export function suivreLaHauteurDeLEntete(): void {').replace("const entete = () => document.querySelector(", "const entete = () => document.querySelector<HTMLElement>(")}
|
|
287
|
+
`;
|
|
259
288
|
/** Le prérendu capture la page après le script : il retire les marques du mouvement (rien de caché sans JavaScript). */
|
|
260
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.
|
|
261
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, ''),
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Les repères du mode développement (« humalis developper ») : chaque
|
|
3
|
+
* élément de la page porte son fichier Humalis et sa ligne,
|
|
4
|
+
* `data-humalis-source="pages/accueil.humalis:12"`, pour que l'inspecteur
|
|
5
|
+
* du navigateur mène droit au code. Le chemin est relatif au fichier
|
|
6
|
+
* principal, comme l'auteur l'écrit ; la ligne est celle de ce fichier.
|
|
7
|
+
*
|
|
8
|
+
* Une compilation normale n'en reçoit aucun : les cibles ne les écrivent
|
|
9
|
+
* que si on leur donne la table des fichiers du projet (`sources`).
|
|
10
|
+
*/
|
|
11
|
+
import { DECALAGE, ligneLocale } from '../../fichiers.js';
|
|
12
|
+
import { echapperAttribut } from '../statique/html.js';
|
|
13
|
+
/**
|
|
14
|
+
* « pages/accueil.humalis:12 » : la ligne fusionnée du projet (k × DECALAGE
|
|
15
|
+
* + ligne, voir src/fichiers.ts) rendue à son fichier. `fichiers` est la
|
|
16
|
+
* liste rendue par compilerProjet, le principal en premier. Undefined si la
|
|
17
|
+
* ligne est inconnue (0) ou hors de la table.
|
|
18
|
+
*/
|
|
19
|
+
export function repereSource(ligne, fichiers) {
|
|
20
|
+
const locale = ligneLocale(ligne);
|
|
21
|
+
const fichier = fichiers[Math.floor(ligne / DECALAGE)];
|
|
22
|
+
return fichier === undefined || locale <= 0 ? undefined : `${fichier}:${locale}`;
|
|
23
|
+
}
|
|
24
|
+
/** L'attribut en HTML : ` data-humalis-source="…"`, la valeur échappée. */
|
|
25
|
+
export function attributSourceHtml(repere) {
|
|
26
|
+
return ` data-humalis-source="${echapperAttribut(repere)}"`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* L'attribut en JSX. Une chaîne JSX entre guillemets lit les entités HTML
|
|
30
|
+
* (« & ») et ne connaît pas l'échappement : dès qu'un caractère
|
|
31
|
+
* pourrait gêner, la valeur devient une expression JavaScript.
|
|
32
|
+
*/
|
|
33
|
+
export function attributSourceJsx(repere) {
|
|
34
|
+
return /^[\w./:@ -]*$/u.test(repere) ? ` data-humalis-source="${repere}"` : ` data-humalis-source={${JSON.stringify(repere)}}`;
|
|
35
|
+
}
|
|
36
|
+
/** Ajoute l'attribut juste après le nom de la balise ouvrante (`<h1 …`, `<Lien …`) ; un fragment (`<>`) reste tel quel. */
|
|
37
|
+
export function marquerBalise(balise, attribut) {
|
|
38
|
+
return balise.replace(/^<[A-Za-z][\w.]*/, (debut) => debut + attribut);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* L'aide du mode développement : la touche Alt (Option sur Mac) tenue, le
|
|
42
|
+
* survol d'un élément montre son repère dans une petite étiquette près du
|
|
43
|
+
* curseur. Du JavaScript simple, servi par « humalis developper » avec le
|
|
44
|
+
* panneau des erreurs ; il n'entre jamais dans le code produit.
|
|
45
|
+
*/
|
|
46
|
+
export const AIDE_REPERES = `(() => {
|
|
47
|
+
const etiquette = document.createElement('div');
|
|
48
|
+
etiquette.id = 'humalis-repere';
|
|
49
|
+
Object.assign(etiquette.style, {
|
|
50
|
+
position: 'fixed', zIndex: '999999', pointerEvents: 'none', display: 'none',
|
|
51
|
+
padding: '2px 6px', borderRadius: '4px', background: '#1d2b24', color: '#fff',
|
|
52
|
+
font: '12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace', whiteSpace: 'nowrap',
|
|
53
|
+
});
|
|
54
|
+
document.body.append(etiquette);
|
|
55
|
+
let dernier = null;
|
|
56
|
+
const cacher = () => { etiquette.style.display = 'none'; };
|
|
57
|
+
const montrer = (evenement) => {
|
|
58
|
+
const cible = evenement.target && evenement.target.closest ? evenement.target.closest('[data-humalis-source]') : null;
|
|
59
|
+
if (!evenement.altKey || !cible) return cacher();
|
|
60
|
+
etiquette.textContent = cible.getAttribute('data-humalis-source');
|
|
61
|
+
etiquette.style.left = (evenement.clientX + 12) + 'px';
|
|
62
|
+
etiquette.style.top = (evenement.clientY + 14) + 'px';
|
|
63
|
+
etiquette.style.display = 'block';
|
|
64
|
+
};
|
|
65
|
+
document.addEventListener('mousemove', (evenement) => { dernier = evenement; montrer(evenement); });
|
|
66
|
+
document.addEventListener('keydown', (evenement) => { if (evenement.key === 'Alt' && dernier) montrer({ altKey: true, target: dernier.target, clientX: dernier.clientX, clientY: dernier.clientY }); });
|
|
67
|
+
document.addEventListener('keyup', (evenement) => { if (evenement.key === 'Alt') cacher(); });
|
|
68
|
+
window.addEventListener('blur', cacher);
|
|
69
|
+
})();
|
|
70
|
+
`;
|
|
@@ -221,6 +221,12 @@ export function verifierPourLeMobile(application) {
|
|
|
221
221
|
for (const ligne of lignesAvecReglage([...application.ecrans, ...application.composants].flatMap((e) => e.vue), 'colle')) {
|
|
222
222
|
avertir(ligne, "« colle » n'a pas d'effet sur le téléphone, sauf pour l'en-tête et le pied de page.");
|
|
223
223
|
}
|
|
224
|
+
// « defilement interne », « hauteur ecran » : sur un téléphone, toute la page défile.
|
|
225
|
+
for (const reglage of ['defilement', 'hauteur']) {
|
|
226
|
+
for (const ligne of lignesAvecReglage([...application.ecrans, ...application.composants].flatMap((e) => e.vue), reglage)) {
|
|
227
|
+
avertir(ligne, `« ${reglage === 'defilement' ? 'defilement interne' : 'hauteur ecran'} » n'a pas d'effet sur le téléphone : toute la page défile.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
224
230
|
// M7 : pas de souris ; les composants React sont recopiés tels quels.
|
|
225
231
|
const gestionnaires = [...application.ecrans, ...application.composants].flatMap((e) => [
|
|
226
232
|
...e.gestionnaires,
|
|
@@ -11,6 +11,7 @@ import { verificationsDuChamp, messageEnCode, libellesEnLangues, messageEnLangue
|
|
|
11
11
|
import { sousActions } from '../../ri/actions.js';
|
|
12
12
|
import { colorerCodeHumalis } from '../commun/colorerCode.js';
|
|
13
13
|
import { attributsFrappe } from '../commun/mouvement.js';
|
|
14
|
+
import { repereSource } from '../commun/reperes.js';
|
|
14
15
|
import { arbreWidget, arbreComposite, initiales } from './widgets.js';
|
|
15
16
|
import { iconeHtml, etoilesHtml } from './html-elements.js';
|
|
16
17
|
import { appelGraphique, htmlGraphique } from './graphique.js';
|
|
@@ -912,7 +913,7 @@ ${prise}${code}}
|
|
|
912
913
|
case 'bloc': {
|
|
913
914
|
if (noeud.genre === 'liste' || noeud.genre === 'listeNumerotee') {
|
|
914
915
|
const balise = noeud.genre === 'liste' ? 'ul' : 'ol';
|
|
915
|
-
e.ligne(`creer('${balise}', { class: 'humalis-liste' },`);
|
|
916
|
+
e.ligne(`creer('${balise}', { ${this.repereObjetJs(noeud.ligne)}class: 'humalis-liste' },`);
|
|
916
917
|
e.indenter(() => noeud.corps.forEach((enfant) => {
|
|
917
918
|
if (enfant.type === 'repetition') {
|
|
918
919
|
const debut = `...${expressionJs(enfant.liste, this.contexte)}.flatMap((${nomJs(enfant.variable)}) => `;
|
|
@@ -931,7 +932,7 @@ ${prise}${code}}
|
|
|
931
932
|
const balise = BALISES_BLOC[noeud.genre] ?? 'div';
|
|
932
933
|
// « menu repliable » : le bouton « Menu » en premier enfant (l'écouteur est dans humalis.js).
|
|
933
934
|
const bouton = estRepliable(noeud) ? `creer('button', { type: 'button', class: 'humalis-menu-bouton', 'aria-expanded': 'false', 'aria-label': 'Menu' }, creer('span', { class: 'humalis-menu-icone', 'aria-hidden': 'true' })), ` : '';
|
|
934
|
-
this.ecrireGroupe(e, noeud.corps, `creer('${balise}', { class: ${chaine(classesDuBloc(noeud))} }, ${bouton}...`, '),');
|
|
935
|
+
this.ecrireGroupe(e, noeud.corps, `creer('${balise}', { ${this.repereObjetJs(noeud.ligne)}class: ${chaine(classesDuBloc(noeud))} }, ${bouton}...`, '),');
|
|
935
936
|
// « arrive en bas de » ce bloc : la sentinelle observée et le vrai bouton « Voir plus », tant que la condition est vraie.
|
|
936
937
|
const bas = noeud.id !== undefined ? this.basDesBlocs.get(noeud.id) : undefined;
|
|
937
938
|
if (bas) {
|
|
@@ -947,7 +948,7 @@ ${prise}${code}}
|
|
|
947
948
|
case 'formulaire': {
|
|
948
949
|
// Entrée ou le bouton envoient le formulaire, sans recharger la page.
|
|
949
950
|
const soumission = this.formulaires.get(noeud.id)?.soumission ?? '() => {}';
|
|
950
|
-
const ouverture = `creer('form', { 'data-humalis': ${chaine(noeud.id)}, class: 'humalis-formulaire', novalidate: true, onsubmit: ${soumission} }, ...`;
|
|
951
|
+
const ouverture = `creer('form', { ${this.repereObjetJs(noeud.ligne)}'data-humalis': ${chaine(noeud.id)}, class: 'humalis-formulaire', novalidate: true, onsubmit: ${soumission} }, ...`;
|
|
951
952
|
if (this.montres.has(noeud.id) || this.caches.has(noeud.id)) {
|
|
952
953
|
this.ecrireGroupe(e, noeud.corps, `...(visibilite[${chaine(noeud.id)}] ? [${ouverture}`, ')] : []),');
|
|
953
954
|
}
|
|
@@ -970,7 +971,7 @@ ${prise}${code}}
|
|
|
970
971
|
/** `creer('p', { … }, contenu)`. */
|
|
971
972
|
elementJs(noeud) {
|
|
972
973
|
const commun = (avant = []) => {
|
|
973
|
-
const liste = [...avant, ["'data-humalis'", chaine(noeud.id)]];
|
|
974
|
+
const liste = [...this.repereJs(noeud.ligne), ...avant, ["'data-humalis'", chaine(noeud.id)]];
|
|
974
975
|
const classes = this.classes(noeud);
|
|
975
976
|
if (classes)
|
|
976
977
|
liste.push(['class', chaine(classes)]);
|
|
@@ -1051,7 +1052,7 @@ ${prise}${code}}
|
|
|
1051
1052
|
const parmi = saisie?.parmi ? expressionJs(saisie.parmi, this.contexte) : '[]';
|
|
1052
1053
|
const radio = `creer('input', { type: 'radio', name: ${chaine(noeud.id)}, value: valeur, checked: ${variable} === valeur, oninput: () => { ${variable} = valeur; } })`;
|
|
1053
1054
|
const classes = this.classes(noeud);
|
|
1054
|
-
const groupe = [["'data-humalis'", chaine(noeud.id)], ['class', chaine(classes ? `${classes} humalis-choix` : 'humalis-choix')]];
|
|
1055
|
+
const groupe = [...this.repereJs(noeud.ligne), ["'data-humalis'", chaine(noeud.id)], ['class', chaine(classes ? `${classes} humalis-choix` : 'humalis-choix')]];
|
|
1055
1056
|
return `creer('fieldset', ${objet(groupe)}, creer('legend', {}, ${libelle}), ...${parmi}.map((valeur) => creer('label', {}, ${radio}, valeur)))`;
|
|
1056
1057
|
}
|
|
1057
1058
|
case 'liste': {
|
|
@@ -1187,7 +1188,7 @@ ${prise}${code}}
|
|
|
1187
1188
|
case 'bloc': {
|
|
1188
1189
|
if (noeud.genre === 'liste' || noeud.genre === 'listeNumerotee') {
|
|
1189
1190
|
const balise = noeud.genre === 'liste' ? 'ul' : 'ol';
|
|
1190
|
-
lignes.push(`<${balise} class="humalis-liste">`);
|
|
1191
|
+
lignes.push(`<${balise}${attributs([this.repereHtml(noeud.ligne)])} class="humalis-liste">`);
|
|
1191
1192
|
for (const enfant of noeud.corps) {
|
|
1192
1193
|
if (enfant.type === 'repetition') {
|
|
1193
1194
|
const valeurs = evaluer(enfant.liste, env);
|
|
@@ -1210,7 +1211,7 @@ ${prise}${code}}
|
|
|
1210
1211
|
break;
|
|
1211
1212
|
}
|
|
1212
1213
|
const balise = BALISES_BLOC[noeud.genre] ?? 'div';
|
|
1213
|
-
lignes.push(`<${balise}${attributs([['class', classesDuBloc(noeud)]])}>`);
|
|
1214
|
+
lignes.push(`<${balise}${attributs([this.repereHtml(noeud.ligne), ['class', classesDuBloc(noeud)]])}>`);
|
|
1214
1215
|
if (estRepliable(noeud))
|
|
1215
1216
|
lignes.push('<button type="button" class="humalis-menu-bouton" aria-expanded="false" aria-label="Menu"><span class="humalis-menu-icone" aria-hidden="true"></span></button>');
|
|
1216
1217
|
this.ecrireGroupeHtml(noeud.corps, env, lignes);
|
|
@@ -1227,7 +1228,7 @@ ${prise}${code}}
|
|
|
1227
1228
|
// Un formulaire que l'on montre est caché au départ.
|
|
1228
1229
|
if (this.montres.has(noeud.id))
|
|
1229
1230
|
break;
|
|
1230
|
-
lignes.push(`<form${attributs([['data-humalis', noeud.id], ['class', 'humalis-formulaire'], ['novalidate', 'true']])}>`);
|
|
1231
|
+
lignes.push(`<form${attributs([this.repereHtml(noeud.ligne), ['data-humalis', noeud.id], ['class', 'humalis-formulaire'], ['novalidate', 'true']])}>`);
|
|
1231
1232
|
this.ecrireGroupeHtml(noeud.corps, env, lignes);
|
|
1232
1233
|
lignes.push('</form>');
|
|
1233
1234
|
break;
|
|
@@ -1508,7 +1509,7 @@ ${prise}${code}}
|
|
|
1508
1509
|
const saisie = noeud.saisie;
|
|
1509
1510
|
const sorte = saisie?.sorte ?? 'texte';
|
|
1510
1511
|
const libelle = saisie?.obligatoire ? `<span>${contenu}${ETOILE_HTML}</span>` : contenu;
|
|
1511
|
-
const base = [['data-humalis', noeud.id], ['class', classes]];
|
|
1512
|
+
const base = [this.repereHtml(noeud.ligne), ['data-humalis', noeud.id], ['class', classes]];
|
|
1512
1513
|
const options = (liste) => [
|
|
1513
1514
|
...liste,
|
|
1514
1515
|
['required', saisie?.obligatoire ? 'true' : undefined],
|
|
@@ -1527,7 +1528,7 @@ ${prise}${code}}
|
|
|
1527
1528
|
const radios = parmi()
|
|
1528
1529
|
.map((v) => `<label><input${attributs([['type', 'radio'], ['name', noeud.id], ['value', v]])}>${echapperTexte(v)}</label>`)
|
|
1529
1530
|
.join('');
|
|
1530
|
-
return `<fieldset${attributs([['data-humalis', noeud.id], ['class', classes ? `${classes} humalis-choix` : 'humalis-choix']])}><legend>${libelle}</legend>${radios}</fieldset>`;
|
|
1531
|
+
return `<fieldset${attributs([this.repereHtml(noeud.ligne), ['data-humalis', noeud.id], ['class', classes ? `${classes} humalis-choix` : 'humalis-choix']])}><legend>${libelle}</legend>${radios}</fieldset>`;
|
|
1531
1532
|
}
|
|
1532
1533
|
case 'liste': {
|
|
1533
1534
|
const choix = parmi().map((v) => `<option${attributs([['value', v]])}>${echapperTexte(v)}</option>`).join('');
|
|
@@ -1542,9 +1543,22 @@ ${prise}${code}}
|
|
|
1542
1543
|
}
|
|
1543
1544
|
}
|
|
1544
1545
|
}
|
|
1546
|
+
/** Le mode développement : le fichier et la ligne Humalis de l'élément, en HTML (attributs() l'échappe ; absent sans le mode). */
|
|
1547
|
+
repereHtml(ligne) {
|
|
1548
|
+
return ['data-humalis-source', this.site.reperes ? repereSource(ligne, this.site.reperes) : undefined];
|
|
1549
|
+
}
|
|
1550
|
+
/** Le même repère pour creer() : rien sans le mode. */
|
|
1551
|
+
repereJs(ligne) {
|
|
1552
|
+
const repere = this.site.reperes ? repereSource(ligne, this.site.reperes) : undefined;
|
|
1553
|
+
return repere === undefined ? [] : [["'data-humalis-source'", chaine(repere)]];
|
|
1554
|
+
}
|
|
1555
|
+
/** `'data-humalis-source': '…', ` au début d'un objet d'attributs écrit à la main. */
|
|
1556
|
+
repereObjetJs(ligne) {
|
|
1557
|
+
return this.repereJs(ligne).map(([nom, valeur]) => `${nom}: ${valeur}, `).join('');
|
|
1558
|
+
}
|
|
1545
1559
|
elementHtml(noeud, env) {
|
|
1546
1560
|
const classes = this.classes(noeud) || undefined;
|
|
1547
|
-
const commun = (avant = []) => attributs([...avant, ['data-humalis', noeud.id], ['class', classes], ...(noeud.annonce ? [['aria-live', 'polite'], ['aria-atomic', 'true']] : []), ...attributsRaccourci(noeud)]);
|
|
1561
|
+
const commun = (avant = []) => attributs([this.repereHtml(noeud.ligne), ...avant, ['data-humalis', noeud.id], ['class', classes], ...(noeud.annonce ? [['aria-live', 'polite'], ['aria-atomic', 'true']] : []), ...attributsRaccourci(noeud)]);
|
|
1548
1562
|
const lucideHtml = noeud.reglages?.icone ? chercherIcone(noeud.reglages.icone)?.lucide : undefined;
|
|
1549
1563
|
const contenu = (lucideHtml ? iconeHtml(lucideHtml, 'humalis-icone') : '') + this.contenuAccentHtml(noeud.contenu, noeud.genre, env);
|
|
1550
1564
|
switch (noeud.genre) {
|
|
@@ -14,7 +14,7 @@ import { cssGraphique } from '../web/graphique.js';
|
|
|
14
14
|
import { cssEcran } from '../web/css-ecran.js';
|
|
15
15
|
import { cssElements } from '../web/css-elements.js';
|
|
16
16
|
import { cssInteractions } from '../web/interactions.js';
|
|
17
|
-
import { cssMouvement, animeAuDefilement, OBSERVATEUR_DEFILEMENT } from '../commun/mouvement.js';
|
|
17
|
+
import { cssMouvement, animeAuDefilement, OBSERVATEUR_DEFILEMENT, SUIVI_ENTETE, suitLEntete } from '../commun/mouvement.js';
|
|
18
18
|
import { cssTheme, changeLeTheme, modeParDefaut, SCRIPT_THEME, CLE_THEME } from '../web/theme.js';
|
|
19
19
|
import { FORMAT_JS, FORMAT_JS_LANGUES, TAILLE_JS, DATES_JS } from '../format.js';
|
|
20
20
|
import { CLE_LANGUE } from '../web/langue.js';
|
|
@@ -48,6 +48,7 @@ export function genererProjetStatique(application, options) {
|
|
|
48
48
|
composants: new Map(application.composants.map((c) => [c.nom, c])),
|
|
49
49
|
medias: options.medias,
|
|
50
50
|
...(application.langues ? { langues: application.langues } : {}),
|
|
51
|
+
...(options.sources ? { reperes: options.sources } : {}),
|
|
51
52
|
};
|
|
52
53
|
const langue = application.langues?.[0].code;
|
|
53
54
|
// Le CSS de l'utilisateur, recopié tel quel (décision W8), chargé après
|
|
@@ -137,6 +138,10 @@ ${fonctions.join('\n')}`);
|
|
|
137
138
|
if ([...fichiers.values()].some((contenu) => contenu.includes('humalis-menu-bouton'))) {
|
|
138
139
|
fichiers.set('humalis/humalis.js', `${fichiers.get('humalis/humalis.js')}\n${MENU_REPLIABLE_JS}`);
|
|
139
140
|
}
|
|
141
|
+
// « entete avec fixe », « hauteur ecran » : la hauteur de l'en-tête, suivie par un petit script.
|
|
142
|
+
if (suitLEntete([...fichiers.values()])) {
|
|
143
|
+
fichiers.set('humalis/humalis.js', `${fichiers.get('humalis/humalis.js')}\n// La hauteur de l'en-tête, pour « entete avec fixe » et « hauteur ecran ».\n${SUIVI_ENTETE}\nsuivreLaHauteurDeLEntete();\n`);
|
|
144
|
+
}
|
|
140
145
|
// L'observateur des animations « au defilement », seulement si une page en a : il pose humalis-js au chargement.
|
|
141
146
|
if (animeAuDefilement([...fichiers.values()])) {
|
|
142
147
|
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`);
|
|
@@ -24,7 +24,7 @@ const ANGLES = { diagonale: '135deg', vertical: '180deg', horizontal: '90deg' };
|
|
|
24
24
|
/** Les classes `humalis-<réglage>-<valeur>` qui apparaissent dans le code produit. */
|
|
25
25
|
export function reglagesUtilises(codes) {
|
|
26
26
|
const trouves = new Set();
|
|
27
|
-
const motif = /humalis-(colonnes|espacement|remplissage|alignement|repartition|largeur|ombre|degrade|colle|bordure|sur)-([a-z0-9_-]+)/g;
|
|
27
|
+
const motif = /humalis-(colonnes|espacement|remplissage|alignement|repartition|largeur|ombre|degrade|colle|bordure|sur|defilement|hauteur|fixe)-([a-z0-9_-]+)/g;
|
|
28
28
|
for (const code of codes)
|
|
29
29
|
for (const m of code.matchAll(motif))
|
|
30
30
|
trouves.add(`${m[1]}-${m[2]}`);
|
|
@@ -130,6 +130,24 @@ export function cssEcran(codes) {
|
|
|
130
130
|
case 'sur':
|
|
131
131
|
appareil = true;
|
|
132
132
|
break;
|
|
133
|
+
case 'defilement':
|
|
134
|
+
// « defilement interne » : le bloc défile seul, sans entraîner la page ; une barre fine.
|
|
135
|
+
if (valeur === 'interne')
|
|
136
|
+
regles.push(`${classe} { overflow-y: auto; overscroll-behavior: contain; min-height: 0; max-height: 100%; align-self: stretch; scrollbar-width: thin; }`);
|
|
137
|
+
break;
|
|
138
|
+
case 'hauteur':
|
|
139
|
+
// « hauteur ecran » : l'écran moins l'en-tête (mesuré par le script, 0 sans en-tête fixe ou collé).
|
|
140
|
+
if (valeur === 'ecran')
|
|
141
|
+
regles.push(`${classe} { height: calc(100dvh - var(--humalis-hauteur-entete, 0px)); min-height: 0; overflow: auto; }`);
|
|
142
|
+
break;
|
|
143
|
+
case 'fixe':
|
|
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.
|
|
146
|
+
regles.push(`header${classe} { position: fixed; top: 0; left: 0; right: 0; z-index: 50; background: var(--humalis-page); }`);
|
|
147
|
+
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('body:has(header.humalis-fixe-oui) { padding-top: var(--humalis-hauteur-entete, 0px); }');
|
|
150
|
+
break;
|
|
133
151
|
}
|
|
134
152
|
}
|
|
135
153
|
if (appareil) {
|
package/dist/cibles/web/ecran.js
CHANGED
|
@@ -13,6 +13,7 @@ import { typeTS, typeDe, expression, contenuJSX, condition, ficheAvec } from './
|
|
|
13
13
|
import { importsDesInteractions, decrireTouches } from './interactions.js';
|
|
14
14
|
import { colorerCodeHumalis } from '../commun/colorerCode.js';
|
|
15
15
|
import { attributsFrappe } from '../commun/mouvement.js';
|
|
16
|
+
import { repereSource, attributSourceJsx, marquerBalise } from '../commun/reperes.js';
|
|
16
17
|
/** Les sortes de champ où l'on écrit une seule ligne : la touche Entrée y envoie le formulaire. */
|
|
17
18
|
const UNE_LIGNE = new Set(['texte', 'email', 'telephone', 'nombre', 'date', 'heure', 'motDePasse', 'adresseWeb']);
|
|
18
19
|
/** Un texte écrit en clair dans du JSX : tel quel, ou entre accolades s'il contient un caractère du JSX. */
|
|
@@ -1621,7 +1622,7 @@ class GenerateurEcran {
|
|
|
1621
1622
|
break;
|
|
1622
1623
|
}
|
|
1623
1624
|
const balise = noeud.genre === 'liste' ? 'ul' : 'ol';
|
|
1624
|
-
e.ligne(`<${balise} className="humalis-liste"
|
|
1625
|
+
e.ligne(this.marquer(`<${balise} className="humalis-liste">`, noeud.ligne));
|
|
1625
1626
|
e.indenter(() => noeud.corps.forEach((enfant) => {
|
|
1626
1627
|
if (enfant.type === 'repetition') {
|
|
1627
1628
|
this.listeEnCours = noeud.genre;
|
|
@@ -1640,7 +1641,7 @@ class GenerateurEcran {
|
|
|
1640
1641
|
this.ecrireComposite(e, noeud);
|
|
1641
1642
|
break;
|
|
1642
1643
|
}
|
|
1643
|
-
e.ligne(this.rendu.bloc.ouvrir(noeud.genre, noeud.options, noeud.classes, noeud.ancre));
|
|
1644
|
+
e.ligne(this.marquer(this.rendu.bloc.ouvrir(noeud.genre, noeud.options, noeud.classes, noeud.ancre), noeud.ligne));
|
|
1644
1645
|
// Des variables dans le bloc : écrites comme dans une condition.
|
|
1645
1646
|
e.indenter(() => noeud.corps.some((n) => n.type === 'locale') ? this.ecrireGroupe(e, noeud.corps) : this.ecrireVue(e, noeud.corps));
|
|
1646
1647
|
e.ligne(this.rendu.bloc.fermer(noeud.genre, noeud.options));
|
|
@@ -1668,7 +1669,7 @@ class GenerateurEcran {
|
|
|
1668
1669
|
break;
|
|
1669
1670
|
case 'formulaire': {
|
|
1670
1671
|
const ecrire = () => {
|
|
1671
|
-
e.ligne(this.rendu.formulaire.ouvrir(noeud.id, this.formulaires.get(noeud.id)?.soumission));
|
|
1672
|
+
e.ligne(this.marquer(this.rendu.formulaire.ouvrir(noeud.id, this.formulaires.get(noeud.id)?.soumission), noeud.ligne));
|
|
1672
1673
|
// Des variables dans le bloc : écrites comme dans une condition.
|
|
1673
1674
|
e.indenter(() => noeud.corps.some((n) => n.type === 'locale') ? this.ecrireGroupe(e, noeud.corps) : this.ecrireVue(e, noeud.corps));
|
|
1674
1675
|
e.ligne(this.rendu.formulaire.fermer);
|
|
@@ -1876,6 +1877,11 @@ class GenerateurEcran {
|
|
|
1876
1877
|
...(image.prioritaire ? { prioritaire: true } : {}),
|
|
1877
1878
|
};
|
|
1878
1879
|
}
|
|
1880
|
+
/** Le mode développement (le web seulement) : la balise ouvrante porte le fichier et la ligne Humalis. */
|
|
1881
|
+
marquer(balise, ligne) {
|
|
1882
|
+
const repere = this.application.reperes && !this.application.rendu ? repereSource(ligne, this.application.reperes) : undefined;
|
|
1883
|
+
return repere === undefined ? balise : marquerBalise(balise, attributSourceJsx(repere));
|
|
1884
|
+
}
|
|
1879
1885
|
ecrireElement(e, noeud) {
|
|
1880
1886
|
const parAttribut = new Map(this.attributs.get(noeud.id) ?? []);
|
|
1881
1887
|
// Dans un formulaire : un bouton sans clic propre l'envoie, une saisie d'une ligne aussi (touche du clavier).
|
|
@@ -1914,7 +1920,7 @@ class GenerateurEcran {
|
|
|
1914
1920
|
const source = texteLitteral(noeud.contenu);
|
|
1915
1921
|
const variable = this.contexte.champs.get(noeud.id) ?? '';
|
|
1916
1922
|
const ouvert = this.ouvertes.get(noeud.id);
|
|
1917
|
-
const balise = this.rendu.element({
|
|
1923
|
+
const balise = this.marquer(this.rendu.element({
|
|
1918
1924
|
genre: noeud.genre,
|
|
1919
1925
|
id: noeud.id,
|
|
1920
1926
|
options: noeud.options,
|
|
@@ -1949,7 +1955,7 @@ class GenerateurEcran {
|
|
|
1949
1955
|
},
|
|
1950
1956
|
envoie: noeud.genre === 'bouton' && formulaire !== undefined && !aSonClic,
|
|
1951
1957
|
...(noeud.image ? { image: this.image(noeud.image, noeud.contenu) } : {}),
|
|
1952
|
-
});
|
|
1958
|
+
}), noeud.ligne);
|
|
1953
1959
|
if (this.montres.has(noeud.id) || this.caches.has(noeud.id)) {
|
|
1954
1960
|
e.ligne(`{visibilite[${chaine(noeud.id)}] && ${balise}}`);
|
|
1955
1961
|
}
|
|
@@ -51,6 +51,9 @@ ${entete(langues)}const CLE = ${chaine(CLE_LANGUE)};
|
|
|
51
51
|
*/
|
|
52
52
|
function langueDeDepart(): Langue {
|
|
53
53
|
if (typeof window === 'undefined') return LANGUES[0];
|
|
54
|
+
// Le prérendu (un navigateur automatisé) écrit les pages dans la langue de base du site,
|
|
55
|
+
// celle que les moteurs de recherche doivent lire, quelle que soit la langue de ce navigateur.
|
|
56
|
+
if (navigator.webdriver) return LANGUES[0];
|
|
54
57
|
try {
|
|
55
58
|
const gardee = localStorage.getItem(CLE);
|
|
56
59
|
if (estLangue(gardee)) return gardee;
|
|
@@ -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 } from '../commun/mouvement.js';
|
|
6
|
+
import { cssMouvement, animeAuDefilement, MOUVEMENT_WEB, NETTOYAGE_PRERENDU, 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';
|
|
@@ -79,6 +79,7 @@ export function genererProjetWeb(application, options) {
|
|
|
79
79
|
// Le bloc « site » ou un « apercu » : chaque page pose ses balises de partage.
|
|
80
80
|
...(application.site || application.ecrans.some((e) => e.apercu) ? { publication: { site: application.site, adresse: options.adresse } } : {}),
|
|
81
81
|
...(application.langues ? { langues: application.langues } : {}),
|
|
82
|
+
...(options.sources ? { reperes: options.sources } : {}),
|
|
82
83
|
};
|
|
83
84
|
let session = false;
|
|
84
85
|
for (const c of application.composants) {
|
|
@@ -199,6 +200,10 @@ export function genererProjetWeb(application, options) {
|
|
|
199
200
|
const defilementAnime = animeAuDefilement(codes);
|
|
200
201
|
if (defilementAnime)
|
|
201
202
|
fichiers.set('src/humalis/mouvement.ts', MOUVEMENT_WEB);
|
|
203
|
+
// « entete avec fixe », « hauteur ecran » : la hauteur de l'en-tête, suivie par un petit script.
|
|
204
|
+
const enteteSuivie = suitLEntete(codes);
|
|
205
|
+
if (enteteSuivie)
|
|
206
|
+
fichiers.set('src/humalis/entete.ts', ENTETE_WEB);
|
|
202
207
|
// Le CSS de l'utilisateur, recopié tel quel (décision W8), chargé après
|
|
203
208
|
// celui de Humalis pour pouvoir le redéfinir.
|
|
204
209
|
const feuilles = [];
|
|
@@ -225,7 +230,7 @@ export function genererProjetWeb(application, options) {
|
|
|
225
230
|
...(ecran.reservee ? { reservee: { qui: ecran.reservee.qui, sinon: chemins.get(ecran.reservee.sinon) ?? '/' } } : {}),
|
|
226
231
|
sansEntete: ecran.sansEntete,
|
|
227
232
|
sansPied: ecran.sansPied,
|
|
228
|
-
})), feuilles, application.partages.length > 0, session, introuvable?.page.composant, dispositions, utiliseDefilement, defilementAnime));
|
|
233
|
+
})), feuilles, application.partages.length > 0, session, introuvable?.page.composant, dispositions, utiliseDefilement, defilementAnime, enteteSuivie));
|
|
229
234
|
for (const { page } of pages)
|
|
230
235
|
fichiers.set(`src/pages/${page.composant}.tsx`, page.code);
|
|
231
236
|
// Les images locales, recopiées là où le site les cherche (/images/…) : décision M5.
|
|
@@ -506,7 +511,7 @@ function lignesDeRoute(r, toutes, niveau, cheminParent) {
|
|
|
506
511
|
lignes.push(`${espaces(0)}</Route>`);
|
|
507
512
|
return lignes;
|
|
508
513
|
}
|
|
509
|
-
function mainTsx(routes, feuilles, partage, session, introuvable, dispositions = {}, defilement = false, mouvement = false) {
|
|
514
|
+
function mainTsx(routes, feuilles, partage, session, introuvable, dispositions = {}, defilement = false, mouvement = false, entete = false) {
|
|
510
515
|
const imports = routes.map((r) => `import ${r.composant} from './pages/${r.composant}';`).join('\n');
|
|
511
516
|
// Les pages qui ont le même en-tête et le même pied partagent une disposition ; une sous-page suit son parent.
|
|
512
517
|
const groupes = new Map();
|
|
@@ -532,7 +537,7 @@ import { Messages } from './humalis/messages';${defilement ? "\nimport { Defilem
|
|
|
532
537
|
${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';"}
|
|
533
538
|
${dispositions.entete || dispositions.pied ? "import Disposition from './Disposition';\n" : ''}${imports}
|
|
534
539
|
import './humalis/styles.css';
|
|
535
|
-
${mouvement ? "import { observerLeDefilement } from './humalis/mouvement';\n" : ''}${feuilles.map((f) => `import './styles/${f}';\n`).join('')}
|
|
540
|
+
${mouvement ? "import { observerLeDefilement } from './humalis/mouvement';\n" : ''}${entete ? "import { suivreLaHauteurDeLEntete } from './humalis/entete';\n" : ''}${feuilles.map((f) => `import './styles/${f}';\n`).join('')}
|
|
536
541
|
// Un hébergeur statique sert /documentation/index.html pour /documentation/ :
|
|
537
542
|
// le routeur lit l'adresse sans « index.html », comme le dossier.
|
|
538
543
|
if (/\\/index\\.html$/.test(location.pathname)) {
|
|
@@ -548,7 +553,7 @@ ${routesTsx}
|
|
|
548
553
|
</BrowserRouter>
|
|
549
554
|
</StrictMode>,
|
|
550
555
|
);
|
|
551
|
-
${mouvement ? '\n// Les animations « au defilement » attendent que l\'élément entre à l\'écran.\nobserverLeDefilement();\n' : ''}`;
|
|
556
|
+
${mouvement ? '\n// Les animations « au defilement » attendent que l\'élément entre à l\'écran.\nobserverLeDefilement();\n' : ''}${entete ? '\n// « entete avec fixe », « hauteur ecran » : la hauteur de l\'en-tête.\nsuivreLaHauteurDeLEntete();\n' : ''}`;
|
|
552
557
|
}
|
|
553
558
|
/**
|
|
554
559
|
* Les aides de navigation du code produit : le lien « actif », le défilement
|
package/dist/cli.js
CHANGED
|
@@ -163,11 +163,16 @@ function compile(arguments_) {
|
|
|
163
163
|
const medias = lireMedias(application, chemin);
|
|
164
164
|
if (!medias)
|
|
165
165
|
return 1;
|
|
166
|
-
|
|
166
|
+
// --diagnostics-port n'est passé que par « humalis developper » : le mode développement, où chaque
|
|
167
|
+
// élément porte son fichier et sa ligne Humalis (data-humalis-source). Jamais dans une compilation normale.
|
|
168
|
+
const portDiagnostics = option(arguments_, 'diagnostics-port');
|
|
169
|
+
const optionsWeb = {
|
|
170
|
+
nom, adresse: adresse ?? 'http://localhost', source: basename(chemin), fichiersStyle, composantsReact, polices, images, medias,
|
|
171
|
+
...(portDiagnostics !== undefined ? { sources: fichiersDuProjet } : {}),
|
|
172
|
+
};
|
|
167
173
|
const fichiers = composants
|
|
168
174
|
? genererComposantsWeb(application, optionsWeb)
|
|
169
175
|
: genererProjetWeb(application, optionsWeb);
|
|
170
|
-
const portDiagnostics = option(arguments_, 'diagnostics-port');
|
|
171
176
|
if (portDiagnostics !== undefined && fichiers.has('index.html')) {
|
|
172
177
|
const index = fichiers.get('index.html');
|
|
173
178
|
fichiers.set('index.html', index.replace('</body>', `<script type="module" src="http://localhost:${portDiagnostics}/diagnostics.js"></script></body>`));
|
package/dist/fichiers.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ErreurSyntaxique } from './parser/erreurs.js';
|
|
|
5
5
|
import { lireCsv } from './donnees/csv.js';
|
|
6
6
|
import { lireJson } from './donnees/json.js';
|
|
7
7
|
import { analyserSens } from './analyse/analyse.js';
|
|
8
|
+
import { verifierCss } from './analyse/css.js';
|
|
8
9
|
import { construireRepresentation } from './ri/construire.js';
|
|
9
10
|
/**
|
|
10
11
|
* Plusieurs fichiers (trou G4) : les lignes du k-ième fichier utilisé sont
|
|
@@ -158,7 +159,26 @@ export function compilerProjet(principal, lire, sourcePrincipale) {
|
|
|
158
159
|
}
|
|
159
160
|
if (diagnostics.some((d) => d.gravite === 'erreur'))
|
|
160
161
|
return fin();
|
|
161
|
-
const
|
|
162
|
+
const classesCss = new Set();
|
|
163
|
+
let cssIllisible = false;
|
|
164
|
+
for (const bloc of programme.styles) {
|
|
165
|
+
const resultat = verifierCss(bloc.css, bloc.ligne + 1);
|
|
166
|
+
for (const nom of resultat.classes)
|
|
167
|
+
classesCss.add(nom);
|
|
168
|
+
diagnostics.push(...resultat.diagnostics);
|
|
169
|
+
}
|
|
170
|
+
for (const fichier of programme.fichiersStyle) {
|
|
171
|
+
const contenu = lire(joindre(base, fichier.chemin));
|
|
172
|
+
if (contenu === undefined) {
|
|
173
|
+
cssIllisible = true;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const resultat = verifierCss(contenu, 1, fichier.chemin.replace(/^\.\//, ''));
|
|
177
|
+
for (const nom of resultat.classes)
|
|
178
|
+
classesCss.add(nom);
|
|
179
|
+
diagnostics.push(...resultat.diagnostics);
|
|
180
|
+
}
|
|
181
|
+
const analyse = analyserSens(programme, cssIllisible ? undefined : classesCss);
|
|
162
182
|
diagnostics.push(...analyse.diagnostics);
|
|
163
183
|
if (diagnostics.some((d) => d.gravite === 'erreur'))
|
|
164
184
|
return fin();
|