@komekoo/astro-cms 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +144 -2
  2. package/dist/ajv.d.ts +4 -0
  3. package/dist/ajv.js +24 -0
  4. package/dist/apercu/arbre.d.ts +3 -0
  5. package/dist/apercu/arbre.js +64 -0
  6. package/dist/apercu/demarrage.d.ts +1 -0
  7. package/dist/apercu/demarrage.js +5 -0
  8. package/dist/apercu/index.d.ts +2 -0
  9. package/dist/apercu/index.js +2 -0
  10. package/dist/apercu/protocole.d.ts +67 -0
  11. package/dist/apercu/protocole.js +176 -0
  12. package/dist/apercu/script.d.ts +8 -0
  13. package/dist/apercu/script.js +321 -0
  14. package/dist/champs.d.ts +49 -0
  15. package/dist/champs.js +58 -0
  16. package/dist/cli.d.ts +3 -0
  17. package/dist/cli.js +70 -0
  18. package/dist/collections.d.ts +39 -0
  19. package/dist/collections.js +57 -0
  20. package/dist/images.d.ts +8 -0
  21. package/dist/images.js +19 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.js +10 -0
  24. package/dist/integration.d.ts +9 -0
  25. package/dist/integration.js +113 -0
  26. package/dist/marqueur.d.ts +4 -0
  27. package/dist/marqueur.js +8 -0
  28. package/dist/motifs.d.ts +10 -0
  29. package/dist/motifs.js +11 -0
  30. package/dist/navigateur.d.ts +6 -0
  31. package/dist/navigateur.js +6 -0
  32. package/dist/noyau/apercu-build.d.ts +12 -0
  33. package/dist/noyau/apercu-build.js +74 -0
  34. package/dist/noyau/charger.d.ts +4 -0
  35. package/dist/noyau/charger.js +48 -0
  36. package/dist/noyau/collecter.d.ts +27 -0
  37. package/dist/noyau/collecter.js +133 -0
  38. package/dist/noyau/contenus.d.ts +6 -0
  39. package/dist/noyau/contenus.js +218 -0
  40. package/dist/noyau/erreurs.d.ts +7 -0
  41. package/dist/noyau/erreurs.js +14 -0
  42. package/dist/noyau/frontmatter.d.ts +15 -0
  43. package/dist/noyau/frontmatter.js +70 -0
  44. package/dist/noyau/json-schema.d.ts +4 -0
  45. package/dist/noyau/json-schema.js +61 -0
  46. package/dist/noyau/manifest.d.ts +16 -0
  47. package/dist/noyau/manifest.js +136 -0
  48. package/dist/noyau/markdown.d.ts +12 -0
  49. package/dist/noyau/markdown.js +95 -0
  50. package/dist/noyau/marqueurs.d.ts +11 -0
  51. package/dist/noyau/marqueurs.js +57 -0
  52. package/dist/noyau/operations.d.ts +12 -0
  53. package/dist/noyau/operations.js +54 -0
  54. package/dist/noyau/serialiser.d.ts +2 -0
  55. package/dist/noyau/serialiser.js +25 -0
  56. package/dist/noyau/version.d.ts +5 -0
  57. package/dist/noyau/version.js +25 -0
  58. package/dist/socle/hors-perimetre.d.ts +2 -0
  59. package/dist/socle/hors-perimetre.js +9 -0
  60. package/dist/types.d.ts +34 -0
  61. package/dist/types.js +10 -0
  62. package/package.json +62 -3
  63. package/src/composants/CmsImage.astro +26 -0
