@komekoo/astro-cms 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +127 -2
  2. package/dist/ajv.d.ts +4 -0
  3. package/dist/ajv.js +23 -0
  4. package/dist/champs.d.ts +49 -0
  5. package/dist/champs.js +58 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.js +70 -0
  8. package/dist/collections.d.ts +38 -0
  9. package/dist/collections.js +52 -0
  10. package/dist/images.d.ts +8 -0
  11. package/dist/images.js +19 -0
  12. package/dist/index.d.ts +12 -0
  13. package/dist/index.js +9 -0
  14. package/dist/integration.d.ts +9 -0
  15. package/dist/integration.js +91 -0
  16. package/dist/motifs.d.ts +8 -0
  17. package/dist/motifs.js +9 -0
  18. package/dist/navigateur.d.ts +5 -0
  19. package/dist/navigateur.js +5 -0
  20. package/dist/noyau/charger.d.ts +4 -0
  21. package/dist/noyau/charger.js +48 -0
  22. package/dist/noyau/collecter.d.ts +26 -0
  23. package/dist/noyau/collecter.js +132 -0
  24. package/dist/noyau/contenus.d.ts +6 -0
  25. package/dist/noyau/contenus.js +218 -0
  26. package/dist/noyau/erreurs.d.ts +7 -0
  27. package/dist/noyau/erreurs.js +14 -0
  28. package/dist/noyau/frontmatter.d.ts +15 -0
  29. package/dist/noyau/frontmatter.js +70 -0
  30. package/dist/noyau/json-schema.d.ts +4 -0
  31. package/dist/noyau/json-schema.js +58 -0
  32. package/dist/noyau/manifest.d.ts +15 -0
  33. package/dist/noyau/manifest.js +136 -0
  34. package/dist/noyau/markdown.d.ts +11 -0
  35. package/dist/noyau/markdown.js +95 -0
  36. package/dist/noyau/operations.d.ts +12 -0
  37. package/dist/noyau/operations.js +54 -0
  38. package/dist/noyau/serialiser.d.ts +2 -0
  39. package/dist/noyau/serialiser.js +25 -0
  40. package/dist/noyau/version.d.ts +5 -0
  41. package/dist/noyau/version.js +25 -0
  42. package/dist/socle/hors-perimetre.d.ts +2 -0
  43. package/dist/socle/hors-perimetre.js +9 -0
  44. package/dist/types.d.ts +33 -0
  45. package/dist/types.js +10 -0
  46. package/package.json +53 -3
  47. package/src/composants/CmsImage.astro +24 -0
