@appius-fr/apx 2.5.0 → 2.6.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/APX.mjs +121 -118
- package/README.md +55 -22
- package/dist/APX.dev.mjs +1436 -139
- package/dist/APX.mjs +1 -1
- package/dist/APX.prod.mjs +1 -1
- package/dist/APX.standalone.js +1383 -60
- package/dist/APX.standalone.js.map +1 -1
- package/modules/listen/README.md +235 -0
- package/modules/toast/toast.mjs +671 -20
- package/modules/tools/README.md +165 -0
- package/modules/tools/exports.mjs +16 -0
- package/modules/tools/form-packer/README.md +315 -0
- package/modules/tools/form-packer/augment-apx.mjs +30 -0
- package/modules/tools/form-packer/packToJson.mjs +549 -0
- package/package.json +1 -1
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convertit un formulaire HTML en objet JSON
|
|
3
|
+
* @param {HTMLFormElement} form - Le formulaire à convertir
|
|
4
|
+
* @returns {Object} L'objet JSON résultant
|
|
5
|
+
* @throws {TypeError} Si form n'est pas un HTMLFormElement
|
|
6
|
+
*/
|
|
7
|
+
export const packFormToJSON = (form) => {
|
|
8
|
+
// Validation de l'entrée
|
|
9
|
+
if (!form || !(form instanceof HTMLFormElement)) {
|
|
10
|
+
throw new TypeError('packFormToJSON expects an HTMLFormElement');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const formData = new FormData(form);
|
|
14
|
+
const jsonData = {};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Construit une représentation string du chemin pour les messages d'erreur
|
|
18
|
+
* @param {Array} parts - Les parties du chemin
|
|
19
|
+
* @returns {string} Le chemin formaté
|
|
20
|
+
*/
|
|
21
|
+
const buildPathString = (parts) => {
|
|
22
|
+
return parts.reduce((path, part) => {
|
|
23
|
+
if (part.type === 'key') {
|
|
24
|
+
return path ? `${path}[${part.name}]` : part.name;
|
|
25
|
+
}
|
|
26
|
+
if (part.type === 'numeric') {
|
|
27
|
+
return `${path}[${part.index}]`;
|
|
28
|
+
}
|
|
29
|
+
if (part.type === 'array') {
|
|
30
|
+
return `${path}[]`;
|
|
31
|
+
}
|
|
32
|
+
return path;
|
|
33
|
+
}, '');
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// PHASE 1 : ANALYSE ET DÉTECTION DES CONFLITS
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Structure pour stocker l'utilisation d'un chemin
|
|
42
|
+
* @typedef {Object} PathUsage
|
|
43
|
+
* @property {boolean} isFinal - Le chemin est utilisé comme valeur finale
|
|
44
|
+
* @property {boolean} isIntermediate - Le chemin est utilisé comme chemin intermédiaire
|
|
45
|
+
* @property {boolean} hasArraySuffix - Le chemin se termine par [] (crée des valeurs primitives)
|
|
46
|
+
* @property {string} key - La clé du formulaire qui utilise ce chemin
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
const pathUsage = new Map(); // Map<pathString, PathUsage>
|
|
50
|
+
const keyAnalysis = new Map(); // Map<basePath, {hasNumeric: boolean, hasString: boolean}>
|
|
51
|
+
const allEntries = [];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Enregistre l'utilisation d'un chemin dans pathUsage
|
|
55
|
+
* @param {string} currentPath - Le chemin à enregistrer
|
|
56
|
+
* @param {boolean} isFinal - Si c'est une valeur finale
|
|
57
|
+
* @param {boolean} isArraySuffix - Si le chemin se termine par []
|
|
58
|
+
* @param {string} key - La clé du formulaire
|
|
59
|
+
*/
|
|
60
|
+
const recordPathUsage = (currentPath, isFinal, isArraySuffix, key) => {
|
|
61
|
+
const usage = pathUsage.get(currentPath) ?? {
|
|
62
|
+
isFinal: false,
|
|
63
|
+
isIntermediate: false,
|
|
64
|
+
hasArraySuffix: false,
|
|
65
|
+
key
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (!pathUsage.has(currentPath)) {
|
|
69
|
+
pathUsage.set(currentPath, usage);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (isFinal) {
|
|
73
|
+
usage.isFinal = true;
|
|
74
|
+
usage.key = key;
|
|
75
|
+
if (isArraySuffix) {
|
|
76
|
+
usage.hasArraySuffix = true;
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
usage.isIntermediate = true;
|
|
80
|
+
usage.key ??= key;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Première passe : analyser toutes les clés et enregistrer les chemins
|
|
85
|
+
for (const [key, value] of formData.entries()) {
|
|
86
|
+
if (!key) continue;
|
|
87
|
+
|
|
88
|
+
allEntries.push({ key, value });
|
|
89
|
+
const parts = parseKey(key);
|
|
90
|
+
|
|
91
|
+
// Construire tous les chemins intermédiaires et le chemin final
|
|
92
|
+
let currentPath = '';
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < parts.length; i++) {
|
|
95
|
+
const part = parts[i];
|
|
96
|
+
const isLast = i === parts.length - 1;
|
|
97
|
+
const isArraySuffix = part.type === 'array';
|
|
98
|
+
|
|
99
|
+
// Construire le chemin jusqu'à ce niveau
|
|
100
|
+
if (part.type === 'key') {
|
|
101
|
+
currentPath = currentPath ? `${currentPath}[${part.name}]` : part.name;
|
|
102
|
+
} else if (part.type === 'numeric') {
|
|
103
|
+
currentPath = `${currentPath}[${part.index}]`;
|
|
104
|
+
} else if (part.type === 'array') {
|
|
105
|
+
currentPath = `${currentPath}[]`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Enregistrer l'utilisation du chemin
|
|
109
|
+
recordPathUsage(currentPath, isLast, isArraySuffix && isLast, key);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Analyser chaque niveau de la hiérarchie pour détecter les conflits (indices numériques vs clés de chaîne)
|
|
113
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
114
|
+
const part = parts[i];
|
|
115
|
+
const nextPart = parts[i + 1];
|
|
116
|
+
|
|
117
|
+
// Analyser seulement les clés (pas les indices numériques)
|
|
118
|
+
if (part.type === 'key') {
|
|
119
|
+
const basePath = part.name; // Le nom de la première clé dans le chemin
|
|
120
|
+
|
|
121
|
+
const analysis = keyAnalysis.get(basePath) ?? { hasNumeric: false, hasString: false };
|
|
122
|
+
if (!keyAnalysis.has(basePath)) {
|
|
123
|
+
keyAnalysis.set(basePath, analysis);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (nextPart) {
|
|
127
|
+
if (nextPart.type === 'numeric') {
|
|
128
|
+
analysis.hasNumeric = true;
|
|
129
|
+
} else if (nextPart.type === 'key' || nextPart.type === 'array') {
|
|
130
|
+
analysis.hasString = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Détecte et lève une exception pour tous les conflits de chemins
|
|
139
|
+
*
|
|
140
|
+
* Principe simple : pour chaque chemin final (valeur primitive), vérifier si un chemin
|
|
141
|
+
* intermédiaire commence par ce chemin suivi de '['. Si c'est le cas, c'est un conflit.
|
|
142
|
+
*
|
|
143
|
+
* Cette approche unifiée couvre tous les cas :
|
|
144
|
+
* - items[] (final) vs items[0][name] (intermédiaire)
|
|
145
|
+
* - array[3] (final) vs array[3][nested] (intermédiaire)
|
|
146
|
+
* - data[key] (final) vs data[key][sub] (intermédiaire)
|
|
147
|
+
*/
|
|
148
|
+
const detectPathConflicts = () => {
|
|
149
|
+
// 1. Collecter tous les chemins finaux (valeurs primitives)
|
|
150
|
+
const finalPaths = new Set();
|
|
151
|
+
const finalPathKeys = new Map(); // Pour les messages d'erreur
|
|
152
|
+
|
|
153
|
+
for (const [path, usage] of pathUsage.entries()) {
|
|
154
|
+
if (usage.isFinal) {
|
|
155
|
+
finalPaths.add(path);
|
|
156
|
+
finalPathKeys.set(path, usage.key);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 2. Pour chaque chemin intermédiaire, vérifier s'il commence par un chemin final
|
|
161
|
+
for (const [path, usage] of pathUsage.entries()) {
|
|
162
|
+
if (usage.isIntermediate) {
|
|
163
|
+
for (const finalPath of finalPaths) {
|
|
164
|
+
// Vérifier si ce chemin intermédiaire commence par un chemin final suivi de '['
|
|
165
|
+
// Exemple: finalPath = "array[3]", path = "array[3][nested]" → conflit
|
|
166
|
+
if (path.startsWith(`${finalPath}[`)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Path conflict: "${finalPath}" is used as a final value (in field "${finalPathKeys.get(finalPath)}"), ` +
|
|
169
|
+
`but "${path}" tries to use it as an intermediate path (in field "${usage.key}"). ` +
|
|
170
|
+
`This creates incompatible data structures. ` +
|
|
171
|
+
`You cannot use "${finalPath}" as a primitive value and then access it as an object.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Exécuter la détection des conflits
|
|
180
|
+
detectPathConflicts();
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Vérifie si un chemin de base a un conflit (indices numériques ET clés de chaîne)
|
|
184
|
+
* @param {string} basePath - Le chemin de base à vérifier
|
|
185
|
+
* @returns {boolean}
|
|
186
|
+
*/
|
|
187
|
+
const hasConflict = (basePath) => {
|
|
188
|
+
if (!basePath) return false;
|
|
189
|
+
const analysis = keyAnalysis.get(basePath);
|
|
190
|
+
return analysis?.hasNumeric && analysis?.hasString;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Convertit un tableau en objet en préservant les indices numériques comme propriétés
|
|
195
|
+
* @param {Array} arr - Le tableau à convertir
|
|
196
|
+
* @returns {Object} L'objet résultant
|
|
197
|
+
*/
|
|
198
|
+
const arrayToObject = (arr) => {
|
|
199
|
+
const obj = {};
|
|
200
|
+
// Copier les indices numériques
|
|
201
|
+
for (let idx = 0; idx < arr.length; idx++) {
|
|
202
|
+
obj[idx] = arr[idx];
|
|
203
|
+
}
|
|
204
|
+
// Copier les propriétés non-numériques
|
|
205
|
+
for (const [k, v] of Object.entries(arr)) {
|
|
206
|
+
const numKey = Number.parseInt(k, 10);
|
|
207
|
+
if (Number.isNaN(numKey) || k !== String(numKey)) {
|
|
208
|
+
obj[k] = v;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return obj;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Convertit un objet en tableau en préservant les indices numériques
|
|
216
|
+
* @param {Object} obj - L'objet à convertir
|
|
217
|
+
* @returns {Array} Le tableau résultant
|
|
218
|
+
*/
|
|
219
|
+
const objectToArray = (obj) => {
|
|
220
|
+
if (Array.isArray(obj)) return obj;
|
|
221
|
+
const arr = [];
|
|
222
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
223
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
224
|
+
const numKey = Number.parseInt(k, 10);
|
|
225
|
+
if (!Number.isNaN(numKey)) {
|
|
226
|
+
arr[numKey] = v;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return arr;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Assure qu'un conteneur est un objet, en convertissant si nécessaire
|
|
235
|
+
* @param {*} container - Le conteneur à vérifier
|
|
236
|
+
* @param {string|null} key - La clé dans le conteneur parent
|
|
237
|
+
* @param {Object|null} parent - Le conteneur parent
|
|
238
|
+
* @param {boolean} forceObject - Forcer la conversion en objet même s'il n'y a pas de conflit
|
|
239
|
+
* @returns {Object} Le conteneur (converti en objet si nécessaire)
|
|
240
|
+
*/
|
|
241
|
+
const ensureObject = (container, key, parent, forceObject = false) => {
|
|
242
|
+
if (Array.isArray(container)) {
|
|
243
|
+
const obj = arrayToObject(container);
|
|
244
|
+
if (parent && key !== null) {
|
|
245
|
+
parent[key] = obj;
|
|
246
|
+
}
|
|
247
|
+
return obj;
|
|
248
|
+
}
|
|
249
|
+
if (typeof container !== 'object' || container === null) {
|
|
250
|
+
const obj = {};
|
|
251
|
+
if (parent && key !== null) {
|
|
252
|
+
parent[key] = obj;
|
|
253
|
+
}
|
|
254
|
+
return obj;
|
|
255
|
+
}
|
|
256
|
+
return container;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Assure qu'un conteneur est un tableau, en convertissant si nécessaire
|
|
261
|
+
* @param {*} container - Le conteneur à vérifier
|
|
262
|
+
* @param {string|null} key - La clé dans le conteneur parent
|
|
263
|
+
* @param {Object|null} parent - Le conteneur parent
|
|
264
|
+
* @returns {Array} Le conteneur (converti en tableau si nécessaire)
|
|
265
|
+
*/
|
|
266
|
+
const ensureArray = (container, key, parent) => {
|
|
267
|
+
if (!Array.isArray(container)) {
|
|
268
|
+
const arr = objectToArray(container);
|
|
269
|
+
if (parent && key !== null) {
|
|
270
|
+
parent[key] = arr;
|
|
271
|
+
}
|
|
272
|
+
return arr;
|
|
273
|
+
}
|
|
274
|
+
return container;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// ============================================================================
|
|
278
|
+
// PHASE 2 : TRAITEMENT DES VALEURS
|
|
279
|
+
// ============================================================================
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Traite une valeur finale (dernière partie du chemin)
|
|
283
|
+
* @param {*} container - Le conteneur actuel
|
|
284
|
+
* @param {Object} part - La partie à traiter
|
|
285
|
+
* @param {*} value - La valeur à assigner
|
|
286
|
+
* @param {Object} parent - Le conteneur parent
|
|
287
|
+
* @param {string} basePath - Le chemin de base pour la détection de conflit
|
|
288
|
+
*/
|
|
289
|
+
const processFinalValue = (container, part, value, parent, basePath) => {
|
|
290
|
+
if (part.type === 'array') {
|
|
291
|
+
// Tableau explicite avec []
|
|
292
|
+
container[part.name] ??= [];
|
|
293
|
+
if (Array.isArray(container[part.name])) {
|
|
294
|
+
container[part.name].push(value);
|
|
295
|
+
} else {
|
|
296
|
+
container[part.name] = [container[part.name], value];
|
|
297
|
+
}
|
|
298
|
+
} else if (part.type === 'numeric') {
|
|
299
|
+
// Indice numérique final
|
|
300
|
+
const { index } = part;
|
|
301
|
+
container = ensureArray(container, parent.key, parent.container);
|
|
302
|
+
while (container.length <= index) {
|
|
303
|
+
container.push(undefined);
|
|
304
|
+
}
|
|
305
|
+
container[index] = value;
|
|
306
|
+
} else {
|
|
307
|
+
// Clé simple finale
|
|
308
|
+
const conflict = hasConflict(basePath);
|
|
309
|
+
|
|
310
|
+
if (conflict) {
|
|
311
|
+
container = ensureObject(container, parent.key, parent.container, true);
|
|
312
|
+
if (Array.isArray(container[part.name])) {
|
|
313
|
+
container[part.name] = arrayToObject(container[part.name]);
|
|
314
|
+
}
|
|
315
|
+
if (container[part.name] !== undefined &&
|
|
316
|
+
(typeof container[part.name] !== 'object' || container[part.name] === null)) {
|
|
317
|
+
container[part.name] = value;
|
|
318
|
+
} else if (container[part.name] === undefined) {
|
|
319
|
+
container[part.name] = value;
|
|
320
|
+
} else if (Array.isArray(container[part.name])) {
|
|
321
|
+
container[part.name].push(value);
|
|
322
|
+
} else {
|
|
323
|
+
container[part.name] = [container[part.name], value];
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
if (container[part.name] === undefined) {
|
|
327
|
+
container[part.name] = value;
|
|
328
|
+
} else if (Array.isArray(container[part.name])) {
|
|
329
|
+
container[part.name].push(value);
|
|
330
|
+
} else {
|
|
331
|
+
container[part.name] = [container[part.name], value];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Traite une partie intermédiaire (crée la structure)
|
|
339
|
+
* @param {*} container - Le conteneur actuel
|
|
340
|
+
* @param {Object} part - La partie à traiter
|
|
341
|
+
* @param {Object|null} nextPart - La partie suivante (peut être null)
|
|
342
|
+
* @param {Object} parent - Le conteneur parent
|
|
343
|
+
* @param {string} basePath - Le chemin de base pour la détection de conflit
|
|
344
|
+
* @param {Array} parts - Toutes les parties du chemin (pour construire le chemin complet dans les erreurs)
|
|
345
|
+
* @param {number} i - L'index de la partie actuelle (pour construire le chemin complet dans les erreurs)
|
|
346
|
+
* @param {string} key - La clé du formulaire (pour les messages d'erreur)
|
|
347
|
+
* @returns {*} Le nouveau conteneur après traitement
|
|
348
|
+
*/
|
|
349
|
+
const processIntermediatePart = (container, part, nextPart, parent, basePath, parts, i, key) => {
|
|
350
|
+
if (part.type === 'numeric') {
|
|
351
|
+
// Indice numérique : le container doit être un tableau ou un objet (selon conflit)
|
|
352
|
+
const { index } = part;
|
|
353
|
+
const conflict = hasConflict(basePath);
|
|
354
|
+
|
|
355
|
+
if (conflict) {
|
|
356
|
+
// Conflit : utiliser un objet (les indices seront des propriétés)
|
|
357
|
+
container = ensureObject(container, parent.key, parent.container, true);
|
|
358
|
+
container[index] ??= {};
|
|
359
|
+
if (typeof container[index] !== 'object' || container[index] === null) {
|
|
360
|
+
// Cette erreur ne devrait jamais se produire si la détection fonctionne correctement
|
|
361
|
+
const pathParts = parts.slice(0, i + 1);
|
|
362
|
+
const currentPath = buildPathString(pathParts);
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Cannot access property on primitive value. ` +
|
|
365
|
+
`Key "${key}" tries to access "${currentPath}" but it is already a ${typeof container[index]} value: ${JSON.stringify(container[index])}. ` +
|
|
366
|
+
`This should have been detected during conflict detection phase.`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
// Pas de conflit : utiliser un tableau
|
|
371
|
+
container = ensureArray(container, parent.key, parent.container);
|
|
372
|
+
while (container.length <= index) {
|
|
373
|
+
container.push(undefined);
|
|
374
|
+
}
|
|
375
|
+
container[index] ??= {};
|
|
376
|
+
if (typeof container[index] !== 'object' || container[index] === null) {
|
|
377
|
+
// Cette erreur ne devrait jamais se produire si la détection fonctionne correctement
|
|
378
|
+
const pathParts = parts.slice(0, i + 1);
|
|
379
|
+
const currentPath = buildPathString(pathParts);
|
|
380
|
+
throw new Error(
|
|
381
|
+
`Cannot access property on primitive value. ` +
|
|
382
|
+
`Key "${key}" tries to access "${currentPath}" but it is already a ${typeof container[index]} value: ${JSON.stringify(container[index])}. ` +
|
|
383
|
+
`This should have been detected during conflict detection phase.`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return container[index];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Clé normale : créer objet ou tableau selon la partie suivante
|
|
391
|
+
const nextIsNumeric = nextPart?.type === 'numeric';
|
|
392
|
+
const conflict = hasConflict(basePath);
|
|
393
|
+
|
|
394
|
+
if (conflict) {
|
|
395
|
+
// Conflit : toujours utiliser un objet
|
|
396
|
+
container = ensureObject(container, parent.key, parent.container, true);
|
|
397
|
+
container[part.name] ??= {};
|
|
398
|
+
if (Array.isArray(container[part.name])) {
|
|
399
|
+
container[part.name] = arrayToObject(container[part.name]);
|
|
400
|
+
} else if (typeof container[part.name] !== 'object' || container[part.name] === null) {
|
|
401
|
+
// Cette erreur ne devrait jamais se produire si la détection fonctionne correctement
|
|
402
|
+
const pathParts = parts.slice(0, i + 1);
|
|
403
|
+
const currentPath = buildPathString(pathParts);
|
|
404
|
+
throw new Error(
|
|
405
|
+
`Cannot access property on primitive value. ` +
|
|
406
|
+
`Key "${key}" tries to access "${currentPath}" but it is already a ${typeof container[part.name]} value: ${JSON.stringify(container[part.name])}. ` +
|
|
407
|
+
`This should have been detected during conflict detection phase.`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
} else if (container[part.name] === undefined) {
|
|
411
|
+
// Pas de conflit, créer selon la partie suivante
|
|
412
|
+
container[part.name] = nextIsNumeric ? [] : {};
|
|
413
|
+
} else if (nextIsNumeric && !Array.isArray(container[part.name])) {
|
|
414
|
+
// On a besoin d'un tableau mais c'est un objet
|
|
415
|
+
const hasStringKeys = Object.keys(container[part.name]).some(k => {
|
|
416
|
+
const numKey = Number.parseInt(k, 10);
|
|
417
|
+
return Number.isNaN(numKey) || k !== String(numKey);
|
|
418
|
+
});
|
|
419
|
+
if (!hasStringKeys) {
|
|
420
|
+
container[part.name] = objectToArray(container[part.name]);
|
|
421
|
+
}
|
|
422
|
+
} else if (!nextIsNumeric && !isPlainObject(container[part.name])) {
|
|
423
|
+
// On a besoin d'un objet mais c'est un tableau
|
|
424
|
+
if (Array.isArray(container[part.name])) {
|
|
425
|
+
container[part.name] = arrayToObject(container[part.name]);
|
|
426
|
+
} else {
|
|
427
|
+
container[part.name] = {};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// S'assurer que container[part.name] est bien un objet après toutes les conversions
|
|
432
|
+
if (container[part.name] !== undefined &&
|
|
433
|
+
(typeof container[part.name] !== 'object' || container[part.name] === null)) {
|
|
434
|
+
// Cette erreur ne devrait jamais se produire si la détection fonctionne correctement
|
|
435
|
+
const pathParts = parts.slice(0, i + 1);
|
|
436
|
+
const currentPath = buildPathString(pathParts);
|
|
437
|
+
throw new Error(
|
|
438
|
+
`Cannot access property on primitive value. ` +
|
|
439
|
+
`Key "${key}" tries to access "${currentPath}" but it is already a ${typeof container[part.name]} value: ${JSON.stringify(container[part.name])}. ` +
|
|
440
|
+
`This should have been detected during conflict detection phase.`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
return container[part.name];
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Traiter toutes les entrées
|
|
447
|
+
for (const { value, key } of allEntries) {
|
|
448
|
+
if (!key) continue;
|
|
449
|
+
|
|
450
|
+
const parts = parseKey(key);
|
|
451
|
+
const path = [{ container: jsonData, key: null }];
|
|
452
|
+
let container = jsonData;
|
|
453
|
+
const basePath = parts[0]?.type === 'key' ? parts[0].name : '';
|
|
454
|
+
|
|
455
|
+
for (let i = 0; i < parts.length; i++) {
|
|
456
|
+
const part = parts[i];
|
|
457
|
+
const isLast = i === parts.length - 1;
|
|
458
|
+
const nextPart = i + 1 < parts.length ? parts[i + 1] : null;
|
|
459
|
+
const parent = path[path.length - 1];
|
|
460
|
+
|
|
461
|
+
if (isLast) {
|
|
462
|
+
// Dernière partie : assigner la valeur
|
|
463
|
+
processFinalValue(container, part, value, parent, basePath);
|
|
464
|
+
} else {
|
|
465
|
+
// Partie intermédiaire : créer la structure
|
|
466
|
+
const newContainer = processIntermediatePart(container, part, nextPart, parent, basePath, parts, i, key);
|
|
467
|
+
path.push({ container, key: part.type === 'numeric' ? part.index : part.name });
|
|
468
|
+
container = newContainer;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return jsonData;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Parse une clé de formulaire pour extraire les parties (nom, indices, etc.)
|
|
478
|
+
* @param {string} key - La clé à parser (ex: "user[email]", "tags[]", "items[0][name]")
|
|
479
|
+
* @returns {Array<{name: string, type: string, index?: number}>} Tableau des parties parsées
|
|
480
|
+
* @private
|
|
481
|
+
*/
|
|
482
|
+
const parseKey = (key) => {
|
|
483
|
+
const parts = [];
|
|
484
|
+
let current = '';
|
|
485
|
+
let i = 0;
|
|
486
|
+
const len = key.length;
|
|
487
|
+
|
|
488
|
+
while (i < len) {
|
|
489
|
+
const char = key[i];
|
|
490
|
+
|
|
491
|
+
if (char === '[') {
|
|
492
|
+
// Sauvegarder la partie précédente si elle existe
|
|
493
|
+
if (current) {
|
|
494
|
+
parts.push({ name: current, type: 'key' });
|
|
495
|
+
current = '';
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Trouver la fin du crochet
|
|
499
|
+
i++;
|
|
500
|
+
let bracketContent = '';
|
|
501
|
+
while (i < len && key[i] !== ']') {
|
|
502
|
+
bracketContent += key[i];
|
|
503
|
+
i++;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (bracketContent === '') {
|
|
507
|
+
// [] vide = tableau
|
|
508
|
+
const lastPart = parts[parts.length - 1];
|
|
509
|
+
if (lastPart) {
|
|
510
|
+
lastPart.type = 'array';
|
|
511
|
+
} else {
|
|
512
|
+
// [] au début, créer une partie spéciale
|
|
513
|
+
parts.push({ name: '', type: 'array' });
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
// Contenu dans les crochets
|
|
517
|
+
const numIndex = Number.parseInt(bracketContent, 10);
|
|
518
|
+
if (!Number.isNaN(numIndex) && bracketContent === String(numIndex)) {
|
|
519
|
+
// Indice numérique
|
|
520
|
+
parts.push({ name: String(numIndex), type: 'numeric', index: numIndex });
|
|
521
|
+
} else {
|
|
522
|
+
// Nom de propriété
|
|
523
|
+
parts.push({ name: bracketContent, type: 'key' });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
i++; // Passer le ']'
|
|
527
|
+
} else {
|
|
528
|
+
current += char;
|
|
529
|
+
i++;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Ajouter la dernière partie si elle existe
|
|
534
|
+
if (current) {
|
|
535
|
+
parts.push({ name: current, type: 'key' });
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return parts;
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Vérifie si une valeur est un objet simple (pas un tableau, pas null)
|
|
543
|
+
* @param {*} obj - La valeur à vérifier
|
|
544
|
+
* @returns {boolean}
|
|
545
|
+
* @private
|
|
546
|
+
*/
|
|
547
|
+
const isPlainObject = (obj) => {
|
|
548
|
+
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
|
|
549
|
+
};
|