@kiubi/mcp-designer 1.0.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 (65) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +219 -0
  3. package/package.json +57 -0
  4. package/src/app.ts +39 -0
  5. package/src/index.ts +21 -0
  6. package/src/lib/stemmer.ts +38 -0
  7. package/src/lib/theme-checker/block-validator.ts +353 -0
  8. package/src/lib/theme-checker/checker.ts +897 -0
  9. package/src/lib/theme-checker/filter-validator.ts +163 -0
  10. package/src/lib/theme-checker/index.ts +6 -0
  11. package/src/lib/theme-checker/inspector.ts +193 -0
  12. package/src/lib/theme-checker/lang.ts +42 -0
  13. package/src/lib/theme-checker/parsers/component-xml.ts +120 -0
  14. package/src/lib/theme-checker/parsers/model-xml.ts +82 -0
  15. package/src/lib/theme-checker/parsers/post-type-xml.ts +92 -0
  16. package/src/lib/theme-checker/parsers/symbol-xml.ts +162 -0
  17. package/src/lib/theme-checker/parsers/theme-xml.ts +102 -0
  18. package/src/lib/theme-checker/types.ts +97 -0
  19. package/src/logger.ts +28 -0
  20. package/src/tool.ts +13 -0
  21. package/src/tools/create_type.ts +535 -0
  22. package/src/tools/detail_type.ts +208 -0
  23. package/src/tools/get_doc_section.ts +201 -0
  24. package/src/tools/list_types.ts +87 -0
  25. package/src/tools/list_widgets.ts +181 -0
  26. package/src/tools/ruleset.ts +25 -0
  27. package/src/tools/search_doc.ts +160 -0
  28. package/src/tools/validate_theme.ts +63 -0
  29. package/storage/definitions/blog.json +1444 -0
  30. package/storage/definitions/catalogue.json +2817 -0
  31. package/storage/definitions/cms.json +1002 -0
  32. package/storage/definitions/commandes.json +2409 -0
  33. package/storage/definitions/comptes.json +780 -0
  34. package/storage/definitions/modules.json +418 -0
  35. package/storage/definitions/recherche.json +804 -0
  36. package/storage/documents/CLAUDE.exemple.md +281 -0
  37. package/storage/documents/generated/exemples.md +8054 -0
  38. package/storage/documents/generated/integration/01_theme_perso.md +182 -0
  39. package/storage/documents/generated/integration/02_modeles.md +220 -0
  40. package/storage/documents/generated/integration/03_widgets.md +139 -0
  41. package/storage/documents/generated/integration/04_blocs.md +45 -0
  42. package/storage/documents/generated/integration/05_balises.md +43 -0
  43. package/storage/documents/generated/integration/06_balises_globales.md +201 -0
  44. package/storage/documents/generated/integration/07_balises_widget.md +257 -0
  45. package/storage/documents/generated/integration/08_types_billets.md +154 -0
  46. package/storage/documents/generated/integration/09_types_produits.md +77 -0
  47. package/storage/documents/generated/integration/10_types_composants.md +142 -0
  48. package/storage/documents/generated/integration/11_symboles.md +156 -0
  49. package/storage/documents/generated/integration/12_mediatheque.md +66 -0
  50. package/storage/documents/generated/integration/13_includes.md +25 -0
  51. package/storage/documents/generated/integration/14_404.md +32 -0
  52. package/storage/documents/generated/integration/15_arianne.md +62 -0
  53. package/storage/documents/generated/integration/16_fermeture.md +36 -0
  54. package/storage/documents/generated/integration/17_recherche.md +36 -0
  55. package/storage/documents/generated/integration/18_cms.md +582 -0
  56. package/storage/documents/generated/integration/19_modules.md +479 -0
  57. package/storage/documents/generated/integration/20_blog.md +946 -0
  58. package/storage/documents/generated/integration/21_catalogue.md +1834 -0
  59. package/storage/documents/generated/integration/22_commandes.md +2772 -0
  60. package/storage/documents/generated/integration/23_comptes.md +1195 -0
  61. package/storage/documents/generated/integration/24_recherche.md +542 -0
  62. package/storage/documents/hors_sujet_regles.md +43 -0
  63. package/storage/documents/kiubi_api.md +6690 -0
  64. package/storage/documents/kiubi_ruleset.md +419 -0
  65. package/storage/documents/kiubi_workflows.md +121 -0