package/README.md CHANGED
@@ -1,3 +1,145 @@
1
- # Temporary Holding Version
1
+ # @komekoo/astro-cms
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ CMS git pour sites Astro : les contenus sont des fichiers JSON et Markdown dans `src/content/`, décrits par des
4
+ champs typés (`champ.*`). Le paquet génère le schéma CMS (`cms.schema.json`) et le manifeste destiné aux agents IA
5
+ (`ai/manifest.json`), et vérifie les contenus avant chaque `astro build`.
6
+
7
+ ## Installation
8
+
9
+ Prérequis : Node ≥ 22.23.4, Astro ^7.4.2.
10
+
11
+ ```sh
12
+ pnpm add @komekoo/astro-cms
13
+ ```
14
+
15
+ ## Déclarer les contenus : `src/content.config.ts`
16
+
17
+ ```ts
18
+ // N'importe pas `astro:content` ici : komekoo-cms charge ce fichier hors d'Astro (sync / check).
19
+ import { champ, defineCollection, defineSingleton } from '@komekoo/astro-cms';
20
+
21
+ // Singleton : un seul fichier, src/content/pages/accueil.json
22
+ const accueil = defineSingleton('accueil', {
23
+ libelle: "Page d'accueil",
24
+ schema: {
25
+ titre: champ.texte({ libelle: 'Titre principal', max: 80 }),
26
+ image: champ.image({ libelle: 'Image principale' }),
27
+ presentation: champ.richtext({ libelle: 'Présentation' }),
28
+ reservation: champ.lien({ libelle: 'Lien de réservation', obligatoire: false }),
29
+ },
30
+ });
31
+
32
+ // Collection : un fichier par élément, dans src/content/tarifs/
33
+ const tarifs = defineCollection('tarifs', {
34
+ libelle: 'Tarifs',
35
+ format: 'json', // ou 'markdown' (frontmatter YAML + corps)
36
+ titre: 'prestation', // champ texte servant de titre dans l'admin
37
+ ordonnable: true, // ajoute un champ `ordre` (entier ≥ 0)
38
+ schema: {
39
+ prestation: champ.texte({ libelle: 'Prestation', max: 80 }),
40
+ categorie: champ.choix({ libelle: 'Catégorie', options: ['Femme', 'Homme', 'Enfant'] }),
41
+ prix: champ.nombre({ libelle: 'Prix', unite: '€', min: 0 }),
42
+ },
43
+ });
44
+
45
+ export const collections = { accueil, tarifs };
46
+ ```
47
+
48
+ Champs disponibles : `texte`, `paragraphe`, `richtext`, `image`, `date`, `lien`, `nombre`, `booleen`, `choix`,
49
+ `liste`, `groupe`. Chacun prend `libelle`, `aide` (facultatif) et `obligatoire` (`true` par défaut). Les types
50
+ TypeScript suivent la déclaration : un champ `obligatoire: false` est typé `T | undefined`, `choix` donne l'union
51
+ de ses options.
52
+
53
+ - `onglet` (facultatif, texte non vide) : range le champ dans un onglet du formulaire d'administration, par ex.
54
+ `onglet: 'Référencement Google'`.
55
+ - `adresse` (option de `defineSingleton`, facultative) : adresse publique de la page, par ex. `adresse: '/a-propos'`
56
+ (`/` suivi de minuscules, chiffres, `-` ou `/`). Elle permet à l'admin d'ouvrir l'aperçu de la page.
57
+ - Images : `src` de la forme `/uploads/…/nom-2400.webp`, avec les variantes `-800.webp`, `-1600.webp` et
58
+ `-2400.webp` présentes dans `public/uploads/`.
59
+ - Liens : `https://…`, chemin interne `/…`, `mailto:` ou `tel:`.
60
+ - Markdown (`richtext` et corps des articles) : titres 2 et 3, gras, italique, liens, listes, citations, images
61
+ `/uploads/`, tableaux GFM simples. HTML, code, barré, cases à cocher, notes et lignes horizontales sont refusés.
62
+
63
+ ## Intégration : `astro.config.mjs`
64
+
65
+ ```js
66
+ import { defineConfig } from 'astro/config';
67
+ import cms from '@komekoo/astro-cms/integration';
68
+
69
+ export default defineConfig({
70
+ integrations: [
71
+ cms({
72
+ site: 'Salon de coiffure à Vichy', // présentation du site dans ai/manifest.json (obligatoire)
73
+ contact: 'contact@exemple.fr', // contact proposé pour les demandes hors périmètre (obligatoire)
74
+ url: 'https://exemple.fr', // adresse publique du site, https sans « / » final (obligatoire)
75
+ admin: { accent: '#5DEFB7', logo: '/uploads/logo-2400.webp', origine: 'https://admin.exemple.fr', apercu: 'https://apercu.exemple.pages.dev' }, // facultatif
76
+ horsPerimetre: ['Modifier le menu de navigation'], // ajouts à la liste socle (facultatif)
77
+ }),
78
+ ],
79
+ });
80
+ ```
81
+
82
+ - `astro dev` et `astro sync` régénèrent `cms.schema.json` et `ai/manifest.json`.
83
+ - `astro build` vérifie les fichiers générés et tous les contenus, puis échoue avec une ligne
84
+ `fichier:ligne — motif` par problème.
85
+
86
+ ## CLI
87
+
88
+ ```sh
89
+ komekoo-cms sync # génère cms.schema.json et ai/manifest.json
90
+ komekoo-cms check # vérifie les fichiers générés et valide les contenus (code 1 si problème)
91
+ ```
92
+
93
+ ## Point d'entrée navigateur : `@komekoo/astro-cms/navigateur`
94
+
95
+ Pour les outils qui tournent dans un navigateur (ex. l'interface d'administration) : n'importe rien de `node:*`,
96
+ `astro` ni `vite`. Exporte `creerAjv` et `MOTS_CLES_CMS` (validation du schéma CMS), `validerMarkdown` (et le
97
+ type `Violation`), `lireFrontmatter`, `ecrireFrontmatter` (écrit `---`, YAML, `---`, corps ; dates entre
98
+ guillemets), `MOTIF_LIEN` et `MOTIF_SRC_IMAGE`.
99
+
100
+ Options récentes : `url` (obligatoire) et `admin` de `cms()`, `onglet` sur les champs, `adresse` sur les
101
+ singletons, et le bloc `x-cms-site` de `cms.schema.json`.
102
+
103
+ ## Aperçu des pages : `cms()`
104
+
105
+ ```astro
106
+ ---
107
+ import { cms } from '@komekoo/astro-cms';
108
+ ---
109
+ <h1 {...cms('accueil.titre')}>{page.titre}</h1>
110
+ ```
111
+
112
+ `cms('<page>.<champ>')` marque l'élément comme éditable : l'admin le met à jour pendant la saisie et le relie au champ. Chemin : `<page>.<champ>(.<sous-champ>|.<indice>)*`, pages (singletons) uniquement. Au build, chaque marqueur est vérifié contre `cms.schema.json` ; erreur `page HTML — chemin — motif` si le chemin est inconnu, si un champ image n'est pas sur un `<img>` ou si un champ lien n'est pas sur un `<a>`.
113
+
114
+ - `defineSingleton(id, { …, apercu: '/contact' })` : page à montrer pour un singleton sans `adresse` (par ex. les Réglages).
115
+ - `cms({ …, admin: { apercu: 'https://apercu.<projet>.pages.dev' } })` : adresse fixe de la branche d'aperçu ; active « Prévisualiser ».
116
+ - Le build ajoute au site `/_cms/chargeur.js` (balise `<script>` dans chaque page), `/_cms/apercu/` et `/_cms/motifs.js`, et un `_headers` avec `Content-Security-Policy: frame-ancestors 'self' <origine admin>` ; `X-Robots-Tag: noindex` si `CF_PAGES_BRANCH` n'est pas `main`. Un `X-Frame-Options` `DENY` ou `SAMEORIGIN` déjà présent déclenche un avertissement : le retirer. Une CSP propre au site doit autoriser `script-src 'self'`.
117
+ - Sous-chemins sans dépendance à Node : `@komekoo/astro-cms/apercu` (protocole d'échange, sans analyseur Markdown), `@komekoo/astro-cms/apercu/arbre` (`versArbre`, Markdown → arbre d'aperçu, à charger à la demande) et `@komekoo/astro-cms/navigateur`.
118
+
119
+ ## Composant `<CmsImage>`
120
+
121
+ ```astro
122
+ ---
123
+ import CmsImage from '@komekoo/astro-cms/CmsImage.astro';
124
+ ---
125
+ <CmsImage image={page.image} sizes="(min-width: 60rem) 50vw, 100vw" loading="eager" class="heros" />
126
+ ```
127
+
128
+ Produit un `<img>` avec `srcset` sur les variantes 800 / 1600 / 2400, `width`, `height` et `alt`.
129
+ Props : `image` (obligatoire), `sizes` (`100vw` par défaut), `loading` (`lazy` par défaut), `class`, `cms`
130
+ (chemin du marqueur d'aperçu, par ex. `cms="accueil.image"`).
131
+
132
+ ## Limites connues
133
+
134
+ - `src/content.config.ts` ne doit pas importer `astro:content` : il est chargé hors d'Astro par la CLI.
135
+ - Frontmatter TOML (`+++`) refusé : utiliser YAML (`---`). Mettre les dates entre guillemets
136
+ (`date: "2026-10-01"`) ; une date nue est lue comme un horodatage YAML et refusée.
137
+ - Node ≥ 22.23.4 (exigence d'Astro 7.4.2).
138
+ - Les fichiers générés (`cms.schema.json`, `ai/manifest.json`) doivent être committés ; un fichier périmé fait
139
+ échouer `astro build` et `komekoo-cms check`. Pour éviter les conversions CRLF sous Windows, ajouter à
140
+ `.gitattributes` :
141
+
142
+ ```gitattributes
143
+ **/cms.schema.json text eol=lf
144
+ **/ai/manifest.json text eol=lf
145
+ ```
package/dist/ajv.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { Ajv2020 } from 'ajv/dist/2020.js';
2
+ export declare const MOTS_CLES_CMS: readonly string[];
3
+ /** Ajv 2020-12 strict, formats activés, mots-clés `x-cms*` déclarés. */
4
+ export declare function creerAjv(): Ajv2020;
package/dist/ajv.js ADDED
@@ -0,0 +1,24 @@
1
+ import { Ajv2020 } from 'ajv/dist/2020.js';
2
+ import addFormats from 'ajv-formats';
3
+ export const MOTS_CLES_CMS = [
4
+ 'x-cms',
5
+ 'x-cms-version',
6
+ 'x-cms-site',
7
+ 'x-cms-libelle',
8
+ 'x-cms-fichier',
9
+ 'x-cms-adresse',
10
+ 'x-cms-apercu',
11
+ 'x-cms-dossier',
12
+ 'x-cms-format',
13
+ 'x-cms-titre',
14
+ 'x-cms-corps',
15
+ 'x-cms-ordonnable',
16
+ ];
17
+ /** Ajv 2020-12 strict, formats activés, mots-clés `x-cms*` déclarés. */
18
+ export function creerAjv() {
19
+ const ajv = new Ajv2020({ strict: true, allErrors: true });
20
+ addFormats(ajv);
21
+ for (const mot of MOTS_CLES_CMS)
22
+ ajv.addKeyword(mot);
23
+ return ajv;
24
+ }
@@ -0,0 +1,3 @@
1
+ import type { NoeudRiche } from './protocole.js';
2
+ /** Convertit du Markdown en arbre restreint ; tout élément hors du sous-ensemble autorisé devient du texte brut. */
3
+ export declare function versArbre(markdown: string): NoeudRiche[];
@@ -0,0 +1,64 @@
1
+ import { analyseur } from '../noyau/markdown.js';
2
+ import { MOTIF_LIEN, MOTIF_SRC_IMAGE } from '../motifs.js';
3
+ /** Texte brut d'un nœud : sa valeur littérale, ou celle de ses descendants. */
4
+ function toString(noeud) {
5
+ if ('value' in noeud)
6
+ return noeud.value;
7
+ return enfants(noeud).map(toString).join('');
8
+ }
9
+ const enfants = (noeud) => ('children' in noeud ? noeud.children : []);
10
+ function convertirTous(liste) {
11
+ return liste.flatMap(convertir);
12
+ }
13
+ function balise(t, liste) {
14
+ return [{ t, c: convertirTous(liste) }];
15
+ }
16
+ function convertir(noeud) {
17
+ switch (noeud.type) {
18
+ case 'paragraph':
19
+ return balise('p', noeud.children);
20
+ case 'heading':
21
+ return noeud.depth === 2 || noeud.depth === 3
22
+ ? balise(noeud.depth === 2 ? 'h2' : 'h3', noeud.children)
23
+ : [{ t: 'texte', v: toString(noeud) }];
24
+ case 'strong':
25
+ return balise('strong', noeud.children);
26
+ case 'emphasis':
27
+ return balise('em', noeud.children);
28
+ case 'list':
29
+ return balise(noeud.ordered ? 'ol' : 'ul', noeud.children);
30
+ case 'listItem':
31
+ return balise('li', noeud.children);
32
+ case 'blockquote':
33
+ return balise('blockquote', noeud.children);
34
+ case 'table': {
35
+ const [entete, ...corps] = noeud.children;
36
+ const ligne = (l, cellule) => ({
37
+ t: 'tr',
38
+ c: l.children.map((c) => ({ t: cellule, c: convertirTous(c.children) })),
39
+ });
40
+ const c = [];
41
+ if (entete)
42
+ c.push({ t: 'thead', c: [ligne(entete, 'th')] });
43
+ c.push({ t: 'tbody', c: corps.map((l) => ligne(l, 'td')) });
44
+ return [{ t: 'table', c }];
45
+ }
46
+ case 'break':
47
+ return [{ t: 'br' }];
48
+ case 'text':
49
+ return [{ t: 'texte', v: noeud.value }];
50
+ case 'link':
51
+ return MOTIF_LIEN.test(noeud.url)
52
+ ? [{ t: 'a', href: noeud.url, c: convertirTous(noeud.children) }]
53
+ : convertirTous(noeud.children);
54
+ case 'image':
55
+ return MOTIF_SRC_IMAGE.test(noeud.url) ? [{ t: 'img', src: noeud.url, alt: noeud.alt ?? '' }] : [];
56
+ default:
57
+ return [{ t: 'texte', v: toString(noeud) }];
58
+ }
59
+ }
60
+ /** Convertit du Markdown en arbre restreint ; tout élément hors du sous-ensemble autorisé devient du texte brut. */
61
+ export function versArbre(markdown) {
62
+ const racine = analyseur.parse(markdown.replace(/\r\n/g, '\n'));
63
+ return convertirTous(enfants(racine));
64
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ // Point d'entrée de l'aperçu dans le site construit. L'intégration remplace la chaîne ci-dessous, guillemets compris,
2
+ // par l'origine de l'admin (chaîne JSON) en copiant ce fichier.
3
+ import { demarrerApercu } from './script.js';
4
+ const ORIGINE_ADMIN = '__KOMEKOO_ORIGINE_ADMIN__';
5
+ demarrerApercu(window, ORIGINE_ADMIN);
@@ -0,0 +1,2 @@
1
+ export { BALISES_RICHES, lireMessage } from './protocole.js';
2
+ export type { Message, NoeudRiche, ValeurApercu } from './protocole.js';
@@ -0,0 +1,2 @@
1
+ // Protocole seul : `versArbre` (analyseur Markdown) est servi à part par `@komekoo/astro-cms/apercu/arbre`.
2
+ export { BALISES_RICHES, lireMessage } from './protocole.js';
@@ -0,0 +1,67 @@
1
+ import { MOTIF_CHEMIN_CMS, MOTIF_LIEN, MOTIF_SRC_IMAGE } from '../motifs.js';
2
+ export { MOTIF_CHEMIN_CMS, MOTIF_LIEN, MOTIF_SRC_IMAGE };
3
+ export type NoeudRiche = {
4
+ t: 'texte';
5
+ v: string;
6
+ } | {
7
+ t: 'p' | 'h2' | 'h3' | 'strong' | 'em' | 'ul' | 'ol' | 'li' | 'blockquote' | 'table' | 'thead' | 'tbody' | 'tr' | 'th' | 'td' | 'br';
8
+ c?: NoeudRiche[];
9
+ } | {
10
+ t: 'a';
11
+ href: string;
12
+ c: NoeudRiche[];
13
+ } | {
14
+ t: 'img';
15
+ src: string;
16
+ alt: string;
17
+ };
18
+ export type ValeurApercu = {
19
+ type: 'texte';
20
+ texte: string;
21
+ } | {
22
+ type: 'image';
23
+ src: string;
24
+ srcset: string;
25
+ alt: string;
26
+ } | {
27
+ type: 'lien';
28
+ href: string;
29
+ texte?: string;
30
+ } | {
31
+ type: 'riche';
32
+ arbre: NoeudRiche[];
33
+ } | {
34
+ type: 'indisponible';
35
+ };
36
+ export type Message = {
37
+ cms: 1;
38
+ type: 'bonjour';
39
+ } | {
40
+ cms: 1;
41
+ type: 'pret';
42
+ chemins: string[];
43
+ } | {
44
+ cms: 1;
45
+ type: 'valeurs';
46
+ valeurs: Record<string, ValeurApercu>;
47
+ } | {
48
+ cms: 1;
49
+ type: 'actif';
50
+ chemin: string | null;
51
+ libelle?: string;
52
+ } | {
53
+ cms: 1;
54
+ type: 'clic';
55
+ chemin: string;
56
+ } | {
57
+ cms: 1;
58
+ type: 'lien';
59
+ adresse: string;
60
+ };
61
+ /** Balises de `NoeudRiche` qui contiennent des enfants, hors `texte`, `a` et `img`. */
62
+ export declare const BALISES_RICHES: ReadonlySet<string>;
63
+ export declare const MAX_CHEMINS = 500;
64
+ /** URL absolue http(s), sans espace ni caractère de contrôle. */
65
+ export declare function estUrlAbsolue(x: unknown): x is string;
66
+ /** Valide strictement un message reçu ; renvoie une copie neuve, ou `null` si quoi que ce soit est refusé. */
67
+ export declare function lireMessage(donnee: unknown): Message | null;
@@ -0,0 +1,176 @@
1
+ // Protocole d'échange entre l'admin et le site (aperçu). Ne dépend que de `../motifs.js` : ce fichier est aussi
2
+ // copié tel quel dans le site construit, où il est chargé comme module du navigateur.
3
+ import { MOTIF_CHEMIN_CMS, MOTIF_LIEN, MOTIF_SRC_IMAGE } from '../motifs.js';
4
+ // Réexportés pour `script.ts`, qui ne doit importer que ce fichier une fois copié dans le site.
5
+ export { MOTIF_CHEMIN_CMS, MOTIF_LIEN, MOTIF_SRC_IMAGE };
6
+ /** Balises de `NoeudRiche` qui contiennent des enfants, hors `texte`, `a` et `img`. */
7
+ export const BALISES_RICHES = new Set([
8
+ 'p', 'h2', 'h3', 'strong', 'em', 'ul', 'ol', 'li', 'blockquote', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'br',
9
+ ]);
10
+ export const MAX_CHEMINS = 500;
11
+ const PROFONDEUR_MAX = 20;
12
+ /** Chemin interne : ni « // » ni « /\ » au début, aucun antislash, espace ni caractère de contrôle. */
13
+ const MOTIF_ADRESSE = /^\/(?![\/\\])[^\s\x00-\x1f\\]*$/;
14
+ const estObjet = (x) => typeof x === 'object' && x !== null && !Array.isArray(x);
15
+ const cles = (o, attendues, facultatives = []) => Object.keys(o).every((k) => attendues.includes(k) || facultatives.includes(k)) && attendues.every((k) => k in o);
16
+ const estChemin = (x) => typeof x === 'string' && MOTIF_CHEMIN_CMS.test(x);
17
+ const estLien = (x) => typeof x === 'string' && MOTIF_LIEN.test(x);
18
+ /** URL absolue http(s), sans espace ni caractère de contrôle. */
19
+ export function estUrlAbsolue(x) {
20
+ if (typeof x !== 'string' || /[\s\x00-\x1f]/.test(x))
21
+ return false;
22
+ try {
23
+ const protocole = new URL(x).protocol;
24
+ return protocole === 'https:' || protocole === 'http:';
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ function srcsetValide(x) {
31
+ if (typeof x !== 'string')
32
+ return false;
33
+ if (x === '')
34
+ return true;
35
+ return x.split(',').every((entree) => {
36
+ const [url, ...reste] = entree.trim().split(/\s+/);
37
+ return estUrlAbsolue(url) && reste.length <= 1;
38
+ });
39
+ }
40
+ function lireNoeud(x, profondeur) {
41
+ if (profondeur > PROFONDEUR_MAX || !estObjet(x))
42
+ return null;
43
+ const t = x.t;
44
+ if (t === 'texte') {
45
+ return cles(x, ['t', 'v']) && typeof x.v === 'string' ? { t, v: x.v } : null;
46
+ }
47
+ if (t === 'img') {
48
+ if (!cles(x, ['t', 'src', 'alt']) || typeof x.alt !== 'string')
49
+ return null;
50
+ // Les arbres de `versArbre` portent des chemins relatifs /uploads/ ; l'admin peut aussi envoyer des URL absolues.
51
+ const ok = estUrlAbsolue(x.src) || (typeof x.src === 'string' && MOTIF_SRC_IMAGE.test(x.src));
52
+ return ok ? { t, src: x.src, alt: x.alt } : null;
53
+ }
54
+ if (t === 'a') {
55
+ if (!cles(x, ['t', 'href', 'c']) || !estLien(x.href))
56
+ return null;
57
+ const c = lireNoeuds(x.c, profondeur + 1);
58
+ return c ? { t, href: x.href, c } : null;
59
+ }
60
+ if (typeof t === 'string' && BALISES_RICHES.has(t)) {
61
+ if (!cles(x, ['t'], ['c']))
62
+ return null;
63
+ const balise = t;
64
+ if (x.c === undefined)
65
+ return { t: balise };
66
+ const c = lireNoeuds(x.c, profondeur + 1);
67
+ return c ? { t: balise, c } : null;
68
+ }
69
+ return null;
70
+ }
71
+ function lireNoeuds(x, profondeur) {
72
+ if (!Array.isArray(x))
73
+ return null;
74
+ const sortie = [];
75
+ for (const element of x) {
76
+ const noeud = lireNoeud(element, profondeur);
77
+ if (!noeud)
78
+ return null;
79
+ sortie.push(noeud);
80
+ }
81
+ return sortie;
82
+ }
83
+ function lireValeur(x) {
84
+ if (!estObjet(x))
85
+ return null;
86
+ switch (x.type) {
87
+ case 'texte':
88
+ return cles(x, ['type', 'texte']) && typeof x.texte === 'string' ? { type: 'texte', texte: x.texte } : null;
89
+ case 'image':
90
+ return cles(x, ['type', 'src', 'srcset', 'alt']) &&
91
+ estUrlAbsolue(x.src) &&
92
+ srcsetValide(x.srcset) &&
93
+ typeof x.alt === 'string'
94
+ ? { type: 'image', src: x.src, srcset: x.srcset, alt: x.alt }
95
+ : null;
96
+ case 'lien': {
97
+ if (!cles(x, ['type', 'href'], ['texte']) || !estLien(x.href))
98
+ return null;
99
+ if (x.texte === undefined)
100
+ return { type: 'lien', href: x.href };
101
+ return typeof x.texte === 'string' ? { type: 'lien', href: x.href, texte: x.texte } : null;
102
+ }
103
+ case 'riche': {
104
+ if (!cles(x, ['type', 'arbre']))
105
+ return null;
106
+ const arbre = lireNoeuds(x.arbre, 1);
107
+ return arbre ? { type: 'riche', arbre } : null;
108
+ }
109
+ case 'indisponible':
110
+ return cles(x, ['type']) ? { type: 'indisponible' } : null;
111
+ default:
112
+ return null;
113
+ }
114
+ }
115
+ /** Valide strictement un message reçu ; renvoie une copie neuve, ou `null` si quoi que ce soit est refusé. */
116
+ export function lireMessage(donnee) {
117
+ if (!estObjet(donnee) || donnee.cms !== 1)
118
+ return null;
119
+ switch (donnee.type) {
120
+ case 'bonjour':
121
+ return cles(donnee, ['cms', 'type']) ? { cms: 1, type: 'bonjour' } : null;
122
+ case 'pret': {
123
+ if (!cles(donnee, ['cms', 'type', 'chemins']))
124
+ return null;
125
+ const chemins = donnee.chemins;
126
+ if (!Array.isArray(chemins) || chemins.length > MAX_CHEMINS)
127
+ return null;
128
+ const copie = [];
129
+ for (const chemin of chemins) {
130
+ // for…of visite aussi les trous d'un tableau creux (valeur undefined), qui sont donc refusés.
131
+ if (!estChemin(chemin))
132
+ return null;
133
+ copie.push(chemin);
134
+ }
135
+ return { cms: 1, type: 'pret', chemins: copie };
136
+ }
137
+ case 'valeurs': {
138
+ if (!cles(donnee, ['cms', 'type', 'valeurs']) || !estObjet(donnee.valeurs))
139
+ return null;
140
+ const valeurs = {};
141
+ for (const [chemin, brut] of Object.entries(donnee.valeurs)) {
142
+ const valeur = lireValeur(brut);
143
+ if (!estChemin(chemin) || !valeur)
144
+ return null;
145
+ valeurs[chemin] = valeur;
146
+ }
147
+ return { cms: 1, type: 'valeurs', valeurs };
148
+ }
149
+ case 'actif': {
150
+ if (!cles(donnee, ['cms', 'type', 'chemin'], ['libelle']))
151
+ return null;
152
+ const chemin = donnee.chemin;
153
+ if (chemin !== null && !estChemin(chemin))
154
+ return null;
155
+ if (donnee.libelle === undefined)
156
+ return { cms: 1, type: 'actif', chemin };
157
+ return typeof donnee.libelle === 'string'
158
+ ? { cms: 1, type: 'actif', chemin, libelle: donnee.libelle }
159
+ : null;
160
+ }
161
+ case 'clic':
162
+ return cles(donnee, ['cms', 'type', 'chemin']) && estChemin(donnee.chemin)
163
+ ? { cms: 1, type: 'clic', chemin: donnee.chemin }
164
+ : null;
165
+ case 'lien': {
166
+ if (!cles(donnee, ['cms', 'type', 'adresse']))
167
+ return null;
168
+ const adresse = donnee.adresse;
169
+ return typeof adresse === 'string' && MOTIF_ADRESSE.test(adresse)
170
+ ? { cms: 1, type: 'lien', adresse }
171
+ : null;
172
+ }
173
+ default:
174
+ return null;
175
+ }
176
+ }
@@ -0,0 +1,8 @@
1
+ import { type NoeudRiche } from './protocole.js';
2
+ /** Remplace les enfants de `element` par l'arbre, en ne créant que les nœuds et attributs autorisés. */
3
+ export declare function appliquerArbre(element: Element, arbre: NoeudRiche[]): void;
4
+ /**
5
+ * Démarre l'aperçu dans `fenetre` : écoute les messages de l'admin (`origineAdmin`, fenêtre parente).
6
+ * Renvoie une fonction d'arrêt.
7
+ */
8
+ export declare function demarrerApercu(fenetre: Window, origineAdmin: string): () => void;