@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.
- package/README.md +144 -2
- package/dist/ajv.d.ts +4 -0
- package/dist/ajv.js +24 -0
- package/dist/apercu/arbre.d.ts +3 -0
- package/dist/apercu/arbre.js +64 -0
- package/dist/apercu/demarrage.d.ts +1 -0
- package/dist/apercu/demarrage.js +5 -0
- package/dist/apercu/index.d.ts +2 -0
- package/dist/apercu/index.js +2 -0
- package/dist/apercu/protocole.d.ts +67 -0
- package/dist/apercu/protocole.js +176 -0
- package/dist/apercu/script.d.ts +8 -0
- package/dist/apercu/script.js +321 -0
- package/dist/champs.d.ts +49 -0
- package/dist/champs.js +58 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +70 -0
- package/dist/collections.d.ts +39 -0
- package/dist/collections.js +57 -0
- package/dist/images.d.ts +8 -0
- package/dist/images.js +19 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/dist/integration.d.ts +9 -0
- package/dist/integration.js +113 -0
- package/dist/marqueur.d.ts +4 -0
- package/dist/marqueur.js +8 -0
- package/dist/motifs.d.ts +10 -0
- package/dist/motifs.js +11 -0
- package/dist/navigateur.d.ts +6 -0
- package/dist/navigateur.js +6 -0
- package/dist/noyau/apercu-build.d.ts +12 -0
- package/dist/noyau/apercu-build.js +74 -0
- package/dist/noyau/charger.d.ts +4 -0
- package/dist/noyau/charger.js +48 -0
- package/dist/noyau/collecter.d.ts +27 -0
- package/dist/noyau/collecter.js +133 -0
- package/dist/noyau/contenus.d.ts +6 -0
- package/dist/noyau/contenus.js +218 -0
- package/dist/noyau/erreurs.d.ts +7 -0
- package/dist/noyau/erreurs.js +14 -0
- package/dist/noyau/frontmatter.d.ts +15 -0
- package/dist/noyau/frontmatter.js +70 -0
- package/dist/noyau/json-schema.d.ts +4 -0
- package/dist/noyau/json-schema.js +61 -0
- package/dist/noyau/manifest.d.ts +16 -0
- package/dist/noyau/manifest.js +136 -0
- package/dist/noyau/markdown.d.ts +12 -0
- package/dist/noyau/markdown.js +95 -0
- package/dist/noyau/marqueurs.d.ts +11 -0
- package/dist/noyau/marqueurs.js +57 -0
- package/dist/noyau/operations.d.ts +12 -0
- package/dist/noyau/operations.js +54 -0
- package/dist/noyau/serialiser.d.ts +2 -0
- package/dist/noyau/serialiser.js +25 -0
- package/dist/noyau/version.d.ts +5 -0
- package/dist/noyau/version.js +25 -0
- package/dist/socle/hors-perimetre.d.ts +2 -0
- package/dist/socle/hors-perimetre.js +9 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.js +10 -0
- package/package.json +62 -3
- package/src/composants/CmsImage.astro +26 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// Script d'aperçu exécuté dans le site du client, affiché dans l'admin. Frontière de sécurité :
|
|
2
|
+
// - seuls les messages de l'origine de l'admin, venant de la fenêtre parente et validés par `lireMessage`, sont lus ;
|
|
3
|
+
// - avant le premier `bonjour` valide, rien n'est modifié et aucun événement n'est capté ;
|
|
4
|
+
// - le contenu n'est construit qu'avec `createElement`, `createTextNode`, `setAttribute` et `textContent`.
|
|
5
|
+
// N'importe que `./protocole.js` : ce fichier est copié tel quel dans le site construit (module du navigateur).
|
|
6
|
+
import { BALISES_RICHES, MAX_CHEMINS, MOTIF_CHEMIN_CMS, MOTIF_LIEN, MOTIF_SRC_IMAGE, estUrlAbsolue, lireMessage, } from './protocole.js';
|
|
7
|
+
const CLASSE_ZONE = 'komekoo-zone';
|
|
8
|
+
const CLASSE_ACTIVE = 'komekoo-zone--active';
|
|
9
|
+
const CLASSE_ETIQUETTE = 'komekoo-zone__etiquette';
|
|
10
|
+
const CLASSE_INFO = 'komekoo-info';
|
|
11
|
+
const TEXTE_LIEN_DESACTIVE = "Les liens sont désactivés dans l'aperçu";
|
|
12
|
+
const TEXTE_INDISPONIBLE = 'Visible dans la prévisualisation';
|
|
13
|
+
const DUREE_INFO = 2000;
|
|
14
|
+
const PROFONDEUR_MAX = 20;
|
|
15
|
+
// Valeurs de la maquette 04 (`.zone-editable`, `--active`, `__etiquette`) recopiées en dur, préfixe `komekoo-`.
|
|
16
|
+
const STYLES = `
|
|
17
|
+
.${CLASSE_ZONE}{outline:1px dashed transparent!important;outline-offset:2px!important;cursor:pointer!important;transition:outline-color 120ms ease}
|
|
18
|
+
.${CLASSE_ZONE}:hover{outline-color:#52525B!important}
|
|
19
|
+
.${CLASSE_ACTIVE}{box-shadow:0 0 0 2px #5DEFB7!important}
|
|
20
|
+
.${CLASSE_ETIQUETTE},.${CLASSE_INFO}{position:fixed;z-index:2147483647;margin:0;pointer-events:none;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:0.8125rem;font-weight:500;line-height:1.5;letter-spacing:normal;text-transform:none;white-space:nowrap}
|
|
21
|
+
.${CLASSE_ETIQUETTE}{padding:0 0.5rem;border-radius:0.375rem;background:#5DEFB7;color:#0E2A1E}
|
|
22
|
+
.${CLASSE_INFO}{padding:0.25rem 0.75rem;border-radius:0.625rem;background:#18181B;color:#FFFFFF;box-shadow:0 12px 32px rgba(31,28,25,0.14),0 1px 2px rgba(31,28,25,0.06)}
|
|
23
|
+
@media (prefers-reduced-motion:reduce){.${CLASSE_ZONE}{transition:none}}
|
|
24
|
+
`;
|
|
25
|
+
const estObjet = (x) => typeof x === 'object' && x !== null && !Array.isArray(x);
|
|
26
|
+
const srcImageAutorisee = (x) => estUrlAbsolue(x) || (typeof x === 'string' && MOTIF_SRC_IMAGE.test(x));
|
|
27
|
+
function construire(document, noeuds, profondeur, parent) {
|
|
28
|
+
if (!Array.isArray(noeuds) || profondeur > PROFONDEUR_MAX)
|
|
29
|
+
return;
|
|
30
|
+
for (const noeud of noeuds) {
|
|
31
|
+
if (!estObjet(noeud))
|
|
32
|
+
continue;
|
|
33
|
+
const t = noeud.t;
|
|
34
|
+
if (t === 'texte') {
|
|
35
|
+
if (typeof noeud.v === 'string')
|
|
36
|
+
parent.appendChild(document.createTextNode(noeud.v));
|
|
37
|
+
}
|
|
38
|
+
else if (t === 'a') {
|
|
39
|
+
if (typeof noeud.href === 'string' && MOTIF_LIEN.test(noeud.href)) {
|
|
40
|
+
const a = document.createElement('a');
|
|
41
|
+
a.setAttribute('href', noeud.href);
|
|
42
|
+
construire(document, noeud.c, profondeur + 1, a);
|
|
43
|
+
parent.appendChild(a);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Adresse refusée : le texte du lien est gardé, sans lien.
|
|
47
|
+
construire(document, noeud.c, profondeur + 1, parent);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (t === 'img') {
|
|
51
|
+
if (srcImageAutorisee(noeud.src)) {
|
|
52
|
+
const img = document.createElement('img');
|
|
53
|
+
img.setAttribute('src', noeud.src);
|
|
54
|
+
img.setAttribute('alt', typeof noeud.alt === 'string' ? noeud.alt : '');
|
|
55
|
+
parent.appendChild(img);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (typeof t === 'string' && BALISES_RICHES.has(t)) {
|
|
59
|
+
const element = document.createElement(t);
|
|
60
|
+
if (t !== 'br')
|
|
61
|
+
construire(document, noeud.c, profondeur + 1, element);
|
|
62
|
+
parent.appendChild(element);
|
|
63
|
+
}
|
|
64
|
+
// Tout autre type est ignoré, avec ses enfants.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Remplace les enfants de `element` par l'arbre, en ne créant que les nœuds et attributs autorisés. */
|
|
68
|
+
export function appliquerArbre(element, arbre) {
|
|
69
|
+
const document = element.ownerDocument;
|
|
70
|
+
const fragment = document.createDocumentFragment();
|
|
71
|
+
construire(document, arbre, 1, fragment);
|
|
72
|
+
element.replaceChildren(fragment);
|
|
73
|
+
}
|
|
74
|
+
function appliquerValeur(element, valeur) {
|
|
75
|
+
switch (valeur.type) {
|
|
76
|
+
case 'texte':
|
|
77
|
+
element.textContent = valeur.texte;
|
|
78
|
+
return;
|
|
79
|
+
case 'image':
|
|
80
|
+
if (element.localName !== 'img' || !estUrlAbsolue(valeur.src))
|
|
81
|
+
return;
|
|
82
|
+
element.setAttribute('src', valeur.src);
|
|
83
|
+
if (valeur.srcset === '')
|
|
84
|
+
element.removeAttribute('srcset');
|
|
85
|
+
else
|
|
86
|
+
element.setAttribute('srcset', valeur.srcset);
|
|
87
|
+
element.setAttribute('alt', valeur.alt);
|
|
88
|
+
return;
|
|
89
|
+
case 'lien':
|
|
90
|
+
if (element.localName !== 'a' || !MOTIF_LIEN.test(valeur.href))
|
|
91
|
+
return;
|
|
92
|
+
element.setAttribute('href', valeur.href);
|
|
93
|
+
if (valeur.texte !== undefined && Array.from(element.childNodes).every((n) => n.nodeType === 3)) {
|
|
94
|
+
element.textContent = valeur.texte;
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
case 'riche':
|
|
98
|
+
appliquerArbre(element, valeur.arbre);
|
|
99
|
+
return;
|
|
100
|
+
case 'indisponible':
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function elementDe(cible) {
|
|
105
|
+
const noeud = cible;
|
|
106
|
+
if (!noeud || typeof noeud.nodeType !== 'number')
|
|
107
|
+
return null;
|
|
108
|
+
return noeud.nodeType === 1 ? noeud : noeud.parentElement;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Démarre l'aperçu dans `fenetre` : écoute les messages de l'admin (`origineAdmin`, fenêtre parente).
|
|
112
|
+
* Renvoie une fonction d'arrêt.
|
|
113
|
+
*/
|
|
114
|
+
export function demarrerApercu(fenetre, origineAdmin) {
|
|
115
|
+
const document = fenetre.document;
|
|
116
|
+
if (fenetre.parent === fenetre)
|
|
117
|
+
return () => { };
|
|
118
|
+
let actif = false;
|
|
119
|
+
let feuille = null;
|
|
120
|
+
let etiquette = null;
|
|
121
|
+
let info = null;
|
|
122
|
+
let minuterieInfo;
|
|
123
|
+
let zoneActive = null;
|
|
124
|
+
const indisponibles = new Set();
|
|
125
|
+
const marques = () => Array.from(document.querySelectorAll('[data-cms]'));
|
|
126
|
+
const marquesDe = (chemin) => marques().filter((e) => e.getAttribute('data-cms') === chemin);
|
|
127
|
+
function envoyer(message) {
|
|
128
|
+
fenetre.parent.postMessage(message, origineAdmin);
|
|
129
|
+
}
|
|
130
|
+
function installerStyles() {
|
|
131
|
+
try {
|
|
132
|
+
const Feuille = fenetre.CSSStyleSheet;
|
|
133
|
+
if (typeof Feuille !== 'function' || !Array.isArray(document.adoptedStyleSheets))
|
|
134
|
+
return;
|
|
135
|
+
const nouvelle = new Feuille();
|
|
136
|
+
nouvelle.replaceSync(STYLES);
|
|
137
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, nouvelle];
|
|
138
|
+
feuille = nouvelle;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
feuille = null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function marquerZones() {
|
|
145
|
+
for (const element of marques())
|
|
146
|
+
element.classList.add(CLASSE_ZONE);
|
|
147
|
+
}
|
|
148
|
+
function placer(boite, gauche, haut) {
|
|
149
|
+
const largeur = fenetre.innerWidth || 0;
|
|
150
|
+
boite.style.setProperty('left', `${Math.max(0, Math.min(gauche, largeur - boite.offsetWidth - 4))}px`);
|
|
151
|
+
boite.style.setProperty('top', `${Math.max(0, haut)}px`);
|
|
152
|
+
}
|
|
153
|
+
function placerEtiquette() {
|
|
154
|
+
if (!etiquette || !zoneActive)
|
|
155
|
+
return;
|
|
156
|
+
const cible = marquesDe(zoneActive.chemin)[0];
|
|
157
|
+
if (!cible)
|
|
158
|
+
return;
|
|
159
|
+
const rect = cible.getBoundingClientRect();
|
|
160
|
+
const haut = rect.top - etiquette.offsetHeight - 4;
|
|
161
|
+
placer(etiquette, rect.left, haut < 0 ? rect.bottom + 4 : haut);
|
|
162
|
+
}
|
|
163
|
+
function retirerActif() {
|
|
164
|
+
for (const element of document.querySelectorAll(`.${CLASSE_ACTIVE}`))
|
|
165
|
+
element.classList.remove(CLASSE_ACTIVE);
|
|
166
|
+
etiquette?.remove();
|
|
167
|
+
}
|
|
168
|
+
function afficherActif(defiler) {
|
|
169
|
+
retirerActif();
|
|
170
|
+
if (!zoneActive)
|
|
171
|
+
return;
|
|
172
|
+
const elements = marquesDe(zoneActive.chemin);
|
|
173
|
+
if (elements.length === 0)
|
|
174
|
+
return;
|
|
175
|
+
for (const element of elements)
|
|
176
|
+
element.classList.add(CLASSE_ACTIVE);
|
|
177
|
+
const morceaux = [zoneActive.libelle, indisponibles.has(zoneActive.chemin) ? TEXTE_INDISPONIBLE : ''].filter(Boolean);
|
|
178
|
+
if (morceaux.length > 0) {
|
|
179
|
+
etiquette ??= document.createElement('span');
|
|
180
|
+
etiquette.className = CLASSE_ETIQUETTE;
|
|
181
|
+
etiquette.setAttribute('aria-hidden', 'true');
|
|
182
|
+
etiquette.textContent = morceaux.join(' — ');
|
|
183
|
+
(document.body ?? document.documentElement).appendChild(etiquette);
|
|
184
|
+
placerEtiquette();
|
|
185
|
+
}
|
|
186
|
+
if (defiler) {
|
|
187
|
+
const premier = elements[0];
|
|
188
|
+
const reduit = typeof fenetre.matchMedia === 'function' && fenetre.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
189
|
+
if (typeof premier.scrollIntoView === 'function') {
|
|
190
|
+
premier.scrollIntoView({ behavior: reduit ? 'auto' : 'smooth', block: 'nearest', inline: 'nearest' });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function retirerInfo() {
|
|
195
|
+
if (minuterieInfo !== undefined)
|
|
196
|
+
fenetre.clearTimeout(minuterieInfo);
|
|
197
|
+
minuterieInfo = undefined;
|
|
198
|
+
info?.remove();
|
|
199
|
+
info = null;
|
|
200
|
+
}
|
|
201
|
+
function afficherInfo(gauche, haut) {
|
|
202
|
+
retirerInfo();
|
|
203
|
+
const bulle = document.createElement('div');
|
|
204
|
+
bulle.className = CLASSE_INFO;
|
|
205
|
+
bulle.setAttribute('role', 'status');
|
|
206
|
+
bulle.style.setProperty('position', 'fixed');
|
|
207
|
+
bulle.style.setProperty('z-index', '2147483647');
|
|
208
|
+
bulle.style.setProperty('pointer-events', 'none');
|
|
209
|
+
bulle.textContent = TEXTE_LIEN_DESACTIVE;
|
|
210
|
+
(document.body ?? document.documentElement).appendChild(bulle);
|
|
211
|
+
placer(bulle, gauche + 12, haut + 12);
|
|
212
|
+
info = bulle;
|
|
213
|
+
minuterieInfo = fenetre.setTimeout(retirerInfo, DUREE_INFO);
|
|
214
|
+
}
|
|
215
|
+
function surClic(evenement) {
|
|
216
|
+
const element = elementDe(evenement.target);
|
|
217
|
+
if (!element)
|
|
218
|
+
return;
|
|
219
|
+
const zone = element.closest('[data-cms]');
|
|
220
|
+
const chemin = zone?.getAttribute('data-cms');
|
|
221
|
+
if (chemin && MOTIF_CHEMIN_CMS.test(chemin)) {
|
|
222
|
+
evenement.preventDefault();
|
|
223
|
+
envoyer({ cms: 1, type: 'clic', chemin });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const lien = element.closest('a[href], area[href]');
|
|
227
|
+
if (!lien)
|
|
228
|
+
return;
|
|
229
|
+
evenement.preventDefault();
|
|
230
|
+
let adresse = null;
|
|
231
|
+
try {
|
|
232
|
+
adresse = new URL(lien.getAttribute('href') ?? '', document.baseURI);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
adresse = null;
|
|
236
|
+
}
|
|
237
|
+
if (adresse && adresse.origin === fenetre.location.origin) {
|
|
238
|
+
const message = lireMessage({ cms: 1, type: 'lien', adresse: adresse.pathname });
|
|
239
|
+
if (message) {
|
|
240
|
+
envoyer(message);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
afficherInfo(evenement.clientX, evenement.clientY);
|
|
245
|
+
}
|
|
246
|
+
function surAuxClic(evenement) {
|
|
247
|
+
// Clic du milieu sur un lien : ouvrirait la page hors de l'aperçu.
|
|
248
|
+
if (elementDe(evenement.target)?.closest('a[href], area[href]'))
|
|
249
|
+
evenement.preventDefault();
|
|
250
|
+
}
|
|
251
|
+
function surEnvoi(evenement) {
|
|
252
|
+
evenement.preventDefault();
|
|
253
|
+
const formulaire = elementDe(evenement.target);
|
|
254
|
+
const rect = formulaire?.getBoundingClientRect();
|
|
255
|
+
afficherInfo(rect ? rect.left : 0, rect ? rect.top : 0);
|
|
256
|
+
}
|
|
257
|
+
function surDefilement() {
|
|
258
|
+
placerEtiquette();
|
|
259
|
+
}
|
|
260
|
+
function activer() {
|
|
261
|
+
actif = true;
|
|
262
|
+
installerStyles();
|
|
263
|
+
fenetre.addEventListener('click', surClic, true);
|
|
264
|
+
fenetre.addEventListener('auxclick', surAuxClic, true);
|
|
265
|
+
fenetre.addEventListener('submit', surEnvoi, true);
|
|
266
|
+
fenetre.addEventListener('scroll', surDefilement, { capture: true, passive: true });
|
|
267
|
+
fenetre.addEventListener('resize', surDefilement, { passive: true });
|
|
268
|
+
}
|
|
269
|
+
function surMessage(evenement) {
|
|
270
|
+
if (evenement.origin !== origineAdmin || evenement.source !== fenetre.parent)
|
|
271
|
+
return;
|
|
272
|
+
const message = lireMessage(evenement.data);
|
|
273
|
+
if (!message)
|
|
274
|
+
return;
|
|
275
|
+
if (message.type === 'bonjour') {
|
|
276
|
+
if (!actif)
|
|
277
|
+
activer();
|
|
278
|
+
marquerZones();
|
|
279
|
+
const chemins = [...new Set(marques().map((e) => e.getAttribute('data-cms') ?? ''))]
|
|
280
|
+
.filter((chemin) => MOTIF_CHEMIN_CMS.test(chemin))
|
|
281
|
+
.slice(0, MAX_CHEMINS);
|
|
282
|
+
envoyer({ cms: 1, type: 'pret', chemins });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (!actif)
|
|
286
|
+
return;
|
|
287
|
+
if (message.type === 'valeurs') {
|
|
288
|
+
marquerZones();
|
|
289
|
+
for (const [chemin, valeur] of Object.entries(message.valeurs)) {
|
|
290
|
+
if (valeur.type === 'indisponible')
|
|
291
|
+
indisponibles.add(chemin);
|
|
292
|
+
else
|
|
293
|
+
indisponibles.delete(chemin);
|
|
294
|
+
for (const element of marquesDe(chemin))
|
|
295
|
+
appliquerValeur(element, valeur);
|
|
296
|
+
}
|
|
297
|
+
afficherActif(false);
|
|
298
|
+
}
|
|
299
|
+
else if (message.type === 'actif') {
|
|
300
|
+
marquerZones();
|
|
301
|
+
zoneActive = message.chemin === null ? null : { chemin: message.chemin, libelle: message.libelle ?? '' };
|
|
302
|
+
afficherActif(true);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
fenetre.addEventListener('message', surMessage);
|
|
306
|
+
return () => {
|
|
307
|
+
fenetre.removeEventListener('message', surMessage);
|
|
308
|
+
fenetre.removeEventListener('click', surClic, true);
|
|
309
|
+
fenetre.removeEventListener('auxclick', surAuxClic, true);
|
|
310
|
+
fenetre.removeEventListener('submit', surEnvoi, true);
|
|
311
|
+
fenetre.removeEventListener('scroll', surDefilement, true);
|
|
312
|
+
fenetre.removeEventListener('resize', surDefilement);
|
|
313
|
+
retirerInfo();
|
|
314
|
+
retirerActif();
|
|
315
|
+
if (feuille) {
|
|
316
|
+
const retiree = feuille;
|
|
317
|
+
document.adoptedStyleSheets = document.adoptedStyleSheets.filter((f) => f !== retiree);
|
|
318
|
+
}
|
|
319
|
+
actif = false;
|
|
320
|
+
};
|
|
321
|
+
}
|
package/dist/champs.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'astro/zod';
|
|
2
|
+
import type { OptionsBase } from './types.js';
|
|
3
|
+
import { MOTIF_LIEN, MOTIF_SRC_IMAGE } from './motifs.js';
|
|
4
|
+
export { MOTIF_LIEN, MOTIF_SRC_IMAGE };
|
|
5
|
+
type Bornes = {
|
|
6
|
+
min?: number;
|
|
7
|
+
max?: number;
|
|
8
|
+
};
|
|
9
|
+
/** `obligatoire: false` → `ZodOptional<T>` ; absent ou `true` → `T` ; `boolean` non littéral → l'un ou l'autre. */
|
|
10
|
+
export type Finalise<T extends z.ZodType, O extends OptionsBase> = O extends {
|
|
11
|
+
obligatoire: false;
|
|
12
|
+
} ? z.ZodOptional<T> : O extends {
|
|
13
|
+
obligatoire: true;
|
|
14
|
+
} ? T : 'obligatoire' extends keyof O ? T | z.ZodOptional<T> : T;
|
|
15
|
+
export declare const champ: {
|
|
16
|
+
texte: <O extends OptionsBase & Bornes>(o: O) => Finalise<z.ZodString, O>;
|
|
17
|
+
paragraphe: <O extends OptionsBase & Bornes>(o: O) => Finalise<z.ZodString, O>;
|
|
18
|
+
richtext: <O extends OptionsBase & {
|
|
19
|
+
max?: number;
|
|
20
|
+
}>(o: O) => Finalise<z.ZodString, O>;
|
|
21
|
+
image: <O extends OptionsBase>(o: O) => Finalise<z.ZodObject<{
|
|
22
|
+
src: z.ZodString;
|
|
23
|
+
alt: z.ZodString;
|
|
24
|
+
largeur: z.ZodInt;
|
|
25
|
+
hauteur: z.ZodInt;
|
|
26
|
+
}, z.core.$strict>, O>;
|
|
27
|
+
date: <O extends OptionsBase>(o: O) => Finalise<z.ZodISODate, O>;
|
|
28
|
+
lien: <O extends OptionsBase>(o: O) => Finalise<z.ZodString, O>;
|
|
29
|
+
nombre: <O extends OptionsBase & Bornes & {
|
|
30
|
+
entier?: boolean;
|
|
31
|
+
unite?: string;
|
|
32
|
+
}>(o: O) => Finalise<z.ZodNumber, O>;
|
|
33
|
+
booleen: <O extends OptionsBase>(o: O) => Finalise<z.ZodBoolean, O>;
|
|
34
|
+
choix: <const Options extends readonly [string, ...string[]], O extends OptionsBase>(o: O & {
|
|
35
|
+
options: Options;
|
|
36
|
+
}) => Finalise<z.ZodEnum<{ [k in Options[number]]: k; } extends infer T ? { [k in keyof T]: T[k]; } : never>, O & {
|
|
37
|
+
options: Options;
|
|
38
|
+
}>;
|
|
39
|
+
liste: <E extends z.ZodType, O extends OptionsBase & Bornes>(o: O & {
|
|
40
|
+
element: E;
|
|
41
|
+
}) => Finalise<z.ZodArray<E>, O & {
|
|
42
|
+
element: E;
|
|
43
|
+
}>;
|
|
44
|
+
groupe: <S extends Record<string, z.ZodType>, O extends OptionsBase>(o: O & {
|
|
45
|
+
champs: S;
|
|
46
|
+
}) => Finalise<z.ZodObject<{ -readonly [P in keyof S]: S[P]; }, z.core.$strict>, O & {
|
|
47
|
+
champs: S;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
package/dist/champs.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from 'astro/zod';
|
|
2
|
+
import { MOTIF_LIEN, MOTIF_SRC_IMAGE } from './motifs.js';
|
|
3
|
+
export { MOTIF_LIEN, MOTIF_SRC_IMAGE };
|
|
4
|
+
/** Appose la méta CMS puis, si le champ n'est pas obligatoire, `.optional()`. */
|
|
5
|
+
function finaliser(schema, o, editeur, extra = {}) {
|
|
6
|
+
const cms = { editeur, libelle: o.libelle };
|
|
7
|
+
if (o.aide !== undefined)
|
|
8
|
+
cms.aide = o.aide;
|
|
9
|
+
if (o.onglet !== undefined) {
|
|
10
|
+
if (o.onglet === '')
|
|
11
|
+
throw new Error(`onglet du champ « ${o.libelle} » : texte non vide attendu`);
|
|
12
|
+
cms.onglet = o.onglet;
|
|
13
|
+
}
|
|
14
|
+
if (extra.unite !== undefined)
|
|
15
|
+
cms.unite = extra.unite;
|
|
16
|
+
const avecMeta = schema.meta({ cms });
|
|
17
|
+
return (o.obligatoire === false ? avecMeta.optional() : avecMeta);
|
|
18
|
+
}
|
|
19
|
+
function chaine({ min, max }) {
|
|
20
|
+
let s = z.string();
|
|
21
|
+
if (min !== undefined)
|
|
22
|
+
s = s.min(min);
|
|
23
|
+
if (max !== undefined)
|
|
24
|
+
s = s.max(max);
|
|
25
|
+
return s;
|
|
26
|
+
}
|
|
27
|
+
export const champ = {
|
|
28
|
+
texte: (o) => finaliser(chaine(o), o, 'texte'),
|
|
29
|
+
paragraphe: (o) => finaliser(chaine(o), o, 'paragraphe'),
|
|
30
|
+
richtext: (o) => finaliser(chaine({ max: o.max }), o, 'richtext'),
|
|
31
|
+
image: (o) => finaliser(z.strictObject({
|
|
32
|
+
src: z.string().regex(MOTIF_SRC_IMAGE),
|
|
33
|
+
alt: z.string().max(250),
|
|
34
|
+
largeur: z.int().positive(),
|
|
35
|
+
hauteur: z.int().positive(),
|
|
36
|
+
}), o, 'image'),
|
|
37
|
+
date: (o) => finaliser(z.iso.date(), o, 'date'),
|
|
38
|
+
lien: (o) => finaliser(z.string().regex(MOTIF_LIEN), o, 'lien'),
|
|
39
|
+
nombre: (o) => {
|
|
40
|
+
let s = o.entier ? z.int() : z.number();
|
|
41
|
+
if (o.min !== undefined)
|
|
42
|
+
s = s.min(o.min);
|
|
43
|
+
if (o.max !== undefined)
|
|
44
|
+
s = s.max(o.max);
|
|
45
|
+
return finaliser(s, o, 'nombre', { unite: o.unite });
|
|
46
|
+
},
|
|
47
|
+
booleen: (o) => finaliser(z.boolean(), o, 'booleen'),
|
|
48
|
+
choix: (o) => finaliser(z.enum(o.options), o, 'choix'),
|
|
49
|
+
liste: (o) => {
|
|
50
|
+
let s = z.array(o.element);
|
|
51
|
+
if (o.min !== undefined)
|
|
52
|
+
s = s.min(o.min);
|
|
53
|
+
if (o.max !== undefined)
|
|
54
|
+
s = s.max(o.max);
|
|
55
|
+
return finaliser(s, o, 'liste');
|
|
56
|
+
},
|
|
57
|
+
groupe: (o) => finaliser(z.strictObject(o.champs), o, 'groupe'),
|
|
58
|
+
};
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { chargerOptions } from './integration.js';
|
|
6
|
+
import { formaterViolations } from './noyau/contenus.js';
|
|
7
|
+
import { ErreurCms } from './noyau/erreurs.js';
|
|
8
|
+
import { synchroniser, verifier } from './noyau/operations.js';
|
|
9
|
+
const AIDE = `Usage : komekoo-cms <commande>
|
|
10
|
+
|
|
11
|
+
Commandes :
|
|
12
|
+
sync génère cms.schema.json et ai/manifest.json
|
|
13
|
+
check vérifie les fichiers générés et valide les contenus (code 1 si problème)
|
|
14
|
+
|
|
15
|
+
Options :
|
|
16
|
+
-h, --help affiche cette aide`;
|
|
17
|
+
/** Cœur testable de la CLI : renvoie le code de sortie. */
|
|
18
|
+
export async function executerCli(argv, racine) {
|
|
19
|
+
const { positionals, values } = parseArgs({
|
|
20
|
+
args: argv,
|
|
21
|
+
allowPositionals: true,
|
|
22
|
+
options: { help: { type: 'boolean', short: 'h' } },
|
|
23
|
+
});
|
|
24
|
+
const commande = positionals[0];
|
|
25
|
+
if (values.help || (commande !== 'sync' && commande !== 'check')) {
|
|
26
|
+
console.error(AIDE);
|
|
27
|
+
return values.help && commande === undefined ? 0 : 2;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const options = await chargerOptions(racine);
|
|
31
|
+
if (commande === 'sync') {
|
|
32
|
+
const ecrits = await synchroniser(racine, options);
|
|
33
|
+
console.log(ecrits.length === 0 ? 'Fichiers à jour.' : ecrits.map((f) => `réécrit : ${f}`).join('\n'));
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
const violations = await verifier(racine, options);
|
|
37
|
+
if (violations.length === 0) {
|
|
38
|
+
console.log('Aucun problème.');
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
console.log(formaterViolations(violations));
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
if (!(e instanceof ErreurCms))
|
|
46
|
+
throw e;
|
|
47
|
+
console.error(e.message);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function estPrincipal() {
|
|
52
|
+
const script = process.argv[1];
|
|
53
|
+
if (script === undefined)
|
|
54
|
+
return false;
|
|
55
|
+
try {
|
|
56
|
+
return import.meta.url === pathToFileURL(realpathSync(script)).href;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (estPrincipal()) {
|
|
63
|
+
try {
|
|
64
|
+
process.exitCode = await executerCli(process.argv.slice(2), process.cwd());
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from 'astro/zod';
|
|
2
|
+
import { glob } from 'astro/loaders';
|
|
3
|
+
import type { DeclCollection, DeclSingleton } from './types.js';
|
|
4
|
+
/** Clé symbole portant les métadonnées CMS sur l'objet collection (invisible pour Astro). */
|
|
5
|
+
export declare const CMS: unique symbol;
|
|
6
|
+
type Loader = ReturnType<typeof glob>;
|
|
7
|
+
type Forme = Record<string, z.ZodType>;
|
|
8
|
+
type ChampOrdre = {
|
|
9
|
+
ordre: z.ZodInt;
|
|
10
|
+
};
|
|
11
|
+
/** Schéma strict d'une collection : sa forme, plus `ordre` si elle est ordonnable. */
|
|
12
|
+
export type SchemaCollection<S extends Forme, Ord extends boolean> = z.ZodObject<Ord extends true ? S & ChampOrdre : S, z.core.$strict>;
|
|
13
|
+
/** Type que pose `defineCollection` d'astro:content ; Astro refuse une collection à loader sans lui. */
|
|
14
|
+
declare const TYPE_ASTRO = "content_layer";
|
|
15
|
+
export declare function defineSingleton<S extends Forme>(id: string, o: {
|
|
16
|
+
libelle: string;
|
|
17
|
+
adresse?: string;
|
|
18
|
+
apercu?: string;
|
|
19
|
+
schema: S;
|
|
20
|
+
}): {
|
|
21
|
+
type: typeof TYPE_ASTRO;
|
|
22
|
+
loader: Loader;
|
|
23
|
+
schema: z.ZodObject<S, z.core.$strict>;
|
|
24
|
+
[CMS]: DeclSingleton;
|
|
25
|
+
};
|
|
26
|
+
export declare function defineCollection<S extends Forme, Ord extends boolean = false>(id: string, o: {
|
|
27
|
+
libelle: string;
|
|
28
|
+
format: 'json' | 'markdown';
|
|
29
|
+
titre: string;
|
|
30
|
+
ordonnable?: Ord;
|
|
31
|
+
schema: S;
|
|
32
|
+
}): {
|
|
33
|
+
type: typeof TYPE_ASTRO;
|
|
34
|
+
loader: Loader;
|
|
35
|
+
schema: SchemaCollection<S, Ord>;
|
|
36
|
+
[CMS]: DeclCollection;
|
|
37
|
+
};
|
|
38
|
+
export declare function lireDecl(c: unknown): DeclSingleton | DeclCollection | undefined;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'astro/zod';
|
|
2
|
+
import { glob } from 'astro/loaders';
|
|
3
|
+
import { lireMeta } from './types.js';
|
|
4
|
+
/** Clé symbole portant les métadonnées CMS sur l'objet collection (invisible pour Astro). */
|
|
5
|
+
export const CMS = Symbol.for('komekoo-cms.decl');
|
|
6
|
+
/** Type que pose `defineCollection` d'astro:content ; Astro refuse une collection à loader sans lui. */
|
|
7
|
+
const TYPE_ASTRO = 'content_layer';
|
|
8
|
+
const RE_ADRESSE = /^\/[a-z0-9\-\/]*$/;
|
|
9
|
+
const RE_ID = /^[a-z][a-z0-9-]*$/;
|
|
10
|
+
function verifierId(id) {
|
|
11
|
+
if (!RE_ID.test(id)) {
|
|
12
|
+
throw new Error(`id « ${id} » invalide : attendu ${RE_ID.source}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function defineSingleton(id, o) {
|
|
16
|
+
verifierId(id);
|
|
17
|
+
if (o.adresse !== undefined && !RE_ADRESSE.test(o.adresse)) {
|
|
18
|
+
throw new Error(`adresse « ${o.adresse} » invalide : attendu ${RE_ADRESSE.source}`);
|
|
19
|
+
}
|
|
20
|
+
if (o.apercu !== undefined && !RE_ADRESSE.test(o.apercu)) {
|
|
21
|
+
throw new Error(`apercu « ${o.apercu} » invalide : attendu ${RE_ADRESSE.source}`);
|
|
22
|
+
}
|
|
23
|
+
const decl = { type: 'singleton', id, libelle: o.libelle };
|
|
24
|
+
if (o.adresse !== undefined)
|
|
25
|
+
decl.adresse = o.adresse;
|
|
26
|
+
if (o.apercu !== undefined)
|
|
27
|
+
decl.apercu = o.apercu;
|
|
28
|
+
return {
|
|
29
|
+
type: TYPE_ASTRO,
|
|
30
|
+
loader: glob({ pattern: `${id}.json`, base: './src/content/pages' }),
|
|
31
|
+
schema: z.strictObject(o.schema),
|
|
32
|
+
[CMS]: decl,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function defineCollection(id, o) {
|
|
36
|
+
verifierId(id);
|
|
37
|
+
if (id === 'pages') {
|
|
38
|
+
throw new Error("l'id « pages » est réservé aux singletons");
|
|
39
|
+
}
|
|
40
|
+
const champTitre = o.schema[o.titre];
|
|
41
|
+
if (!champTitre || lireMeta(champTitre)?.editeur !== 'texte') {
|
|
42
|
+
throw new Error(`titre « ${o.titre} » doit désigner un champ existant de type texte`);
|
|
43
|
+
}
|
|
44
|
+
const ordonnable = o.ordonnable ?? false;
|
|
45
|
+
const forme = ordonnable ? { ...o.schema, ordre: z.int().min(0) } : o.schema;
|
|
46
|
+
return {
|
|
47
|
+
type: TYPE_ASTRO,
|
|
48
|
+
loader: glob({ pattern: o.format === 'json' ? '**/*.json' : '**/*.md', base: `./src/content/${id}` }),
|
|
49
|
+
schema: z.strictObject(forme),
|
|
50
|
+
[CMS]: { type: 'collection', id, libelle: o.libelle, format: o.format, titre: o.titre, ordonnable },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function lireDecl(c) {
|
|
54
|
+
if (typeof c !== 'object' || c === null)
|
|
55
|
+
return undefined;
|
|
56
|
+
return c[CMS];
|
|
57
|
+
}
|
package/dist/images.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const LARGEURS: readonly [800, 1600, 2400];
|
|
2
|
+
export type Variante = {
|
|
3
|
+
largeur: (typeof LARGEURS)[number];
|
|
4
|
+
url: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function variantes(src: string): Variante[];
|
|
7
|
+
/** Variantes strictement plus petites que l'original, puis le fichier -2400 (original non agrandi). */
|
|
8
|
+
export declare function srcsetDe(src: string, largeurOriginale: number): string;
|
package/dist/images.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MOTIF_SRC_IMAGE } from './motifs.js';
|
|
2
|
+
export const LARGEURS = [800, 1600, 2400];
|
|
3
|
+
export function variantes(src) {
|
|
4
|
+
if (!MOTIF_SRC_IMAGE.test(src)) {
|
|
5
|
+
throw new Error(`Chemin d'image invalide : ${src}`);
|
|
6
|
+
}
|
|
7
|
+
const base = src.slice(0, -'-2400.webp'.length);
|
|
8
|
+
return LARGEURS.map((largeur) => ({ largeur, url: `${base}-${largeur}.webp` }));
|
|
9
|
+
}
|
|
10
|
+
/** Variantes strictement plus petites que l'original, puis le fichier -2400 (original non agrandi). */
|
|
11
|
+
export function srcsetDe(src, largeurOriginale) {
|
|
12
|
+
const toutes = variantes(src);
|
|
13
|
+
const plusGrande = toutes[toutes.length - 1];
|
|
14
|
+
const entrees = toutes
|
|
15
|
+
.filter((v) => v !== plusGrande && v.largeur < largeurOriginale)
|
|
16
|
+
.map((v) => `${v.url} ${v.largeur}w`);
|
|
17
|
+
entrees.push(`${plusGrande.url} ${Math.min(largeurOriginale, 2400)}w`);
|
|
18
|
+
return entrees.join(', ');
|
|
19
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const VERSION_SCHEMA = 1;
|
|
2
|
+
export { champ } from './champs.js';
|
|
3
|
+
export { lireMeta } from './types.js';
|
|
4
|
+
export type { Editeur, MetaCms, OptionsBase, DeclSingleton, DeclCollection } from './types.js';
|
|
5
|
+
export { ErreurCms } from './noyau/erreurs.js';
|
|
6
|
+
export { defineSingleton, defineCollection, lireDecl, CMS } from './collections.js';
|
|
7
|
+
export { cms } from './marqueur.js';
|
|
8
|
+
export { creerAjv, MOTS_CLES_CMS } from './ajv.js';
|
|
9
|
+
export { versManifest } from './noyau/manifest.js';
|
|
10
|
+
export type { OptionsCms } from './noyau/manifest.js';
|
|
11
|
+
export { HORS_PERIMETRE_SOCLE } from './socle/hors-perimetre.js';
|
|
12
|
+
export { LARGEURS, variantes, srcsetDe } from './images.js';
|
|
13
|
+
export type { Variante } from './images.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const VERSION_SCHEMA = 1;
|
|
2
|
+
export { champ } from './champs.js';
|
|
3
|
+
export { lireMeta } from './types.js';
|
|
4
|
+
export { ErreurCms } from './noyau/erreurs.js';
|
|
5
|
+
export { defineSingleton, defineCollection, lireDecl, CMS } from './collections.js';
|
|
6
|
+
export { cms } from './marqueur.js';
|
|
7
|
+
export { creerAjv, MOTS_CLES_CMS } from './ajv.js';
|
|
8
|
+
export { versManifest } from './noyau/manifest.js';
|
|
9
|
+
export { HORS_PERIMETRE_SOCLE } from './socle/hors-perimetre.js';
|
|
10
|
+
export { LARGEURS, variantes, srcsetDe } from './images.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro';
|
|
2
|
+
import type { OptionsCms } from './noyau/manifest.js';
|
|
3
|
+
/** `Symbol.for` : la même clé que l'intégration soit chargée par Node ou par Vite (`chargerOptions`). */
|
|
4
|
+
export declare const OPTIONS: unique symbol;
|
|
5
|
+
export default function cms(options: OptionsCms): AstroIntegration & {
|
|
6
|
+
[OPTIONS]: OptionsCms;
|
|
7
|
+
};
|
|
8
|
+
/** Retrouve les options passées à `cms()` dans `astro.config.mjs` (utilisé par la CLI). */
|
|
9
|
+
export declare function chargerOptions(racine: string): Promise<OptionsCms>;
|