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, '&')
|
|
@@ -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(); //
|
|
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
|
-
//
|
|
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
|
-
//
|
|
357
|
-
|
|
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.
|
|
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);
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
//
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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.
|
|
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",
|