@bhanquier/template-render 0.1.2
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/dist/index.cjs +298 -0
- package/dist/index.js +258 -0
- package/package.json +45 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
StubPdfmeFormEngine: () => StubPdfmeFormEngine,
|
|
34
|
+
buildByotContext: () => buildByotContext,
|
|
35
|
+
buildPreviewContext: () => buildPreviewContext,
|
|
36
|
+
renderDocxWithFallback: () => renderDocxWithFallback,
|
|
37
|
+
renderDocxWithSyntax: () => renderDocxWithSyntax,
|
|
38
|
+
renderMergeFieldsDocx: () => renderMergeFieldsDocx
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
async function renderDocxWithSyntax(sourceBuffer, context, syntax) {
|
|
42
|
+
const [{ default: PizZip }, { default: Docxtemplater }] = await Promise.all([
|
|
43
|
+
import("pizzip"),
|
|
44
|
+
import("docxtemplater")
|
|
45
|
+
]);
|
|
46
|
+
const zip = new PizZip(Buffer.from(sourceBuffer).toString("binary"));
|
|
47
|
+
const options = {
|
|
48
|
+
paragraphLoop: true,
|
|
49
|
+
linebreaks: true
|
|
50
|
+
};
|
|
51
|
+
if (syntax === "bracket") {
|
|
52
|
+
options.delimiters = { start: "[", end: "]" };
|
|
53
|
+
} else if (syntax === "guillemets") {
|
|
54
|
+
options.delimiters = { start: "\xAB", end: "\xBB" };
|
|
55
|
+
}
|
|
56
|
+
const doc = new Docxtemplater(zip, options);
|
|
57
|
+
doc.render(context);
|
|
58
|
+
return Buffer.from(doc.getZip().generate({ type: "nodebuffer", compression: "DEFLATE" }));
|
|
59
|
+
}
|
|
60
|
+
async function renderDocxWithFallback(sourceBuffer, context, preferredSyntax) {
|
|
61
|
+
const attempts = preferredSyntax === "bracket" ? ["bracket", "curly", "guillemets"] : ["curly", "bracket", "guillemets"];
|
|
62
|
+
let lastError = null;
|
|
63
|
+
for (const syntax of attempts) {
|
|
64
|
+
try {
|
|
65
|
+
return await renderDocxWithSyntax(sourceBuffer, context, syntax);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Unable to render DOCX with fallback syntaxes (${lastError?.message ?? "unknown error"})`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
async function renderMergeFieldsDocx(sourceBuffer, context) {
|
|
75
|
+
const { default: PizZip } = await import("pizzip");
|
|
76
|
+
const zip = new PizZip(Buffer.from(sourceBuffer).toString("binary"));
|
|
77
|
+
const filesToPatch = [
|
|
78
|
+
"word/document.xml",
|
|
79
|
+
"word/header1.xml",
|
|
80
|
+
"word/header2.xml",
|
|
81
|
+
"word/header3.xml",
|
|
82
|
+
"word/footer1.xml",
|
|
83
|
+
"word/footer2.xml",
|
|
84
|
+
"word/footer3.xml",
|
|
85
|
+
"word/footnotes.xml",
|
|
86
|
+
"word/endnotes.xml"
|
|
87
|
+
];
|
|
88
|
+
for (const fileName of filesToPatch) {
|
|
89
|
+
const file = zip.file(fileName);
|
|
90
|
+
if (!file) continue;
|
|
91
|
+
const xml = file.asText();
|
|
92
|
+
zip.file(fileName, replaceMergeFieldsInXml(xml, context));
|
|
93
|
+
}
|
|
94
|
+
const settingsFile = zip.file("word/settings.xml");
|
|
95
|
+
if (settingsFile) {
|
|
96
|
+
let settingsXml = settingsFile.asText();
|
|
97
|
+
settingsXml = settingsXml.replace(/<w:mailMerge[\s\S]*?<\/w:mailMerge>/g, "");
|
|
98
|
+
settingsXml = settingsXml.replace(/<w:mailMerge\s*\/>/g, "");
|
|
99
|
+
settingsXml = settingsXml.replace(/<w:attachedTemplate[\s\S]*?\/>/g, "");
|
|
100
|
+
zip.file("word/settings.xml", settingsXml);
|
|
101
|
+
}
|
|
102
|
+
sanitizeAllFieldCodes(zip);
|
|
103
|
+
return Buffer.from(zip.generate({ type: "nodebuffer", compression: "DEFLATE" }));
|
|
104
|
+
}
|
|
105
|
+
function buildByotContext(mappings, templateData) {
|
|
106
|
+
const context = {};
|
|
107
|
+
let mappedCount = 0;
|
|
108
|
+
let unresolvedCount = 0;
|
|
109
|
+
let curly = 0;
|
|
110
|
+
let bracket = 0;
|
|
111
|
+
let mergefield = 0;
|
|
112
|
+
for (const row of mappings) {
|
|
113
|
+
if (row.status === "ignored") continue;
|
|
114
|
+
if (row.syntax === "curly") curly += 1;
|
|
115
|
+
if (row.syntax === "bracket") bracket += 1;
|
|
116
|
+
if (row.syntax === "mergefield") mergefield += 1;
|
|
117
|
+
const targetField = row.mappedField ?? row.suggestedField;
|
|
118
|
+
if (!targetField) {
|
|
119
|
+
unresolvedCount += 1;
|
|
120
|
+
context[row.rawToken] = "";
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const value = templateData[targetField];
|
|
124
|
+
if (value === void 0 || value === null || value === "") {
|
|
125
|
+
unresolvedCount += 1;
|
|
126
|
+
context[row.rawToken] = "";
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
mappedCount += 1;
|
|
130
|
+
context[row.rawToken] = String(value);
|
|
131
|
+
}
|
|
132
|
+
const preferredSyntax = mergefield > curly && mergefield > bracket ? "mergefield" : bracket > curly ? "bracket" : "curly";
|
|
133
|
+
return { context, mappedCount, unresolvedCount, preferredSyntax };
|
|
134
|
+
}
|
|
135
|
+
function buildPreviewContext(mappings, resolveExample, resolveRequirement) {
|
|
136
|
+
const context = {};
|
|
137
|
+
let mappedCount = 0;
|
|
138
|
+
let unresolvedCount = 0;
|
|
139
|
+
let curly = 0;
|
|
140
|
+
let bracket = 0;
|
|
141
|
+
let mergefield = 0;
|
|
142
|
+
const unresolvedBreakdown = { required: 0, recommended: 0, optional: 0, unknown: 0 };
|
|
143
|
+
const toMissingMarker = (label, tokenOrField) => `[[${label}:${tokenOrField}]]`;
|
|
144
|
+
const markUnresolved = (requirement, tokenOrField) => {
|
|
145
|
+
unresolvedCount += 1;
|
|
146
|
+
if (requirement === "required") {
|
|
147
|
+
unresolvedBreakdown.required += 1;
|
|
148
|
+
return toMissingMarker("MANQUANT_OBLIGATOIRE", tokenOrField);
|
|
149
|
+
}
|
|
150
|
+
if (requirement === "recommended") {
|
|
151
|
+
unresolvedBreakdown.recommended += 1;
|
|
152
|
+
return toMissingMarker("MANQUANT_RECOMMANDE", tokenOrField);
|
|
153
|
+
}
|
|
154
|
+
if (requirement === "optional") {
|
|
155
|
+
unresolvedBreakdown.optional += 1;
|
|
156
|
+
return toMissingMarker("MANQUANT_OPTIONNEL", tokenOrField);
|
|
157
|
+
}
|
|
158
|
+
unresolvedBreakdown.unknown += 1;
|
|
159
|
+
return toMissingMarker("A_MAPPER", tokenOrField);
|
|
160
|
+
};
|
|
161
|
+
for (const row of mappings) {
|
|
162
|
+
if (row.status === "ignored") continue;
|
|
163
|
+
if (row.syntax === "curly") curly += 1;
|
|
164
|
+
if (row.syntax === "bracket") bracket += 1;
|
|
165
|
+
if (row.syntax === "mergefield") mergefield += 1;
|
|
166
|
+
const targetField = row.mappedField ?? row.suggestedField;
|
|
167
|
+
if (!targetField) {
|
|
168
|
+
context[row.rawToken] = markUnresolved(null, row.rawToken);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const example = resolveExample(targetField);
|
|
172
|
+
if (example === null || example === "") {
|
|
173
|
+
const requirement = resolveRequirement ? resolveRequirement(targetField) : null;
|
|
174
|
+
context[row.rawToken] = markUnresolved(requirement, targetField);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
mappedCount += 1;
|
|
178
|
+
context[row.rawToken] = example;
|
|
179
|
+
}
|
|
180
|
+
const preferredSyntax = mergefield > curly && mergefield > bracket ? "mergefield" : bracket > curly ? "bracket" : "curly";
|
|
181
|
+
return {
|
|
182
|
+
context,
|
|
183
|
+
mappedCount,
|
|
184
|
+
unresolvedCount,
|
|
185
|
+
unresolvedBreakdown,
|
|
186
|
+
preferredSyntax
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
var StubPdfmeFormEngine = class {
|
|
190
|
+
name = "pdfme-stub";
|
|
191
|
+
async renderPdfForm(_template, _inputs) {
|
|
192
|
+
return new Uint8Array();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
function replaceMergeFieldsInXml(xml, context) {
|
|
196
|
+
const instrTextRe = /MERGEFIELD\s+([\w\u00C0-\u024FŒœ]+)/;
|
|
197
|
+
const pRegex = /<w:p[\s>][\s\S]*?<\/w:p>/g;
|
|
198
|
+
let result = "";
|
|
199
|
+
let lastIndex = 0;
|
|
200
|
+
let pMatch;
|
|
201
|
+
while ((pMatch = pRegex.exec(xml)) !== null) {
|
|
202
|
+
result += xml.slice(lastIndex, pMatch.index);
|
|
203
|
+
let pXml = pMatch[0];
|
|
204
|
+
let hadMergeField = false;
|
|
205
|
+
let safety = 0;
|
|
206
|
+
while (pXml.includes("MERGEFIELD") && safety++ < 20) {
|
|
207
|
+
const instrParts = [];
|
|
208
|
+
const iRe = /<w:instrText[^>]*>([\s\S]*?)<\/w:instrText>/g;
|
|
209
|
+
let iM;
|
|
210
|
+
while ((iM = iRe.exec(pXml)) !== null) instrParts.push(iM[1]);
|
|
211
|
+
if (instrParts.length === 0) break;
|
|
212
|
+
const allInstr = instrParts.join("");
|
|
213
|
+
const fieldMatch = instrTextRe.exec(allInstr);
|
|
214
|
+
if (!fieldMatch) break;
|
|
215
|
+
hadMergeField = true;
|
|
216
|
+
const replacement = context[fieldMatch[1]] ?? "";
|
|
217
|
+
const before = pXml;
|
|
218
|
+
pXml = stripMergeFieldToPlainText(pXml, replacement);
|
|
219
|
+
if (pXml === before) break;
|
|
220
|
+
}
|
|
221
|
+
if (hadMergeField) {
|
|
222
|
+
const visibleText = extractVisibleText(pXml);
|
|
223
|
+
if (visibleText.replace(/[\s\-\u2013\u2014\u2022·]/g, "") === "") {
|
|
224
|
+
pXml = "";
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
result += pXml;
|
|
228
|
+
lastIndex = pMatch.index + pMatch[0].length;
|
|
229
|
+
}
|
|
230
|
+
result += xml.slice(lastIndex);
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
function stripMergeFieldToPlainText(pXml, replacement) {
|
|
234
|
+
const beginRunRe = /<w:r\b[^>]*>[\s\S]*?<w:fldChar\s+w:fldCharType="begin"[\s\S]*?<\/w:r>/;
|
|
235
|
+
const beginMatch = beginRunRe.exec(pXml);
|
|
236
|
+
if (!beginMatch) return pXml;
|
|
237
|
+
const endRunRe = /<w:r\b[^>]*>[\s\S]*?<w:fldChar\s+w:fldCharType="end"[\s\S]*?<\/w:r>/;
|
|
238
|
+
const afterBegin = pXml.slice(beginMatch.index + beginMatch[0].length);
|
|
239
|
+
const endMatch = endRunRe.exec(afterBegin);
|
|
240
|
+
if (!endMatch) return pXml;
|
|
241
|
+
const startPos = beginMatch.index;
|
|
242
|
+
const endPos = beginMatch.index + beginMatch[0].length + endMatch.index + endMatch[0].length;
|
|
243
|
+
const fieldSection = pXml.slice(startPos, endPos);
|
|
244
|
+
const rPrMatch = /<w:rPr>([\s\S]*?)<\/w:rPr>/.exec(fieldSection);
|
|
245
|
+
const rPr = rPrMatch ? `<w:rPr>${rPrMatch[1]}</w:rPr>` : "";
|
|
246
|
+
const plainRun = `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(replacement)}</w:t></w:r>`;
|
|
247
|
+
return pXml.slice(0, startPos) + plainRun + pXml.slice(endPos);
|
|
248
|
+
}
|
|
249
|
+
function extractVisibleText(pXml) {
|
|
250
|
+
let text = "";
|
|
251
|
+
const tRe = /<w:t[^>]*>([^<]*)<\/w:t>/g;
|
|
252
|
+
let m;
|
|
253
|
+
while ((m = tRe.exec(pXml)) !== null) text += m[1];
|
|
254
|
+
return text;
|
|
255
|
+
}
|
|
256
|
+
function escapeXml(str) {
|
|
257
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
258
|
+
}
|
|
259
|
+
function stripRemainingMergeFields(xml) {
|
|
260
|
+
const pRegex = /<w:p[\s>][\s\S]*?<\/w:p>/g;
|
|
261
|
+
let result = "";
|
|
262
|
+
let lastIndex = 0;
|
|
263
|
+
let pMatch;
|
|
264
|
+
while ((pMatch = pRegex.exec(xml)) !== null) {
|
|
265
|
+
result += xml.slice(lastIndex, pMatch.index);
|
|
266
|
+
let pXml = pMatch[0];
|
|
267
|
+
let safety = 0;
|
|
268
|
+
while (pXml.includes("MERGEFIELD") && safety++ < 20) {
|
|
269
|
+
const before = pXml;
|
|
270
|
+
pXml = stripMergeFieldToPlainText(pXml, "");
|
|
271
|
+
if (pXml === before) break;
|
|
272
|
+
}
|
|
273
|
+
result += pXml;
|
|
274
|
+
lastIndex = pMatch.index + pMatch[0].length;
|
|
275
|
+
}
|
|
276
|
+
result += xml.slice(lastIndex);
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
function sanitizeAllFieldCodes(zip) {
|
|
280
|
+
const contentPattern = /^word\/(document|header\d*|footer\d*|footnotes|endnotes|comments)\.xml$/;
|
|
281
|
+
const files = zip.file(/\.xml$/);
|
|
282
|
+
for (const entry of files) {
|
|
283
|
+
if (!contentPattern.test(entry.name)) continue;
|
|
284
|
+
const xml = entry.asText();
|
|
285
|
+
if (xml.includes("MERGEFIELD")) {
|
|
286
|
+
zip.file(entry.name, stripRemainingMergeFields(xml));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
291
|
+
0 && (module.exports = {
|
|
292
|
+
StubPdfmeFormEngine,
|
|
293
|
+
buildByotContext,
|
|
294
|
+
buildPreviewContext,
|
|
295
|
+
renderDocxWithFallback,
|
|
296
|
+
renderDocxWithSyntax,
|
|
297
|
+
renderMergeFieldsDocx
|
|
298
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
async function renderDocxWithSyntax(sourceBuffer, context, syntax) {
|
|
3
|
+
const [{ default: PizZip }, { default: Docxtemplater }] = await Promise.all([
|
|
4
|
+
import("pizzip"),
|
|
5
|
+
import("docxtemplater")
|
|
6
|
+
]);
|
|
7
|
+
const zip = new PizZip(Buffer.from(sourceBuffer).toString("binary"));
|
|
8
|
+
const options = {
|
|
9
|
+
paragraphLoop: true,
|
|
10
|
+
linebreaks: true
|
|
11
|
+
};
|
|
12
|
+
if (syntax === "bracket") {
|
|
13
|
+
options.delimiters = { start: "[", end: "]" };
|
|
14
|
+
} else if (syntax === "guillemets") {
|
|
15
|
+
options.delimiters = { start: "\xAB", end: "\xBB" };
|
|
16
|
+
}
|
|
17
|
+
const doc = new Docxtemplater(zip, options);
|
|
18
|
+
doc.render(context);
|
|
19
|
+
return Buffer.from(doc.getZip().generate({ type: "nodebuffer", compression: "DEFLATE" }));
|
|
20
|
+
}
|
|
21
|
+
async function renderDocxWithFallback(sourceBuffer, context, preferredSyntax) {
|
|
22
|
+
const attempts = preferredSyntax === "bracket" ? ["bracket", "curly", "guillemets"] : ["curly", "bracket", "guillemets"];
|
|
23
|
+
let lastError = null;
|
|
24
|
+
for (const syntax of attempts) {
|
|
25
|
+
try {
|
|
26
|
+
return await renderDocxWithSyntax(sourceBuffer, context, syntax);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Unable to render DOCX with fallback syntaxes (${lastError?.message ?? "unknown error"})`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
async function renderMergeFieldsDocx(sourceBuffer, context) {
|
|
36
|
+
const { default: PizZip } = await import("pizzip");
|
|
37
|
+
const zip = new PizZip(Buffer.from(sourceBuffer).toString("binary"));
|
|
38
|
+
const filesToPatch = [
|
|
39
|
+
"word/document.xml",
|
|
40
|
+
"word/header1.xml",
|
|
41
|
+
"word/header2.xml",
|
|
42
|
+
"word/header3.xml",
|
|
43
|
+
"word/footer1.xml",
|
|
44
|
+
"word/footer2.xml",
|
|
45
|
+
"word/footer3.xml",
|
|
46
|
+
"word/footnotes.xml",
|
|
47
|
+
"word/endnotes.xml"
|
|
48
|
+
];
|
|
49
|
+
for (const fileName of filesToPatch) {
|
|
50
|
+
const file = zip.file(fileName);
|
|
51
|
+
if (!file) continue;
|
|
52
|
+
const xml = file.asText();
|
|
53
|
+
zip.file(fileName, replaceMergeFieldsInXml(xml, context));
|
|
54
|
+
}
|
|
55
|
+
const settingsFile = zip.file("word/settings.xml");
|
|
56
|
+
if (settingsFile) {
|
|
57
|
+
let settingsXml = settingsFile.asText();
|
|
58
|
+
settingsXml = settingsXml.replace(/<w:mailMerge[\s\S]*?<\/w:mailMerge>/g, "");
|
|
59
|
+
settingsXml = settingsXml.replace(/<w:mailMerge\s*\/>/g, "");
|
|
60
|
+
settingsXml = settingsXml.replace(/<w:attachedTemplate[\s\S]*?\/>/g, "");
|
|
61
|
+
zip.file("word/settings.xml", settingsXml);
|
|
62
|
+
}
|
|
63
|
+
sanitizeAllFieldCodes(zip);
|
|
64
|
+
return Buffer.from(zip.generate({ type: "nodebuffer", compression: "DEFLATE" }));
|
|
65
|
+
}
|
|
66
|
+
function buildByotContext(mappings, templateData) {
|
|
67
|
+
const context = {};
|
|
68
|
+
let mappedCount = 0;
|
|
69
|
+
let unresolvedCount = 0;
|
|
70
|
+
let curly = 0;
|
|
71
|
+
let bracket = 0;
|
|
72
|
+
let mergefield = 0;
|
|
73
|
+
for (const row of mappings) {
|
|
74
|
+
if (row.status === "ignored") continue;
|
|
75
|
+
if (row.syntax === "curly") curly += 1;
|
|
76
|
+
if (row.syntax === "bracket") bracket += 1;
|
|
77
|
+
if (row.syntax === "mergefield") mergefield += 1;
|
|
78
|
+
const targetField = row.mappedField ?? row.suggestedField;
|
|
79
|
+
if (!targetField) {
|
|
80
|
+
unresolvedCount += 1;
|
|
81
|
+
context[row.rawToken] = "";
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const value = templateData[targetField];
|
|
85
|
+
if (value === void 0 || value === null || value === "") {
|
|
86
|
+
unresolvedCount += 1;
|
|
87
|
+
context[row.rawToken] = "";
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
mappedCount += 1;
|
|
91
|
+
context[row.rawToken] = String(value);
|
|
92
|
+
}
|
|
93
|
+
const preferredSyntax = mergefield > curly && mergefield > bracket ? "mergefield" : bracket > curly ? "bracket" : "curly";
|
|
94
|
+
return { context, mappedCount, unresolvedCount, preferredSyntax };
|
|
95
|
+
}
|
|
96
|
+
function buildPreviewContext(mappings, resolveExample, resolveRequirement) {
|
|
97
|
+
const context = {};
|
|
98
|
+
let mappedCount = 0;
|
|
99
|
+
let unresolvedCount = 0;
|
|
100
|
+
let curly = 0;
|
|
101
|
+
let bracket = 0;
|
|
102
|
+
let mergefield = 0;
|
|
103
|
+
const unresolvedBreakdown = { required: 0, recommended: 0, optional: 0, unknown: 0 };
|
|
104
|
+
const toMissingMarker = (label, tokenOrField) => `[[${label}:${tokenOrField}]]`;
|
|
105
|
+
const markUnresolved = (requirement, tokenOrField) => {
|
|
106
|
+
unresolvedCount += 1;
|
|
107
|
+
if (requirement === "required") {
|
|
108
|
+
unresolvedBreakdown.required += 1;
|
|
109
|
+
return toMissingMarker("MANQUANT_OBLIGATOIRE", tokenOrField);
|
|
110
|
+
}
|
|
111
|
+
if (requirement === "recommended") {
|
|
112
|
+
unresolvedBreakdown.recommended += 1;
|
|
113
|
+
return toMissingMarker("MANQUANT_RECOMMANDE", tokenOrField);
|
|
114
|
+
}
|
|
115
|
+
if (requirement === "optional") {
|
|
116
|
+
unresolvedBreakdown.optional += 1;
|
|
117
|
+
return toMissingMarker("MANQUANT_OPTIONNEL", tokenOrField);
|
|
118
|
+
}
|
|
119
|
+
unresolvedBreakdown.unknown += 1;
|
|
120
|
+
return toMissingMarker("A_MAPPER", tokenOrField);
|
|
121
|
+
};
|
|
122
|
+
for (const row of mappings) {
|
|
123
|
+
if (row.status === "ignored") continue;
|
|
124
|
+
if (row.syntax === "curly") curly += 1;
|
|
125
|
+
if (row.syntax === "bracket") bracket += 1;
|
|
126
|
+
if (row.syntax === "mergefield") mergefield += 1;
|
|
127
|
+
const targetField = row.mappedField ?? row.suggestedField;
|
|
128
|
+
if (!targetField) {
|
|
129
|
+
context[row.rawToken] = markUnresolved(null, row.rawToken);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const example = resolveExample(targetField);
|
|
133
|
+
if (example === null || example === "") {
|
|
134
|
+
const requirement = resolveRequirement ? resolveRequirement(targetField) : null;
|
|
135
|
+
context[row.rawToken] = markUnresolved(requirement, targetField);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
mappedCount += 1;
|
|
139
|
+
context[row.rawToken] = example;
|
|
140
|
+
}
|
|
141
|
+
const preferredSyntax = mergefield > curly && mergefield > bracket ? "mergefield" : bracket > curly ? "bracket" : "curly";
|
|
142
|
+
return {
|
|
143
|
+
context,
|
|
144
|
+
mappedCount,
|
|
145
|
+
unresolvedCount,
|
|
146
|
+
unresolvedBreakdown,
|
|
147
|
+
preferredSyntax
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
var StubPdfmeFormEngine = class {
|
|
151
|
+
name = "pdfme-stub";
|
|
152
|
+
async renderPdfForm(_template, _inputs) {
|
|
153
|
+
return new Uint8Array();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
function replaceMergeFieldsInXml(xml, context) {
|
|
157
|
+
const instrTextRe = /MERGEFIELD\s+([\w\u00C0-\u024FŒœ]+)/;
|
|
158
|
+
const pRegex = /<w:p[\s>][\s\S]*?<\/w:p>/g;
|
|
159
|
+
let result = "";
|
|
160
|
+
let lastIndex = 0;
|
|
161
|
+
let pMatch;
|
|
162
|
+
while ((pMatch = pRegex.exec(xml)) !== null) {
|
|
163
|
+
result += xml.slice(lastIndex, pMatch.index);
|
|
164
|
+
let pXml = pMatch[0];
|
|
165
|
+
let hadMergeField = false;
|
|
166
|
+
let safety = 0;
|
|
167
|
+
while (pXml.includes("MERGEFIELD") && safety++ < 20) {
|
|
168
|
+
const instrParts = [];
|
|
169
|
+
const iRe = /<w:instrText[^>]*>([\s\S]*?)<\/w:instrText>/g;
|
|
170
|
+
let iM;
|
|
171
|
+
while ((iM = iRe.exec(pXml)) !== null) instrParts.push(iM[1]);
|
|
172
|
+
if (instrParts.length === 0) break;
|
|
173
|
+
const allInstr = instrParts.join("");
|
|
174
|
+
const fieldMatch = instrTextRe.exec(allInstr);
|
|
175
|
+
if (!fieldMatch) break;
|
|
176
|
+
hadMergeField = true;
|
|
177
|
+
const replacement = context[fieldMatch[1]] ?? "";
|
|
178
|
+
const before = pXml;
|
|
179
|
+
pXml = stripMergeFieldToPlainText(pXml, replacement);
|
|
180
|
+
if (pXml === before) break;
|
|
181
|
+
}
|
|
182
|
+
if (hadMergeField) {
|
|
183
|
+
const visibleText = extractVisibleText(pXml);
|
|
184
|
+
if (visibleText.replace(/[\s\-\u2013\u2014\u2022·]/g, "") === "") {
|
|
185
|
+
pXml = "";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
result += pXml;
|
|
189
|
+
lastIndex = pMatch.index + pMatch[0].length;
|
|
190
|
+
}
|
|
191
|
+
result += xml.slice(lastIndex);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
function stripMergeFieldToPlainText(pXml, replacement) {
|
|
195
|
+
const beginRunRe = /<w:r\b[^>]*>[\s\S]*?<w:fldChar\s+w:fldCharType="begin"[\s\S]*?<\/w:r>/;
|
|
196
|
+
const beginMatch = beginRunRe.exec(pXml);
|
|
197
|
+
if (!beginMatch) return pXml;
|
|
198
|
+
const endRunRe = /<w:r\b[^>]*>[\s\S]*?<w:fldChar\s+w:fldCharType="end"[\s\S]*?<\/w:r>/;
|
|
199
|
+
const afterBegin = pXml.slice(beginMatch.index + beginMatch[0].length);
|
|
200
|
+
const endMatch = endRunRe.exec(afterBegin);
|
|
201
|
+
if (!endMatch) return pXml;
|
|
202
|
+
const startPos = beginMatch.index;
|
|
203
|
+
const endPos = beginMatch.index + beginMatch[0].length + endMatch.index + endMatch[0].length;
|
|
204
|
+
const fieldSection = pXml.slice(startPos, endPos);
|
|
205
|
+
const rPrMatch = /<w:rPr>([\s\S]*?)<\/w:rPr>/.exec(fieldSection);
|
|
206
|
+
const rPr = rPrMatch ? `<w:rPr>${rPrMatch[1]}</w:rPr>` : "";
|
|
207
|
+
const plainRun = `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(replacement)}</w:t></w:r>`;
|
|
208
|
+
return pXml.slice(0, startPos) + plainRun + pXml.slice(endPos);
|
|
209
|
+
}
|
|
210
|
+
function extractVisibleText(pXml) {
|
|
211
|
+
let text = "";
|
|
212
|
+
const tRe = /<w:t[^>]*>([^<]*)<\/w:t>/g;
|
|
213
|
+
let m;
|
|
214
|
+
while ((m = tRe.exec(pXml)) !== null) text += m[1];
|
|
215
|
+
return text;
|
|
216
|
+
}
|
|
217
|
+
function escapeXml(str) {
|
|
218
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
219
|
+
}
|
|
220
|
+
function stripRemainingMergeFields(xml) {
|
|
221
|
+
const pRegex = /<w:p[\s>][\s\S]*?<\/w:p>/g;
|
|
222
|
+
let result = "";
|
|
223
|
+
let lastIndex = 0;
|
|
224
|
+
let pMatch;
|
|
225
|
+
while ((pMatch = pRegex.exec(xml)) !== null) {
|
|
226
|
+
result += xml.slice(lastIndex, pMatch.index);
|
|
227
|
+
let pXml = pMatch[0];
|
|
228
|
+
let safety = 0;
|
|
229
|
+
while (pXml.includes("MERGEFIELD") && safety++ < 20) {
|
|
230
|
+
const before = pXml;
|
|
231
|
+
pXml = stripMergeFieldToPlainText(pXml, "");
|
|
232
|
+
if (pXml === before) break;
|
|
233
|
+
}
|
|
234
|
+
result += pXml;
|
|
235
|
+
lastIndex = pMatch.index + pMatch[0].length;
|
|
236
|
+
}
|
|
237
|
+
result += xml.slice(lastIndex);
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
function sanitizeAllFieldCodes(zip) {
|
|
241
|
+
const contentPattern = /^word\/(document|header\d*|footer\d*|footnotes|endnotes|comments)\.xml$/;
|
|
242
|
+
const files = zip.file(/\.xml$/);
|
|
243
|
+
for (const entry of files) {
|
|
244
|
+
if (!contentPattern.test(entry.name)) continue;
|
|
245
|
+
const xml = entry.asText();
|
|
246
|
+
if (xml.includes("MERGEFIELD")) {
|
|
247
|
+
zip.file(entry.name, stripRemainingMergeFields(xml));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export {
|
|
252
|
+
StubPdfmeFormEngine,
|
|
253
|
+
buildByotContext,
|
|
254
|
+
buildPreviewContext,
|
|
255
|
+
renderDocxWithFallback,
|
|
256
|
+
renderDocxWithSyntax,
|
|
257
|
+
renderMergeFieldsDocx
|
|
258
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhanquier/template-render",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Rendering contracts for DOCX/HTML/PDF templating",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/TrustAkt/trustakt-templating.git"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"docxtemplater": "^3.67.6",
|
|
29
|
+
"pizzip": "^3.2.0",
|
|
30
|
+
"@bhanquier/template-core": "0.1.2"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^20.10.0",
|
|
34
|
+
"tsup": "^8.5.1",
|
|
35
|
+
"typescript": "^5.3.0",
|
|
36
|
+
"vitest": "^4.0.15"
|
|
37
|
+
},
|
|
38
|
+
"license": "UNLICENSED",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup src/index.ts --format esm,cjs --out-dir dist --clean && tsc --noEmit",
|
|
41
|
+
"dev": "tsup src/index.ts --format esm,cjs --out-dir dist --watch",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
}
|
|
45
|
+
}
|