@@ -0,0 +1,353 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import type { FieldDef } from './types.ts';
5
+
6
+ const DEFINITIONS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'storage', 'definitions');
7
+
8
+ const GLOBAL_TAGS :string[] = [
9
+ 'baseLangue',
10
+ 'racine',
11
+ 'cdn',
12
+
13
+ 'SOCIETE.societe_nom',
14
+ 'SOCIETE.societe_forme',
15
+ 'SOCIETE.societe_adresse',
16
+ 'SOCIETE.societe_cp',
17
+ 'SOCIETE.societe_ville',
18
+ 'SOCIETE.societe_tel',
19
+ 'SOCIETE.societe_mobile',
20
+ 'SOCIETE.societe_fax',
21
+ 'SOCIETE.societe_cnil',
22
+ 'SOCIETE.societe_capital',
23
+ 'SOCIETE.societe_siret',
24
+ 'SOCIETE.societe_rcs',
25
+ 'SOCIETE.societe_ape',
26
+ 'SOCIETE.societe_tva',
27
+ 'SOCIETE.societe_pays',
28
+ 'SOCIETE.contact_nom',
29
+ 'SOCIETE.contact_prenom',
30
+ 'SOCIETE.contact_email',
31
+ 'SOCIETE.contact_mail',
32
+
33
+ 'liste_domaines',
34
+ 'schema',
35
+ 'domaine',
36
+
37
+ 'num_jour_maintenant',
38
+ 'jour_semaine_maintenant',
39
+ 'num_mois_maintenant',
40
+ 'mois_maintenant',
41
+ 'mois_abrev_maintenant',
42
+ 'annee_maintenant',
43
+ 'heure_maintenant',
44
+ 'minute_maintenant',
45
+ 'seconde_maintenant',
46
+ 'timestamp_maintenant',
47
+
48
+ 'CLIENT.client_id',
49
+ 'CLIENT.num',
50
+ 'CLIENT.nom',
51
+ 'CLIENT.prenom',
52
+ 'CLIENT.genre',
53
+ 'CLIENT.email',
54
+ 'CLIENT.web',
55
+ 'CLIENT.pseudo',
56
+ 'CLIENT.avatar',
57
+ 'CLIENT.page_extranet',
58
+ 'CLIENT.groupe_extranet',
59
+ 'CLIENT.groupe_extranet_id',
60
+
61
+ 'API.actif',
62
+
63
+ 'illustration_pagecourante_g_miniature',
64
+ 'illustration_pagecourante_g_vignette',
65
+ 'illustration_pagecourante_vignette',
66
+ 'illustration_pagecourante_miniature',
67
+ 'illustration_pagecourante',
68
+ 'canonical',
69
+ 'contexte_pagecourante',
70
+ 'optim_pagecourante',
71
+ 'intitule_pagecourante',
72
+ 'lien_pagecourante',
73
+ 'lien_pageparente',
74
+ 'intitule_pageparente',
75
+
76
+ 'miniature_l',
77
+ 'miniature_h',
78
+ 'g_miniature_h',
79
+ 'g_miniature_l',
80
+ 'vignette_h',
81
+ 'vignette_l',
82
+ 'g_vignette_h',
83
+ 'g_vignette_l',
84
+
85
+ 'site_nom',
86
+ 'desc_site',
87
+ 'accroche_site',
88
+ 'devise',
89
+ 'devise_iso',
90
+ 'theme',
91
+ 'logo_site',
92
+ 'url_logo_site',
93
+ 'lg'
94
+ ];
95
+
96
+ // ─── Public types ─────────────────────────────────────────────────────────────
97
+
98
+ export type BlockValidationIssue = {
99
+ level: 'error' | 'warning';
100
+ msg: string;
101
+ line: number;
102
+ };
103
+
104
+ export type BlockDefNode = {
105
+ tags: Set<string>;
106
+ children: Map<string, BlockDefNode>;
107
+ hasWildcardChild: boolean;
108
+ };
109
+
110
+ // ─── Internal types ───────────────────────────────────────────────────────────
111
+
112
+ type RawBlockDef = {
113
+ tags: Record<string, string> | unknown[];
114
+ children: Record<string, RawBlockDef> | unknown[];
115
+ };
116
+
117
+ type RawDef = {
118
+ name: string;
119
+ basePath: string;
120
+ blocks: Record<string, RawBlockDef>;
121
+ };
122
+
123
+ type StackFrame = {
124
+ blockName: string;
125
+ lineNo: number;
126
+ defNode: BlockDefNode | null;
127
+ inheritedTags: Set<string>;
128
+ };
129
+
130
+ // ─── JSON normalisation ───────────────────────────────────────────────────────
131
+
132
+ function normalizeNode(raw: RawBlockDef): BlockDefNode {
133
+ const tags = Array.isArray(raw.tags) ? new Set<string>() : new Set(Object.keys(raw.tags as Record<string, string>));
134
+ const children = new Map<string, BlockDefNode>();
135
+ let hasWildcardChild = false;
136
+ if (!Array.isArray(raw.children)) {
137
+ for (const [k, v] of Object.entries(raw.children as Record<string, RawBlockDef>)) {
138
+ if (k === 'xxx') {
139
+ hasWildcardChild = true;
140
+ continue;
141
+ }
142
+ children.set(k, normalizeNode(v));
143
+ }
144
+ }
145
+ return { tags, children, hasWildcardChild };
146
+ }
147
+
148
+ // ─── Definition loading ───────────────────────────────────────────────────────
149
+
150
+ const defCache = new Map<string, BlockDefNode | null>();
151
+
152
+ export function loadBlockDef(jsonFilename: string, targetBasePath: string): BlockDefNode | null {
153
+ const cacheKey = `${jsonFilename}::${targetBasePath}`;
154
+ if (defCache.has(cacheKey)) {
155
+ return defCache.get(cacheKey) ?? null;
156
+ }
157
+
158
+ const jsonPath = join(DEFINITIONS_DIR, jsonFilename);
159
+ let result: BlockDefNode | null = null;
160
+
161
+ try {
162
+ if (!existsSync(jsonPath)) {
163
+ defCache.set(cacheKey, null);
164
+ return null;
165
+ }
166
+ const raw = JSON.parse(readFileSync(jsonPath, 'utf-8')) as RawDef[];
167
+ const entry = raw.find(e => e.basePath === targetBasePath);
168
+ if (entry && entry.blocks && entry.blocks['main']) {
169
+ const mainNode = normalizeNode(entry.blocks['main']);
170
+ mainNode.tags = new Set([... mainNode.tags, ...GLOBAL_TAGS]); // add al global tags to main
171
+ // Return a virtual root node that allows 'main' as the only top-level block
172
+ result = {
173
+ tags: new Set<string>(),
174
+ children: new Map([['main', mainNode]]),
175
+ hasWildcardChild: false,
176
+ };
177
+ }
178
+ } catch {
179
+ result = null;
180
+ }
181
+
182
+ defCache.set(cacheKey, result);
183
+ return result;
184
+ }
185
+
186
+ export function applyCustomFieldsDefinition(def: BlockDefNode, fields: FieldDef[]): BlockDefNode {
187
+ const mainNode = def.children.get('main');
188
+ if (!mainNode) return def;
189
+
190
+ // technicaly wrong, only valid for component. Should not be added for posts or product
191
+ const extraTags: string[] = [];
192
+ for (const f of fields) {
193
+ if (f.type === 'image') {
194
+ extraTags.push(f.field + '_vignette', f.field + '_g_vignette', f.field + '_miniature', f.field + '_g_miniature');
195
+ } else if (f.type === 'select') {
196
+ extraTags.push(f.field + '_label');
197
+ }
198
+ }
199
+ const newTags = new Set([...mainNode.tags, ...fields.map(f => f.field), ...extraTags]);
200
+ const newChildren = new Map(mainNode.children);
201
+ for (const f of fields) {
202
+ if (!newChildren.has(f.field)) {
203
+ newChildren.set(f.field, { tags: new Set(), children: new Map(), hasWildcardChild: false });
204
+ }
205
+ }
206
+ const newMainNode: BlockDefNode = { tags: newTags, children: newChildren, hasWildcardChild: mainNode.hasWildcardChild };
207
+ const newRoot = new Map(def.children);
208
+ newRoot.set('main', newMainNode);
209
+ return { tags: def.tags, children: newRoot, hasWildcardChild: def.hasWildcardChild };
210
+ }
211
+
212
+ export function applyCollectionDefinition(def: BlockDefNode, fields: FieldDef[]): BlockDefNode {
213
+ const mainNode = def.children.get('main');
214
+ if (!mainNode) return def;
215
+ const collectionNode = mainNode.children.get('collection');
216
+ if (!collectionNode) return def;
217
+ const itemNode = collectionNode.children.get('item');
218
+ if (!itemNode) return def;
219
+
220
+ const extraTags: string[] = [];
221
+ for (const f of fields) {
222
+ if (f.type === 'image') {
223
+ extraTags.push(f.field + '_vignette', f.field + '_g_vignette', f.field + '_miniature', f.field + '_g_miniature');
224
+ } else if (f.type === 'select') {
225
+ extraTags.push(f.field + '_label');
226
+ }
227
+ }
228
+ const newTags = new Set([...itemNode.tags, ...fields.map(f => f.field), ...extraTags]);
229
+ const newChildren = new Map(itemNode.children);
230
+ for (const f of fields) {
231
+ if (!newChildren.has(f.field)) {
232
+ newChildren.set(f.field, { tags: new Set(), children: new Map(), hasWildcardChild: false });
233
+ }
234
+ }
235
+ const newItemNode: BlockDefNode = { tags: newTags, children: newChildren, hasWildcardChild: itemNode.hasWildcardChild };
236
+ const newCollectionChildren = new Map(collectionNode.children);
237
+ newCollectionChildren.set('item', newItemNode);
238
+ const newCollectionNode: BlockDefNode = { tags: collectionNode.tags, children: newCollectionChildren, hasWildcardChild: collectionNode.hasWildcardChild };
239
+ const newMainChildren = new Map(mainNode.children);
240
+ newMainChildren.set('collection', newCollectionNode);
241
+ const newMainNode: BlockDefNode = { tags: mainNode.tags, children: newMainChildren, hasWildcardChild: mainNode.hasWildcardChild };
242
+ const newRoot = new Map(def.children);
243
+ newRoot.set('main', newMainNode);
244
+ return { tags: def.tags, children: newRoot, hasWildcardChild: def.hasWildcardChild };
245
+ }
246
+
247
+ // ─── Block validation ─────────────────────────────────────────────────────────
248
+
249
+ const BLOCK_RE = /<!--\s*BEGIN\s*:\s*(\w+)\s*-->|<!--\s*END\s*:\s*(\w+)\s*-->/g;
250
+ const TAG_RE = /\{([^|}]+)(?:\|[^}]*)?\}/g;
251
+ const IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
252
+
253
+ export function validateBlocks(content: string, def: BlockDefNode | null): BlockValidationIssue[] {
254
+ const issues: BlockValidationIssue[] = [];
255
+ const stack: StackFrame[] = [];
256
+ let outsideDepth = 0; // profondeur d'imbrication hors du bloc "main" de premier niveau
257
+ const lines = content.split('\n');
258
+
259
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
260
+ const line = lines[lineIdx];
261
+ const lineNo = lineIdx + 1;
262
+
263
+ // Collect all tokens on this line in order (blocks and tags)
264
+ type Token =
265
+ | { kind: 'begin'; name: string; index: number }
266
+ | { kind: 'end'; name: string; index: number }
267
+ | { kind: 'tag'; name: string; index: number };
268
+
269
+ const tokens: Token[] = [];
270
+
271
+ BLOCK_RE.lastIndex = 0;
272
+ let m: RegExpExecArray | null;
273
+ while ((m = BLOCK_RE.exec(line)) !== null) {
274
+ if (m[1]) tokens.push({ kind: 'begin', name: m[1], index: m.index });
275
+ else if (m[2]) tokens.push({ kind: 'end', name: m[2], index: m.index });
276
+ }
277
+
278
+ TAG_RE.lastIndex = 0;
279
+ while ((m = TAG_RE.exec(line)) !== null) {
280
+ const tagName = m[1];
281
+ if (!tagName.startsWith('ZONE.') && !tagName.startsWith('FILE ') && IDENTIFIER_RE.test(tagName)) {
282
+ tokens.push({ kind: 'tag', name: tagName, index: m.index });
283
+ }
284
+ }
285
+
286
+ tokens.sort((a, b) => a.index - b.index);
287
+
288
+ for (const token of tokens) {
289
+ if (token.kind === 'begin') {
290
+ // Hors de main : blocs imbriqués dans un bloc non-main → incrémenter la profondeur
291
+ if (outsideDepth > 0) { outsideDepth++; continue; }
292
+ // À la racine (hors main) : ignorer tout bloc qui n'est pas "main"
293
+ if (stack.length === 0 && token.name !== 'main') { outsideDepth++; continue; }
294
+
295
+ const blockName = token.name;
296
+ const parentDefNode = stack.length > 0 ? stack[stack.length - 1].defNode : def;
297
+ const parentInherited = stack.length > 0 ? stack[stack.length - 1].inheritedTags : new Set<string>();
298
+ const parentTags = (stack.length > 0 ? stack[stack.length - 1].defNode?.tags : def?.tags) ?? new Set<string>();
299
+
300
+ let childDefNode: BlockDefNode | null = null;
301
+
302
+ if (def !== null && parentDefNode !== null) {
303
+ if (!parentDefNode.children.has(blockName) && !parentDefNode.hasWildcardChild) {
304
+ const parentName = stack.length > 0 ? stack[stack.length - 1].blockName : 'racine';
305
+ issues.push({ level: 'error', msg: `Bloc "${blockName}" inconnu dans le contexte "${parentName}"`, line: lineNo });
306
+ } else {
307
+ childDefNode = parentDefNode.children.get(blockName) ?? null;
308
+ }
309
+ }
310
+
311
+ const inheritedTags = new Set([...parentInherited, ...parentTags]);
312
+ stack.push({ blockName, lineNo, defNode: childDefNode, inheritedTags });
313
+
314
+ } else if (token.kind === 'end') {
315
+ // Hors de main : décrémenter la profondeur
316
+ if (outsideDepth > 0) { outsideDepth--; continue; }
317
+ // END spurieux à la racine (avant ou après main) → ignorer
318
+ if (stack.length === 0) { continue; }
319
+
320
+ const blockName = token.name;
321
+ const top = stack[stack.length - 1];
322
+ if (top.blockName === blockName) {
323
+ stack.pop();
324
+ } else {
325
+ issues.push({ level: 'error', msg: `Croisement de blocs : "${blockName}" fermé avant "${top.blockName}" (ouvert ligne ${top.lineNo})`, line: lineNo });
326
+ stack.pop(); // force-close the wrong block for recovery
327
+ }
328
+
329
+ } else {
330
+ // tag : ignorer si hors de main
331
+ if (outsideDepth > 0 || stack.length === 0) { continue; }
332
+
333
+ const tagName = token.name;
334
+ if (def !== null && stack.length > 0) {
335
+ const top = stack[stack.length - 1];
336
+ if (top.defNode !== null) {
337
+ const available = new Set([...top.inheritedTags, ...top.defNode.tags]);
338
+ if (!available.has(tagName)) {
339
+ issues.push({ level: 'warning', msg: `Balise "${tagName}" non déclarée dans ce contexte`, line: lineNo });
340
+ }
341
+ }
342
+ }
343
+ }
344
+ }
345
+ }
346
+
347
+ // Report unclosed blocks
348
+ for (const frame of stack) {
349
+ issues.push({ level: 'error', msg: `Bloc "${frame.blockName}" ouvert ligne ${frame.lineNo} mais jamais fermé`, line: frame.lineNo });
350
+ }
351
+
352
+ return issues;
353
+ }