@@ -0,0 +1,48 @@
1
+ import path from 'node:path';
2
+ import { createServer } from 'vite';
3
+ import { ErreurCms } from './erreurs.js';
4
+ const FICHIER_CONFIG = 'src/content.config.ts';
5
+ const FICHIER_ASTRO = 'astro.config.mjs';
6
+ /**
7
+ * Charge un module du site via Vite (même chaîne de résolution qu'Astro) et le passe à `lire`.
8
+ * Un serveur neuf par appel : `ssrLoadModule` met les modules en cache, une config modifiée doit être relue.
9
+ */
10
+ async function chargerModule(racine, fichier, lire) {
11
+ const serveur = await createServer({
12
+ root: path.resolve(racine),
13
+ appType: 'custom',
14
+ server: { middlewareMode: true, watch: null, ws: false },
15
+ optimizeDeps: { noDiscovery: true },
16
+ logLevel: 'silent',
17
+ configFile: false,
18
+ });
19
+ try {
20
+ return lire(await serveur.ssrLoadModule(`/${fichier}`));
21
+ }
22
+ catch (e) {
23
+ if (e instanceof ErreurCms)
24
+ throw e;
25
+ const cause = e instanceof Error ? e.message : String(e);
26
+ if (cause.includes('astro:content')) {
27
+ throw new ErreurCms("import de « astro:content » non pris en charge par komekoo-cms : utilise astro/zod, astro/loaders et l'API de @komekoo/astro-cms", fichier);
28
+ }
29
+ throw new ErreurCms(`chargement impossible : ${cause}`, fichier);
30
+ }
31
+ finally {
32
+ await serveur.close();
33
+ }
34
+ }
35
+ /** Charge `src/content.config.ts` du site et renvoie ses `collections`. */
36
+ export function chargerCollections(racine) {
37
+ return chargerModule(racine, FICHIER_CONFIG, (module) => {
38
+ const collections = module['collections'];
39
+ if (typeof collections !== 'object' || collections === null) {
40
+ throw new ErreurCms('`collections` n’est pas exporté', FICHIER_CONFIG);
41
+ }
42
+ return collections;
43
+ });
44
+ }
45
+ /** Charge `astro.config.mjs` et renvoie son export par défaut. */
46
+ export function chargerConfigAstro(racine) {
47
+ return chargerModule(racine, FICHIER_ASTRO, (module) => module['default']);
48
+ }
@@ -0,0 +1,26 @@
1
+ import type { z } from 'astro/zod';
2
+ import type { Editeur } from '../types.js';
3
+ export interface ModeleSingleton {
4
+ id: string;
5
+ libelle: string;
6
+ /** `src/content/pages/<id>.json` */
7
+ fichier: string;
8
+ adresse?: string;
9
+ schema: z.ZodObject;
10
+ }
11
+ export interface ModeleCollection {
12
+ id: string;
13
+ libelle: string;
14
+ dossier: string;
15
+ format: 'json' | 'markdown';
16
+ titre: string;
17
+ ordonnable: boolean;
18
+ schema: z.ZodObject;
19
+ }
20
+ export interface ModeleCms {
21
+ singletons: ModeleSingleton[];
22
+ collections: ModeleCollection[];
23
+ }
24
+ export declare function collecter(collections: Record<string, unknown>): ModeleCms;
25
+ /** Déduit l'éditeur d'un schéma Zod brut ; la méta CMS prime quand elle existe. */
26
+ export declare function deduireEditeur(schema: z.ZodType): Editeur;
@@ -0,0 +1,132 @@
1
+ import { lireDecl } from '../collections.js';
2
+ import { lireMeta } from '../types.js';
3
+ import { ErreurCms } from './erreurs.js';
4
+ const def = (s) => s._zod.def;
5
+ const REFUSES = {
6
+ function: 'type « function » non sérialisable',
7
+ transform: 'transformation non sérialisable',
8
+ any: 'type « any » refusé : déclare un type précis',
9
+ unknown: 'type « unknown » refusé : déclare un type précis',
10
+ date: 'type « date » refusé : utilise champ.date()',
11
+ };
12
+ const FEUILLES = new Set(['string', 'number', 'boolean', 'enum', 'literal', 'null']);
13
+ const ENVELOPPES = new Set(['optional', 'nullable', 'default', 'prefault', 'nonoptional', 'readonly']);
14
+ function verifier(schema, chemin, vus) {
15
+ if (vus.has(schema))
16
+ return;
17
+ vus.add(schema);
18
+ const d = def(schema);
19
+ const refus = REFUSES[d.type];
20
+ if (refus !== undefined)
21
+ throw new ErreurCms(`${chemin} : ${refus}`);
22
+ if (FEUILLES.has(d.type))
23
+ return;
24
+ if (ENVELOPPES.has(d.type)) {
25
+ if (d.innerType)
26
+ verifier(d.innerType, chemin, vus);
27
+ return;
28
+ }
29
+ switch (d.type) {
30
+ case 'object':
31
+ for (const [cle, sous] of Object.entries(d.shape ?? {}))
32
+ verifier(sous, `${chemin}.${cle}`, vus);
33
+ return;
34
+ case 'array':
35
+ if (d.element)
36
+ verifier(d.element, `${chemin}[]`, vus);
37
+ return;
38
+ case 'union':
39
+ if (d.discriminator === undefined) {
40
+ throw new ErreurCms(`${chemin} : union sans discriminant refusée : utilise z.discriminatedUnion()`);
41
+ }
42
+ for (const option of d.options ?? [])
43
+ verifier(option, chemin, vus);
44
+ return;
45
+ case 'pipe':
46
+ if (def(d.in).type === 'transform' || def(d.out).type === 'transform') {
47
+ throw new ErreurCms(`${chemin} : ${REFUSES.transform}`);
48
+ }
49
+ verifier(d.in, chemin, vus);
50
+ verifier(d.out, chemin, vus);
51
+ return;
52
+ default:
53
+ throw new ErreurCms(`${chemin} : type « ${d.type} » non pris en charge`);
54
+ }
55
+ }
56
+ export function collecter(collections) {
57
+ const modele = { singletons: [], collections: [] };
58
+ for (const [cle, valeur] of Object.entries(collections)) {
59
+ const decl = lireDecl(valeur);
60
+ if (!decl)
61
+ continue;
62
+ if (cle !== decl.id) {
63
+ throw new ErreurCms(`la clé « ${cle} » de l'export collections diffère de l'id déclaré « ${decl.id} »`);
64
+ }
65
+ const schema = valeur.schema;
66
+ verifier(schema, decl.id, new WeakSet());
67
+ if (decl.type === 'singleton') {
68
+ modele.singletons.push({
69
+ id: decl.id,
70
+ libelle: decl.libelle,
71
+ fichier: `src/content/pages/${decl.id}.json`,
72
+ ...(decl.adresse !== undefined && { adresse: decl.adresse }),
73
+ schema,
74
+ });
75
+ }
76
+ else {
77
+ modele.collections.push({
78
+ id: decl.id,
79
+ libelle: decl.libelle,
80
+ dossier: `src/content/${decl.id}`,
81
+ format: decl.format,
82
+ titre: decl.titre,
83
+ ordonnable: decl.ordonnable,
84
+ schema,
85
+ });
86
+ }
87
+ }
88
+ const parId = (a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
89
+ modele.singletons.sort(parId);
90
+ modele.collections.sort(parId);
91
+ return modele;
92
+ }
93
+ /** Déduit l'éditeur d'un schéma Zod brut ; la méta CMS prime quand elle existe. */
94
+ export function deduireEditeur(schema) {
95
+ const meta = lireMeta(schema);
96
+ if (meta)
97
+ return meta.editeur;
98
+ const d = def(schema);
99
+ switch (d.type) {
100
+ case 'string':
101
+ return 'texte';
102
+ case 'literal': {
103
+ const t = typeof d.values?.[0];
104
+ if (t === 'string')
105
+ return 'texte';
106
+ if (t === 'number')
107
+ return 'nombre';
108
+ if (t === 'boolean')
109
+ return 'booleen';
110
+ throw new ErreurCms('impossible de déduire un éditeur pour ce littéral');
111
+ }
112
+ case 'number':
113
+ return 'nombre';
114
+ case 'boolean':
115
+ return 'booleen';
116
+ case 'enum':
117
+ return 'choix';
118
+ case 'array':
119
+ return 'liste';
120
+ case 'object':
121
+ return 'groupe';
122
+ case 'optional':
123
+ case 'nullable':
124
+ case 'default':
125
+ case 'prefault':
126
+ case 'nonoptional':
127
+ case 'readonly':
128
+ return deduireEditeur(d.innerType);
129
+ default:
130
+ throw new ErreurCms(`impossible de déduire un éditeur pour le type « ${d.type} »`);
131
+ }
132
+ }
@@ -0,0 +1,6 @@
1
+ import type { Violation } from './markdown.js';
2
+ import type { ModeleCms } from './collecter.js';
3
+ /** Valide tous les fichiers de contenu d'un site. Ordre : singletons par id, collections par id, fichiers triés. */
4
+ export declare function validerContenus(racine: string, modele: ModeleCms): Promise<Violation[]>;
5
+ /** Une ligne par violation : `fichier:ligne — motif` (ordre d'entrée conservé). */
6
+ export declare function formaterViolations(v: Violation[]): string;
@@ -0,0 +1,218 @@
1
+ import { promises as fsp } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { MOTIF_SRC_IMAGE } from '../champs.js';
4
+ import { lireMeta } from '../types.js';
5
+ import { lireFrontmatter } from './frontmatter.js';
6
+ import { extraireImages, validerMarkdown } from './markdown.js';
7
+ const DOSSIER_PAGES = 'src/content/pages';
8
+ const TAILLES = ['800', '1600', '2400'];
9
+ const def = (s) => s._zod.def;
10
+ const ENVELOPPES = new Set(['optional', 'nullable', 'default', 'prefault', 'nonoptional', 'readonly']);
11
+ const estObjet = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
12
+ const posix = (p) => p.split(path.sep).join('/');
13
+ function formaterChemin(chemin) {
14
+ let s = '';
15
+ for (const p of chemin) {
16
+ if (typeof p === 'number')
17
+ s += `[${p}]`;
18
+ else
19
+ s += s === '' ? String(p) : `.${String(p)}`;
20
+ }
21
+ return s;
22
+ }
23
+ /**
24
+ * Fichiers d'un dossier (récursif), chemins POSIX relatifs à `dossier`, triés ; dossier absent → `[]`.
25
+ * Fichiers et dossiers cachés (`.DS_Store`, `.gitkeep`…) ignorés, comme le glob loader d'Astro (`dot: false`).
26
+ */
27
+ async function lister(dossier) {
28
+ let entrees;
29
+ try {
30
+ entrees = await fsp.readdir(dossier, { recursive: true, withFileTypes: true });
31
+ }
32
+ catch (e) {
33
+ if (e.code === 'ENOENT')
34
+ return [];
35
+ throw e;
36
+ }
37
+ return entrees
38
+ .filter((e) => e.isFile())
39
+ .map((e) => posix(path.relative(dossier, path.join(e.parentPath, e.name))))
40
+ .filter((rel) => !rel.split('/').some((segment) => segment.startsWith('.')))
41
+ .sort();
42
+ }
43
+ async function lireTexte(chemin) {
44
+ try {
45
+ return await fsp.readFile(chemin, 'utf8');
46
+ }
47
+ catch (e) {
48
+ if (e.code === 'ENOENT')
49
+ return undefined;
50
+ throw e;
51
+ }
52
+ }
53
+ async function existe(chemin) {
54
+ try {
55
+ await fsp.access(chemin);
56
+ return true;
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ class Collecteur {
63
+ racine;
64
+ fichier;
65
+ violations = [];
66
+ constructor(racine, fichier) {
67
+ this.racine = racine;
68
+ this.fichier = fichier;
69
+ }
70
+ ajouter(motif, ligne) {
71
+ this.violations.push(ligne === undefined ? { fichier: this.fichier, motif } : { fichier: this.fichier, ligne, motif });
72
+ }
73
+ /** Vérifie les trois variantes d'une image. Un `src` hors motif n'est jamais résolu sur le disque. */
74
+ async image(src, ligne) {
75
+ if (typeof src !== 'string' || !MOTIF_SRC_IMAGE.test(src))
76
+ return;
77
+ const uploads = path.resolve(this.racine, 'public', 'uploads');
78
+ for (const taille of TAILLES) {
79
+ const variante = path.resolve(this.racine, 'public', `${src.slice(1, -'2400.webp'.length)}${taille}.webp`);
80
+ if (!variante.startsWith(uploads + path.sep))
81
+ continue;
82
+ if (!(await existe(variante))) {
83
+ this.ajouter(`variante d'image absente : ${posix(path.relative(this.racine, variante))}`, ligne);
84
+ }
85
+ }
86
+ }
87
+ /** Markdown d'un champ `richtext` : violations préfixées du chemin du champ (ligne inconnue). */
88
+ async richtext(valeur, chemin) {
89
+ if (typeof valeur !== 'string')
90
+ return;
91
+ for (const v of validerMarkdown(valeur))
92
+ this.ajouter(`${chemin} : ${v.motif}`);
93
+ for (const { url } of extraireImages(valeur))
94
+ await this.image(url);
95
+ }
96
+ /** Parcourt données et schéma ensemble : images et champs richtext. */
97
+ async parcourir(schema, valeur, chemin) {
98
+ let noeud = schema;
99
+ for (;;) {
100
+ const editeur = lireMeta(noeud)?.editeur;
101
+ if (editeur === 'image') {
102
+ if (estObjet(valeur))
103
+ await this.image(valeur.src);
104
+ return;
105
+ }
106
+ if (editeur === 'richtext')
107
+ return this.richtext(valeur, chemin);
108
+ const d = def(noeud);
109
+ if (!ENVELOPPES.has(d.type) || !d.innerType)
110
+ break;
111
+ noeud = d.innerType;
112
+ }
113
+ const d = def(noeud);
114
+ if (d.type === 'object' && estObjet(valeur)) {
115
+ for (const [cle, sous] of Object.entries(d.shape ?? {})) {
116
+ if (cle in valeur)
117
+ await this.parcourir(sous, valeur[cle], chemin === '' ? cle : `${chemin}.${cle}`);
118
+ }
119
+ }
120
+ else if (d.type === 'array' && d.element && Array.isArray(valeur)) {
121
+ for (const [i, v] of valeur.entries())
122
+ await this.parcourir(d.element, v, `${chemin}[${i}]`);
123
+ }
124
+ else if (d.type === 'union' && d.discriminator !== undefined && estObjet(valeur)) {
125
+ const option = d.options?.find((o) => {
126
+ const champDisc = def(o).shape?.[d.discriminator];
127
+ return champDisc !== undefined && def(champDisc).values?.includes(valeur[d.discriminator]);
128
+ });
129
+ if (option)
130
+ await this.parcourir(option, valeur, chemin);
131
+ }
132
+ }
133
+ /** Validation Zod puis parcours des images et du richtext. */
134
+ async donnees(schema, donnees) {
135
+ const r = schema.safeParse(donnees);
136
+ if (!r.success) {
137
+ for (const issue of r.error.issues) {
138
+ const chemin = formaterChemin(issue.path);
139
+ this.ajouter(chemin === '' ? issue.message : `${chemin} : ${issue.message}`);
140
+ }
141
+ }
142
+ await this.parcourir(schema, donnees, '');
143
+ }
144
+ }
145
+ function ligneJson(message) {
146
+ const m = /\(line (\d+) column \d+\)/.exec(message);
147
+ return m ? Number(m[1]) : undefined;
148
+ }
149
+ async function validerJson(racine, fichier, texte, schema) {
150
+ const c = new Collecteur(racine, fichier);
151
+ let donnees;
152
+ try {
153
+ donnees = JSON.parse(texte);
154
+ }
155
+ catch (e) {
156
+ c.ajouter(`JSON illisible : ${e.message}`, ligneJson(e.message));
157
+ return c.violations;
158
+ }
159
+ await c.donnees(schema, donnees);
160
+ return c.violations;
161
+ }
162
+ async function validerMd(racine, fichier, texte, schema) {
163
+ const c = new Collecteur(racine, fichier);
164
+ let lu;
165
+ try {
166
+ lu = lireFrontmatter(texte);
167
+ }
168
+ catch (e) {
169
+ const err = e;
170
+ c.ajouter(`frontmatter illisible : ${err.message}`, err.linePos?.[0]?.line);
171
+ return c.violations;
172
+ }
173
+ await c.donnees(schema, lu.donnees);
174
+ for (const v of validerMarkdown(lu.corps, lu.ligneCorps - 1)) {
175
+ c.ajouter(v.motif, v.ligne);
176
+ }
177
+ for (const { url, ligne } of extraireImages(lu.corps)) {
178
+ await c.image(url, ligne === undefined ? undefined : ligne + lu.ligneCorps - 1);
179
+ }
180
+ return c.violations;
181
+ }
182
+ /** Valide tous les fichiers de contenu d'un site. Ordre : singletons par id, collections par id, fichiers triés. */
183
+ export async function validerContenus(racine, modele) {
184
+ const violations = [];
185
+ const declares = new Set(modele.singletons.map((s) => s.fichier));
186
+ for (const s of modele.singletons) {
187
+ const texte = await lireTexte(path.join(racine, s.fichier));
188
+ if (texte === undefined)
189
+ violations.push({ fichier: s.fichier, motif: 'fichier manquant' });
190
+ else
191
+ violations.push(...(await validerJson(racine, s.fichier, texte, s.schema)));
192
+ }
193
+ for (const f of await lister(path.join(racine, DOSSIER_PAGES))) {
194
+ const fichier = `${DOSSIER_PAGES}/${f}`;
195
+ if (!declares.has(fichier))
196
+ violations.push({ fichier, motif: 'fichier non déclaré comme singleton' });
197
+ }
198
+ for (const col of modele.collections) {
199
+ for (const f of await lister(path.join(racine, col.dossier))) {
200
+ const fichier = `${col.dossier}/${f}`;
201
+ const texte = await fsp.readFile(path.join(racine, fichier), 'utf8');
202
+ if (f.endsWith('.json'))
203
+ violations.push(...(await validerJson(racine, fichier, texte, col.schema)));
204
+ else if (f.endsWith('.md'))
205
+ violations.push(...(await validerMd(racine, fichier, texte, col.schema)));
206
+ }
207
+ }
208
+ return violations;
209
+ }
210
+ /** Une ligne par violation : `fichier:ligne — motif` (ordre d'entrée conservé). */
211
+ export function formaterViolations(v) {
212
+ return v
213
+ .map((x) => {
214
+ const lieu = x.fichier === undefined ? '' : x.ligne === undefined ? `${x.fichier} — ` : `${x.fichier}:${x.ligne} — `;
215
+ return `${lieu}${x.motif}`;
216
+ })
217
+ .join('\n');
218
+ }
@@ -0,0 +1,7 @@
1
+ /** Erreur destinée au développeur ; message au format `fichier:ligne — motif` (fichier/ligne omis si inconnus). */
2
+ export declare class ErreurCms extends Error {
3
+ readonly motif: string;
4
+ readonly fichier: string | undefined;
5
+ readonly ligne: number | undefined;
6
+ constructor(motif: string, fichier?: string, ligne?: number);
7
+ }
@@ -0,0 +1,14 @@
1
+ /** Erreur destinée au développeur ; message au format `fichier:ligne — motif` (fichier/ligne omis si inconnus). */
2
+ export class ErreurCms extends Error {
3
+ motif;
4
+ fichier;
5
+ ligne;
6
+ constructor(motif, fichier, ligne) {
7
+ const lieu = fichier === undefined ? '' : ligne === undefined ? `${fichier} — ` : `${fichier}:${ligne} — `;
8
+ super(`${lieu}${motif}`);
9
+ this.name = 'ErreurCms';
10
+ this.motif = motif;
11
+ this.fichier = fichier;
12
+ this.ligne = ligne;
13
+ }
14
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Sépare frontmatter et corps. `ligneCorps` est la ligne (1-based) du fichier où commence
3
+ * `corps` : la ligne `n` du corps est la ligne `ligneCorps + n - 1` du fichier.
4
+ * Lève l'erreur du parseur YAML (`YAMLParseError`, avec `linePos`) si le YAML est invalide.
5
+ */
6
+ export declare function lireFrontmatter(source: string): {
7
+ donnees: unknown;
8
+ corps: string;
9
+ ligneCorps: number;
10
+ };
11
+ /**
12
+ * Inverse de `lireFrontmatter` : `---`, YAML (clés dans l'ordre reçu, lignes non repliées), `---`, corps.
13
+ * Les chaînes qui ressemblent à une date sont entre guillemets doubles, pour être relues comme chaînes.
14
+ */
15
+ export declare function ecrireFrontmatter(donnees: Record<string, unknown>, corps: string): string;
@@ -0,0 +1,70 @@
1
+ import { Document, isScalar, parseDocument, visit } from 'yaml';
2
+ import { ErreurCms } from './erreurs.js';
3
+ // Extraction identique à `@astrojs/internal-helpers/frontmatter` (utilisée par Astro 7).
4
+ const RE_FRONTMATTER = /(?:^?|^\s*\n)(?:---|\+\+\+)([\s\S]*?\n)(?:---|\+\+\+)/;
5
+ const RE_TYPE = /(?:^?|^\s*\n)(---|\+\+\+)/;
6
+ /**
7
+ * Même lecture YAML qu'Astro 7 (`@astrojs/internal-helpers/yaml`) : paquet `yaml`,
8
+ * schéma `core` + tag `timestamp` + `merge`. Une date non entre guillemets devient donc
9
+ * un objet `Date`, que `z.iso.date()` refuse — au build comme ici.
10
+ */
11
+ function lireYaml(source) {
12
+ const document = parseDocument(source, { customTags: ['timestamp'], merge: true, schema: 'core' });
13
+ if (document.errors.length > 0)
14
+ throw document.errors[0];
15
+ if (document.contents === null)
16
+ return undefined;
17
+ return document.toJS();
18
+ }
19
+ const compterLignes = (s) => s.split('\n').length - 1;
20
+ /**
21
+ * Sépare frontmatter et corps. `ligneCorps` est la ligne (1-based) du fichier où commence
22
+ * `corps` : la ligne `n` du corps est la ligne `ligneCorps + n - 1` du fichier.
23
+ * Lève l'erreur du parseur YAML (`YAMLParseError`, avec `linePos`) si le YAML est invalide.
24
+ */
25
+ export function lireFrontmatter(source) {
26
+ const trouve = RE_FRONTMATTER.exec(source);
27
+ const brut = trouve?.[1];
28
+ if (trouve === null || brut === undefined)
29
+ return { donnees: {}, corps: source, ligneCorps: 1 };
30
+ if (RE_TYPE.exec(source)?.[1] === '+++') {
31
+ throw new ErreurCms('frontmatter TOML (+++) non pris en charge : utilise YAML (---)');
32
+ }
33
+ const analyse = lireYaml(brut);
34
+ const donnees = analyse !== null && typeof analyse === 'object' ? analyse : {};
35
+ const fin = trouve.index + trouve[0].length;
36
+ const reste = source.slice(fin);
37
+ // Le corps commence à la ligne qui suit la clôture `---`.
38
+ const saut = /^[^\S\n]*\r?\n/.exec(reste);
39
+ const corps = saut ? reste.slice(saut[0].length) : reste;
40
+ const ligneCloture = compterLignes(source.slice(0, fin)) + 1;
41
+ return { donnees, corps, ligneCorps: ligneCloture + (saut ? 1 : 0) };
42
+ }
43
+ /** Vrai si la chaîne, écrite sans guillemets, serait relue par `lireYaml` autrement que comme cette chaîne (ex. date). */
44
+ function sRelitAutrement(valeur) {
45
+ if (!/^\d/.test(valeur) || valeur.includes('\n'))
46
+ return false;
47
+ try {
48
+ const relu = lireYaml(`k: ${valeur}`);
49
+ return relu?.k !== valeur;
50
+ }
51
+ catch {
52
+ return true;
53
+ }
54
+ }
55
+ /**
56
+ * Inverse de `lireFrontmatter` : `---`, YAML (clés dans l'ordre reçu, lignes non repliées), `---`, corps.
57
+ * Les chaînes qui ressemblent à une date sont entre guillemets doubles, pour être relues comme chaînes.
58
+ */
59
+ export function ecrireFrontmatter(donnees, corps) {
60
+ if (Object.keys(donnees).length === 0)
61
+ return `---\n---\n${corps}`;
62
+ const document = new Document(donnees);
63
+ visit(document, {
64
+ Scalar(_cle, noeud) {
65
+ if (isScalar(noeud) && typeof noeud.value === 'string' && sRelitAutrement(noeud.value))
66
+ noeud.type = 'QUOTE_DOUBLE';
67
+ },
68
+ });
69
+ return `---\n${document.toString({ lineWidth: 0 })}---\n${corps}`;
70
+ }
@@ -0,0 +1,4 @@
1
+ import type { ModeleCms } from './collecter.js';
2
+ import type { OptionsCms } from './manifest.js';
3
+ /** Document `cms.schema.json` (voir spec, section 5). */
4
+ export declare function versJsonSchema(modele: ModeleCms, options: OptionsCms): object;
@@ -0,0 +1,58 @@
1
+ import { z } from 'astro/zod';
2
+ const ACCENT_DEFAUT = '#5DEFB7';
3
+ /** Renomme la méta `cms` en `x-cms` et impose `additionalProperties: false` sur les objets. */
4
+ function surcharger(ctx) {
5
+ const js = ctx.jsonSchema;
6
+ if ('cms' in js) {
7
+ js['x-cms'] = js.cms;
8
+ delete js.cms;
9
+ }
10
+ if (js.type === 'object')
11
+ js.additionalProperties = false;
12
+ }
13
+ function convertir(schema) {
14
+ const js = z.toJSONSchema(schema, {
15
+ target: 'draft-2020-12',
16
+ io: 'input',
17
+ override: surcharger,
18
+ });
19
+ delete js.$schema;
20
+ return js;
21
+ }
22
+ /** Document `cms.schema.json` (voir spec, section 5). */
23
+ export function versJsonSchema(modele, options) {
24
+ const singletons = {};
25
+ for (const s of modele.singletons) {
26
+ singletons[s.id] = {
27
+ 'x-cms-libelle': s.libelle,
28
+ 'x-cms-fichier': s.fichier,
29
+ ...(s.adresse !== undefined && { 'x-cms-adresse': s.adresse }),
30
+ ...convertir(s.schema),
31
+ };
32
+ }
33
+ const collections = {};
34
+ for (const c of modele.collections) {
35
+ const entree = {
36
+ 'x-cms-libelle': c.libelle,
37
+ 'x-cms-dossier': c.dossier,
38
+ 'x-cms-format': c.format,
39
+ 'x-cms-titre': c.titre,
40
+ 'x-cms-ordonnable': c.ordonnable,
41
+ };
42
+ if (c.format === 'markdown')
43
+ entree['x-cms-corps'] = { titres: [2, 3], tableaux: true };
44
+ entree.items = convertir(c.schema);
45
+ collections[c.id] = entree;
46
+ }
47
+ const site = { nom: options.site, url: options.url, accent: options.admin?.accent ?? ACCENT_DEFAUT };
48
+ if (options.admin?.logo !== undefined)
49
+ site.logo = options.admin.logo;
50
+ site.contact = options.contact;
51
+ return {
52
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
53
+ 'x-cms-version': 1,
54
+ 'x-cms-site': site,
55
+ singletons,
56
+ collections,
57
+ };
58
+ }
@@ -0,0 +1,15 @@
1
+ import type { ModeleCms } from './collecter.js';
2
+ export interface OptionsCms {
3
+ site: string;
4
+ contact: string;
5
+ horsPerimetre?: string[];
6
+ /** URL publique du site, en https, sans « / » final. */
7
+ url: string;
8
+ admin?: {
9
+ accent?: string;
10
+ logo?: string;
11
+ origine?: string;
12
+ };
13
+ }
14
+ /** Vue du modèle pour l'IA : langage naturel, aucun chemin de fichier. */
15
+ export declare function versManifest(modele: ModeleCms, options: OptionsCms): object;