n8n-nodes-docx-filler 2.4.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.
@@ -130,6 +130,43 @@ function normalize(text) {
130
130
  .replace(/\s+/g, ' ')
131
131
  .trim();
132
132
  }
133
+ /**
134
+ * Détecte si un paragraphe est un "placeholder" (zone à remplir dans un template)
135
+ * Les templates utilisent souvent des pointillés, soulignés, ou zones vides
136
+ */
137
+ function isPlaceholderOrEmpty(text) {
138
+ const trimmed = text.trim();
139
+ // Vraiment vide ou très court
140
+ if (!trimmed || trimmed.length < 3)
141
+ return true;
142
+ // Lignes de pointillés (................)
143
+ if (/^[.\s…]+$/.test(trimmed))
144
+ return true;
145
+ if (/^\.{3,}$/.test(trimmed))
146
+ return true;
147
+ // Lignes de soulignement (_______)
148
+ if (/^[_\s]+$/.test(trimmed))
149
+ return true;
150
+ // Lignes de tirets (-------)
151
+ if (/^[-–—\s]+$/.test(trimmed))
152
+ return true;
153
+ // Juste des deux-points ou séparateurs
154
+ if (/^[:;\s]+$/.test(trimmed))
155
+ return true;
156
+ // Espaces non cassants et caractères invisibles
157
+ if (/^[\s\u00A0\u200B\u2003]+$/.test(trimmed))
158
+ return true;
159
+ // Combinaison de caractères de remplissage
160
+ if (/^[\.\-_:;\s…]+$/.test(trimmed))
161
+ return true;
162
+ // Texte très court sans valeur sémantique (1-2 chars)
163
+ if (trimmed.length <= 2)
164
+ return true;
165
+ // Numéros seuls (souvent utilisés comme index dans les formulaires)
166
+ if (/^\d{1,2}[\.\):]?$/.test(trimmed))
167
+ return true;
168
+ return false;
169
+ }
133
170
  function escapeXml(text) {
134
171
  return text
135
172
  .replace(/&/g, '&amp;')
@@ -335,6 +372,15 @@ function isFalsePositive(text) {
335
372
  return true;
336
373
  return false;
337
374
  }
375
+ /**
376
+ * Vérifie si un paragraphe contient un label suivi d'un placeholder sur la même ligne
377
+ * Exemple: "Nom commercial : ..............."
378
+ */
379
+ function hasInlinePlaceholder(text) {
380
+ // Pattern: label suivi de deux-points, puis placeholder (pointillés, soulignés, espaces)
381
+ return /:\s*[.\s_\-…]{4,}\s*$/.test(text) ||
382
+ /:\s*$/.test(text); // Label terminant par ":" sans valeur
383
+ }
338
384
  /**
339
385
  * Trouve les positions à remplir dans un template
340
386
  * IMPORTANT: Chaque type de champ n'est rempli qu'UNE SEULE FOIS (premier match)
@@ -342,27 +388,41 @@ function isFalsePositive(text) {
342
388
  function findFillPositions(paragraphs) {
343
389
  const positions = [];
344
390
  const usedFillIndices = new Set();
345
- const usedFieldTypes = new Set(); // NOUVEAU: éviter les doublons par type
391
+ const usedFieldTypes = new Set(); // éviter les doublons par type
346
392
  for (let i = 0; i < paragraphs.length; i++) {
347
393
  const p = paragraphs[i];
348
394
  // Ignorer les faux positifs (notes, titres, etc.)
349
395
  if (isFalsePositive(p.text))
350
396
  continue;
351
397
  const fieldType = detectFieldType(p.text);
352
- // NOUVEAU: Si ce type de champ a déjà été trouvé, passer au suivant
398
+ // Si ce type de champ a déjà été trouvé, passer au suivant
353
399
  if (fieldType && usedFieldTypes.has(fieldType))
354
400
  continue;
355
401
  if (fieldType) {
356
- // Chercher le prochain paragraphe vide pour y insérer la valeur
357
- for (let j = i + 1; j < Math.min(i + 6, paragraphs.length); j++) {
402
+ // CAS 1: Label avec placeholder inline (même paragraphe)
403
+ // Exemple: "Nom commercial : ..............."
404
+ if (hasInlinePlaceholder(p.text) && !usedFillIndices.has(i)) {
405
+ positions.push({
406
+ fieldType,
407
+ labelIndex: i,
408
+ labelText: p.text.slice(0, 60),
409
+ fillIndex: i, // On remplit le même paragraphe
410
+ paragraph: p,
411
+ });
412
+ usedFillIndices.add(i);
413
+ usedFieldTypes.add(fieldType);
414
+ continue;
415
+ }
416
+ // CAS 2: Chercher le prochain paragraphe vide/placeholder
417
+ for (let j = i + 1; j < Math.min(i + 8, paragraphs.length); j++) {
358
418
  if (usedFillIndices.has(j))
359
419
  continue;
360
420
  const nextP = paragraphs[j];
361
421
  // Ignorer les faux positifs comme position de remplissage
362
422
  if (isFalsePositive(nextP.text))
363
423
  continue;
364
- // Position vide = remplissable
365
- if (nextP.isEmpty) {
424
+ // Position vide OU placeholder (pointillés, soulignés, etc.) = remplissable
425
+ if (isPlaceholderOrEmpty(nextP.text)) {
366
426
  positions.push({
367
427
  fieldType,
368
428
  labelIndex: i,
@@ -371,7 +431,7 @@ function findFillPositions(paragraphs) {
371
431
  paragraph: nextP,
372
432
  });
373
433
  usedFillIndices.add(j);
374
- usedFieldTypes.add(fieldType); // NOUVEAU: marquer comme utilisé
434
+ usedFieldTypes.add(fieldType);
375
435
  break;
376
436
  }
377
437
  // Si on trouve un autre label, arrêter
@@ -405,8 +465,22 @@ function fillTemplateXml(templateXml, templateParagraphs, sourceData, fillPositi
405
465
  if (!sourceField)
406
466
  continue;
407
467
  const oldP = pos.paragraph.fullMatch;
408
- // Créer un nouveau paragraphe avec la valeur, en conservant la structure
409
- const newP = oldP.replace(/(<w:p[^>]*>)([\s\S]*?)(<\/w:p>)/, `$1<w:r><w:t>${escapeXml(sourceField.value)}</w:t></w:r>$3`);
468
+ let newP;
469
+ // CAS INLINE: Label et placeholder sur le même paragraphe
470
+ // On doit remplacer seulement la partie après ":" par la valeur
471
+ if (pos.labelIndex === pos.fillIndex && hasInlinePlaceholder(pos.paragraph.text)) {
472
+ // Trouver et remplacer le placeholder après ":"
473
+ // On garde le label et on ajoute la valeur après
474
+ newP = oldP.replace(/(<w:t[^>]*>)([^<]*:\s*)[.\s_\-…]*(<\/w:t>)/, `$1$2${escapeXml(sourceField.value)}$3`);
475
+ // Si pas de match, fallback: ajouter la valeur à la fin
476
+ if (newP === oldP) {
477
+ newP = oldP.replace(/(<\/w:r>)(\s*<\/w:p>)/, `</w:r><w:r><w:t> ${escapeXml(sourceField.value)}</w:t></w:r>$2`);
478
+ }
479
+ }
480
+ else {
481
+ // CAS NORMAL: Remplacer le paragraphe vide par la valeur
482
+ newP = oldP.replace(/(<w:p[^>]*>)([\s\S]*?)(<\/w:p>)/, `$1<w:r><w:t>${escapeXml(sourceField.value)}</w:t></w:r>$3`);
483
+ }
410
484
  modifications.push({
411
485
  start: pos.paragraph.start,
412
486
  end: pos.paragraph.end,
@@ -1117,68 +1191,14 @@ class DocxFillerAI {
1117
1191
  // Fallback au mode sans LLM
1118
1192
  sourceData = extractSourceData(sourceParagraphs);
1119
1193
  }
1120
- // Mapping avec LLM - UTILISER les résultats de l'IA
1121
- const fillPrompt = FILL_PROMPT
1122
- .replace('{source_data}', JSON.stringify(sourceData, null, 2))
1123
- .replace('{template_text}', templateStructured);
1124
- const fillResponse = await llm.invoke(fillPrompt);
1125
- let llmMappings = [];
1126
- // Parser la réponse du LLM
1127
- try {
1128
- let fillText;
1129
- if (typeof fillResponse === 'string') {
1130
- fillText = fillResponse;
1131
- }
1132
- else if (fillResponse && typeof fillResponse.content === 'string') {
1133
- fillText = fillResponse.content;
1134
- }
1135
- else if (fillResponse && typeof fillResponse.text === 'string') {
1136
- fillText = fillResponse.text;
1137
- }
1138
- else if (fillResponse && Array.isArray(fillResponse.content)) {
1139
- fillText = fillResponse.content
1140
- .filter((c) => c.type === 'text')
1141
- .map((c) => c.text)
1142
- .join('');
1143
- }
1144
- else {
1145
- fillText = JSON.stringify(fillResponse);
1146
- }
1147
- const jsonMatch = fillText.match(/\{[\s\S]*\}/);
1148
- if (jsonMatch) {
1149
- const parsed = JSON.parse(jsonMatch[0]);
1150
- llmMappings = parsed.mappings || [];
1151
- }
1152
- }
1153
- catch {
1154
- // Fallback si parsing échoue
1155
- llmMappings = [];
1156
- }
1157
- // Si l'IA a fourni des mappings valides, les utiliser
1158
- if (llmMappings.length > 0) {
1159
- // Utiliser les mappings de l'IA
1160
- const usedIndices = new Set();
1161
- for (const mapping of llmMappings) {
1162
- if (usedIndices.has(mapping.templateIndex))
1163
- continue;
1164
- const targetParagraph = templateParagraphs.find(p => p.index === mapping.templateIndex);
1165
- if (!targetParagraph)
1166
- continue;
1167
- const oldP = targetParagraph.fullMatch;
1168
- const newP = oldP.replace(/(<w:p[^>]*>)([\s\S]*?)(<\/w:p>)/, `$1<w:r><w:t>${escapeXml(mapping.value)}</w:t></w:r>$3`);
1169
- templateXml = templateXml.replace(oldP, newP);
1170
- filledFields.push(`${mapping.fieldType}: ${mapping.value}`);
1171
- usedIndices.add(mapping.templateIndex);
1172
- }
1173
- }
1174
- else {
1175
- // Fallback: utiliser la logique standard
1176
- const fillPositions = findFillPositions(templateParagraphs);
1177
- const result = fillTemplateXml(templateXml, templateParagraphs, sourceData, fillPositions);
1178
- templateXml = result.xml;
1179
- filledFields = result.filledFields;
1180
- modifiedCheckboxes = result.modifiedCheckboxes;
1181
- }
1194
+ // Utiliser la logique standard de mapping (plus fiable)
1195
+ // L'IA a été utilisée pour l'extraction des données, mais le mapping
1196
+ // utilise l'algorithme déterministe pour éviter les corruptions XML
1197
+ const fillPositions = findFillPositions(templateParagraphs);
1198
+ const result = fillTemplateXml(templateXml, templateParagraphs, sourceData, fillPositions);
1199
+ templateXml = result.xml;
1200
+ filledFields = result.filledFields;
1201
+ modifiedCheckboxes = result.modifiedCheckboxes;
1182
1202
  }
1183
1203
  else {
1184
1204
  // Mode sans LLM: extraction et mapping par patterns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-docx-filler",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "description": "n8n node to automatically fill DOCX documents (French DC1, DC2, AE forms) using AI for semantic understanding and field mapping.",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",