office-templates 0.1.0 → 0.2.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/AGENTS.md +6 -6
- package/CHANGELOG.md +13 -12
- package/LICENSE +21 -21
- package/README.md +662 -662
- package/lib/browser.js +15 -24
- package/lib/browser.js.map +1 -1
- package/lib/bundled.d.ts +1 -0
- package/lib/debug.js +2 -2
- package/lib/errors.js +2 -2
- package/lib/main.js +8 -7
- package/lib/process-template.js +45 -12
- package/lib/sandbox.js +4 -3
- package/lib/types.d.ts +1 -0
- package/lib/xml.d.ts +2 -0
- package/lib/xml.js +2 -2
- package/package.json +102 -102
- package/rollup.config.mjs +61 -61
- package/tsconfig.json +19 -19
package/README.md
CHANGED
|
@@ -1,662 +1,662 @@
|
|
|
1
|
-
# Office Templates
|
|
2
|
-
|
|
3
|
-
- Fork of the [docx-templates]() project.
|
|
4
|
-
- Template-based docx and pptx report creation for both Node and the browser.
|
|
5
|
-
- The documents are created using Word or Powerpoint, placeholders can be added directly in the file.
|
|
6
|
-
- Execute JavaScript snippets (`EXEC` command, or `!` for short)
|
|
7
|
-
- Note that the JavaScript code in your .docx or .pptx template will be run as-is by the browser.
|
|
8
|
-
Transpilers can't see this code, and won't be able to polyfill it.
|
|
9
|
-
- Embed images, hyperlinks and even HTML dynamically (`IMAGE`, `LINK`, `HTML`).
|
|
10
|
-
- Logic loops with `FOR`/`END-FOR` commands, with support for table rows, nested loops amnd conditionals using `IF`.
|
|
11
|
-
- Possible to isolate the javascript execution with your own sandbox through `runJs`.
|
|
12
|
-
- Include **literal XML** for custom attributes (e.g. color changes)
|
|
13
|
-
- Supports `.docx` and `.pptx`, as well as macro-enabled `.docm` and `.pptm` templates.
|
|
14
|
-
- Works in node.js and browser
|
|
15
|
-
|
|
16
|
-
## Usage
|
|
17
|
-
|
|
18
|
-
- To install from npm run `npm install office-templates`
|
|
19
|
-
- Here is a simple example, with report data injected directly as an object:
|
|
20
|
-
|
|
21
|
-
```js
|
|
22
|
-
import createReport from 'office-templates';
|
|
23
|
-
import fs from 'fs';
|
|
24
|
-
|
|
25
|
-
const template = fs.readFileSync('myTemplate.docx');
|
|
26
|
-
|
|
27
|
-
const buffer = await createReport({
|
|
28
|
-
template,
|
|
29
|
-
data: {
|
|
30
|
-
name: 'John',
|
|
31
|
-
surname: 'Appleseed',
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
fs.writeFileSync('report.docx', buffer)
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
- You can also **provide a sync or Promise-returning data resolver function** instead of a `data` object:
|
|
39
|
-
|
|
40
|
-
```js
|
|
41
|
-
const report = await createReport({
|
|
42
|
-
template,
|
|
43
|
-
data: async () => fetchReportData(),
|
|
44
|
-
});
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Options
|
|
48
|
-
|
|
49
|
-
- The resolver is called once without arguments; its resolved data is used throughout the template.
|
|
50
|
-
- Other options (with defaults):
|
|
51
|
-
|
|
52
|
-
```js
|
|
53
|
-
const report = await createReport({
|
|
54
|
-
/* Additional data to be provided to the template.
|
|
55
|
-
*
|
|
56
|
-
* All of these will be available to JS snippets in your template commands.
|
|
57
|
-
*/
|
|
58
|
-
additionalJsContext: {
|
|
59
|
-
foo: 'bar',
|
|
60
|
-
qrCode: async url => {
|
|
61
|
-
/* build QR and return image data */
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
/*
|
|
66
|
-
* Delimiter for template commands
|
|
67
|
-
*/
|
|
68
|
-
cmdDelimiter: ['{{', '}}'],
|
|
69
|
-
|
|
70
|
-
/*
|
|
71
|
-
* Delimiter to wrap literal XML that will be injected in the model.
|
|
72
|
-
*/
|
|
73
|
-
literalXmlDelimiter: '||',
|
|
74
|
-
|
|
75
|
-
/*
|
|
76
|
-
* Process line breaks
|
|
77
|
-
*/
|
|
78
|
-
processLineBreaks: true,
|
|
79
|
-
|
|
80
|
-
/* Whether to fail on the first error encountered in the template.
|
|
81
|
-
*
|
|
82
|
-
* Can be used to collect all errors in a template (e.g. misspelled commands) before failing.
|
|
83
|
-
*/
|
|
84
|
-
failFast: true,
|
|
85
|
-
|
|
86
|
-
/* This setting ensures createReport throws an error when the result of an INS, HTML, IMAGE, or LINK command turns out to be null or undefined, as this usually indicates a mistake in the template or the invoking code.
|
|
87
|
-
*/
|
|
88
|
-
rejectNullish: false,
|
|
89
|
-
|
|
90
|
-
/* MS Word usually autocorrects JS string literal quotes with unicode 'smart' quotes ('curly' quotes). E.g. 'aubergine' -> ‘aubergine’.
|
|
91
|
-
*
|
|
92
|
-
* This causes an error when evaluating commands containing these smart quotes, as they are not valid JavaScript.
|
|
93
|
-
*
|
|
94
|
-
* If you set fixSmartQuotes to 'true', these smart quotes will automatically get replaced with straight quotes (') before command evaluation.
|
|
95
|
-
*/
|
|
96
|
-
fixSmartQuotes: false,
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Maximum loop iterations allowed when walking through the template.
|
|
100
|
-
*
|
|
101
|
-
* You can increase this to generate reports with large amount of FOR loop elements.
|
|
102
|
-
*
|
|
103
|
-
* Tip: You can disable infinite loop protection by using the `Infinity` constant.
|
|
104
|
-
*
|
|
105
|
-
* This may be useful if you implement a process timeout instead.
|
|
106
|
-
*/
|
|
107
|
-
maximumWalkingDepth: 1_000_000,
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Whether to indent the generated XML to make it more human-readable.
|
|
111
|
-
*
|
|
112
|
-
* Tip: Set this to true if you want to inspect the generated XML or if you want to use a diff tool to compare the generated Office file with another one.
|
|
113
|
-
*
|
|
114
|
-
* Leaving this option to false will result in a smaller file size.
|
|
115
|
-
*/
|
|
116
|
-
indentXml: true,
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Whether to preserve whitespace in the generated XML.
|
|
120
|
-
*
|
|
121
|
-
* Tip: Set this to true if your template contains significant whitespace that you want to preserve in the output document.
|
|
122
|
-
*
|
|
123
|
-
* Leaving this option to false will result in a smaller file size.
|
|
124
|
-
*/
|
|
125
|
-
preserveSpace: true,
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Compression level for the generated Office file.
|
|
129
|
-
*
|
|
130
|
-
* Integer between 0 (no compression, fastest) and 9 (maximum compression, slowest).
|
|
131
|
-
*/
|
|
132
|
-
compressionLevel: 1,
|
|
133
|
-
});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
## Templates
|
|
137
|
-
|
|
138
|
-
The format is detected from the package manifest. The same API accepts `.docx` and `.pptx` templates and returns the same format as the input:
|
|
139
|
-
|
|
140
|
-
```js
|
|
141
|
-
const template = fs.readFileSync('myTemplate.pptx');
|
|
142
|
-
const buffer = await createReport({ template, data: { title: 'Quarterly update' } });
|
|
143
|
-
fs.writeFileSync('report.pptx', buffer);
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
- Place `{{ title }}` or `{{ INS title }}` in a slide text box. Commands are processed in slides, speaker notes, layouts, and masters, including commands split across formatted text runs. `INS`, `EXEC`, `ALIAS`, `FOR`/`END-FOR`, `IF`/`END-IF`, `IMAGE`, and `LINK` work in PowerPoint.
|
|
147
|
-
- Loops repeat paragraphs or table rows within a part; they do not duplicate whole slides and cannot span slides. JavaScript context starts fresh for each part.
|
|
148
|
-
- Use a dedicated text shape for an `IMAGE` command: the shape is replaced by a picture at the same position, with the requested dimensions. Image captions are Word-only. `LINK` creates a clickable text run. `HTML` is Word-only and raises an error in PowerPoint, which has no equivalent of Word's HTML import element.
|
|
149
|
-
- For browser downloads, use the `.pptx` extension and `application/vnd.openxmlformats-officedocument.presentationml.presentation` MIME type.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
## Supported Commands
|
|
153
|
-
|
|
154
|
-
## `INS` or `=`
|
|
155
|
-
|
|
156
|
-
Inserts the result of a given JavaScript snippet as follows.
|
|
157
|
-
|
|
158
|
-
Using code like this:
|
|
159
|
-
```js
|
|
160
|
-
const report = await createReport({
|
|
161
|
-
template,
|
|
162
|
-
data: { name: 'John', surname: 'Appleseed' },
|
|
163
|
-
cmdDelimiter: ['{{', '}}'],
|
|
164
|
-
});
|
|
165
|
-
```
|
|
166
|
-
And a template like this:
|
|
167
|
-
|
|
168
|
-
```
|
|
169
|
-
{{name}} {{surname}}
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Will produce a result docx file that looks like this:
|
|
173
|
-
|
|
174
|
-
```
|
|
175
|
-
John Appleseed
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Alternatively, you can use the more explicit `INS` (insert) command syntax.
|
|
179
|
-
```
|
|
180
|
-
{{INS name}} {{INS surname}}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Note that the last evaluated expression is inserted into the document, so you can include more complex code if you wish:
|
|
184
|
-
|
|
185
|
-
```
|
|
186
|
-
{{INS
|
|
187
|
-
const a = Math.random();
|
|
188
|
-
const b = Math.round((a - 0.5) * 20);
|
|
189
|
-
`A number between -10 and 10: ${b}.`
|
|
190
|
-
}}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
You can also use `=` as shorthand notation instead of `INS`:
|
|
194
|
-
|
|
195
|
-
```
|
|
196
|
-
{{= name}} {{= surname}}
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
Even shorter (and with custom `cmdDelimiter: ['{', '}']`):
|
|
200
|
-
|
|
201
|
-
```
|
|
202
|
-
{name} {surname}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
`INS` automatically waits when its expression returns a promise, then inserts the resolved value. This also applies to `=` and omitted-command shorthand, promise-valued data fields, and async functions from `additionalJsContext`. You do not need to write `await` in the template.
|
|
206
|
-
|
|
207
|
-
For example, place these commands in your template:
|
|
208
|
-
|
|
209
|
-
```text
|
|
210
|
-
{{ INS customerName }}
|
|
211
|
-
{{ INS loadSummary() }}
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
```js
|
|
215
|
-
const report = await createReport({
|
|
216
|
-
template,
|
|
217
|
-
data: { customerName: Promise.resolve('John') },
|
|
218
|
-
additionalJsContext: {
|
|
219
|
-
loadSummary: async () => 'Quarterly summary',
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
This behavior works in DOCX and PPTX with the default executor or a custom `runJs`. Rejected promises follow the usual command error handling. Validation such as `rejectNullish` applies to the resolved value.
|
|
225
|
-
|
|
226
|
-
Use JavaScript's ternary operator to implement an _if-else_ structure:
|
|
227
|
-
|
|
228
|
-
```
|
|
229
|
-
{{= $details.year != null ? `(${$details.year})` : ''}}
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
## `EXEC` (`!`)
|
|
233
|
-
|
|
234
|
-
Executes a given JavaScript snippet, just like `INS` or `=`, but doesn't insert anything in the document. You can use `EXEC`, for example, to define functions or constants before using them elsewhere in your template.
|
|
235
|
-
|
|
236
|
-
```
|
|
237
|
-
{{EXEC
|
|
238
|
-
this.myFun = () => Math.random();
|
|
239
|
-
this.MY_CONSTANT = 3;
|
|
240
|
-
}}
|
|
241
|
-
|
|
242
|
-
{{! this.ANOTHER_CONSTANT = 5; }}
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
Usage elsewhere will then look like
|
|
246
|
-
|
|
247
|
-
```
|
|
248
|
-
{{= MY_CONSTANT }}
|
|
249
|
-
{{= ANOTHER_CONSTANT }}
|
|
250
|
-
{{= myFun() }}
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
**A note on scoping:**
|
|
254
|
-
|
|
255
|
-
By default, snippets execute with `with(this) { return eval(__code__); }`, using an execution context that is retained between commands within each document part. Use explicit context assignments such as `this.MY_CONSTANT = 3` to make new values available to later commands. Local declarations such as `const`, `let`, and function declarations do not persist between evaluations. A custom `runJs` implementation determines its own scoping behavior.
|
|
256
|
-
|
|
257
|
-
This example declares the test function in the context object, making it callable from another snippet.
|
|
258
|
-
```js
|
|
259
|
-
this.test = () => {};
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
While the below example only declares test in the local scope of the snippet, meaning it gets garbage collected after the snippet has executed.
|
|
263
|
-
```js
|
|
264
|
-
function test() {};
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
## `IMAGE`
|
|
268
|
-
|
|
269
|
-
Includes an image with the data resulting from evaluating a JavaScript snippet:
|
|
270
|
-
|
|
271
|
-
```
|
|
272
|
-
{{IMAGE qrCode(project.url)}}
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
In this case, we use a function from `additionalJsContext` object passed to `createReport()` that looks like this:
|
|
276
|
-
|
|
277
|
-
```js
|
|
278
|
-
additionalJsContext: {
|
|
279
|
-
qrCode: url => {
|
|
280
|
-
const dataUrl = createQrImage(url, { size: 500 });
|
|
281
|
-
const data = dataUrl.slice('data:image/gif;base64,'.length);
|
|
282
|
-
return { width: 6, height: 6, data, extension: '.gif' };
|
|
283
|
-
},
|
|
284
|
-
}
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
`IMAGE` also automatically waits for a promise and inserts the resolved image object. This works with promise-valued data fields and functions returning promises, without `await` in the template:
|
|
288
|
-
|
|
289
|
-
```text
|
|
290
|
-
{{ IMAGE loadImage() }}
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
```js
|
|
294
|
-
const report = await createReport({
|
|
295
|
-
template,
|
|
296
|
-
additionalJsContext: {
|
|
297
|
-
loadImage: async () => ({
|
|
298
|
-
data: await fs.promises.readFile('photo.png'),
|
|
299
|
-
extension: '.png',
|
|
300
|
-
width: 6,
|
|
301
|
-
height: 4,
|
|
302
|
-
}),
|
|
303
|
-
},
|
|
304
|
-
});
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
The resolved image object is validated before insertion. Rejected promises follow the usual command error handling, and `rejectNullish` applies after resolution. For PPTX, place the command in a dedicated text shape as described above.
|
|
308
|
-
|
|
309
|
-
The JS snippet must return an _image object_ or a promise resolving to an _image object_, containing:
|
|
310
|
-
|
|
311
|
-
* `width`: desired width of the image on the page _in cm_. Note that the aspect ratio should match that of the input image to avoid stretching.
|
|
312
|
-
* `height` desired height of the image on the page _in cm_.
|
|
313
|
-
* `data`: either an ArrayBuffer or a base64 string with the image data
|
|
314
|
-
* `extension`: one of `'.png'`, `'.gif'`, `'.jpg'`, `'.jpeg'`, `'.svg'`.
|
|
315
|
-
* `thumbnail` _[optional]_: when injecting an SVG image, a fallback non-SVG (png/jpg/gif, etc.) image can be provided. This thumbnail is used when SVG images are not supported (e.g. older versions of Word) or when the document is previewed by e.g. Windows Explorer. See usage example below.
|
|
316
|
-
* `alt` _[optional]_: optional alt text.
|
|
317
|
-
* `rotation` _[optional]_: optional rotation in degrees, with positive angles moving clockwise.
|
|
318
|
-
* `caption` _[optional]_: optional caption displayed below the image
|
|
319
|
-
|
|
320
|
-
In the .docx template:
|
|
321
|
-
```
|
|
322
|
-
{{IMAGE injectSvg()}}
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
Note that you can center the image by centering the IMAGE command in the template.
|
|
326
|
-
|
|
327
|
-
In the `createReport` call:
|
|
328
|
-
```js
|
|
329
|
-
additionalJsContext: {
|
|
330
|
-
injectSvg: () => {
|
|
331
|
-
const svg_data = Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
332
|
-
<rect x="10" y="10" height="100" width="100" style="stroke:#ff0000; fill: #0000ff"/>
|
|
333
|
-
</svg>`, 'utf-8');
|
|
334
|
-
|
|
335
|
-
// Providing a thumbnail is technically optional, as newer versions of Word will just ignore it.
|
|
336
|
-
const thumbnail = {
|
|
337
|
-
data: fs.readFileSync('sample.png'),
|
|
338
|
-
extension: '.png',
|
|
339
|
-
};
|
|
340
|
-
return { width: 6, height: 6, data: svg_data, extension: '.svg', thumbnail };
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
## `LINK`
|
|
346
|
-
|
|
347
|
-
Includes a hyperlink with the data resulting from evaluating a JavaScript snippet:
|
|
348
|
-
|
|
349
|
-
```
|
|
350
|
-
{{LINK ({ url: project.url, label: project.name })}}
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
If the `label` is not specified, the URL is used as a label.
|
|
354
|
-
|
|
355
|
-
## `HTML`
|
|
356
|
-
|
|
357
|
-
Takes the HTML resulting from evaluating a JavaScript snippet and converts it to Word contents.
|
|
358
|
-
|
|
359
|
-
**Important:** This uses altchunk, which is only supported in Microsoft Word, and not in e.g. LibreOffice or Google Docs.
|
|
360
|
-
|
|
361
|
-
```
|
|
362
|
-
{{HTML `
|
|
363
|
-
<meta charset="UTF-8">
|
|
364
|
-
<body>
|
|
365
|
-
<h1>${film.title}</h1>
|
|
366
|
-
<h3>${film.releaseDate.slice(0, 4)}</h3>
|
|
367
|
-
<p>
|
|
368
|
-
<strong style="color: red;">This paragraph should be red and strong</strong>
|
|
369
|
-
</p>
|
|
370
|
-
</body>
|
|
371
|
-
`}}
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
## `FOR` and `END-FOR`
|
|
375
|
-
|
|
376
|
-
Loop over a group of elements (resulting from the evaluation of a JavaScript expression):
|
|
377
|
-
|
|
378
|
-
```
|
|
379
|
-
{{FOR person IN project.people}}
|
|
380
|
-
{{INS $person.name}} (since {{INS $person.since}})
|
|
381
|
-
{{END-FOR person}}
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
Note that inside the loop, the variable relative to the current element being processed must be prefixed with `$`.
|
|
385
|
-
|
|
386
|
-
The `$idx` variable is the zero-based index of the innermost active `FOR` loop. It remains available in `IF` conditions and inside their contents; `IF` blocks do not replace or reset it. When a nested `FOR` finishes, `$idx` refers to the enclosing `FOR` again. For example:
|
|
387
|
-
```
|
|
388
|
-
{{FOR company IN companies}}
|
|
389
|
-
Company ({{$idx}}): {{INS $company.name}}
|
|
390
|
-
Executives:
|
|
391
|
-
{{FOR executive IN $company.executives}}
|
|
392
|
-
- {{$idx}} {{$executive}}
|
|
393
|
-
{{END-FOR executive}}
|
|
394
|
-
{{END-FOR company}}
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
Use `$idx` in a condition to select specific iterations:
|
|
398
|
-
|
|
399
|
-
```text
|
|
400
|
-
{{ FOR person IN people }}{{ IF $idx === 1 }}{{ $person.name }} (index {{ $idx }}){{ END-IF }}{{ END-FOR person }}
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
This inserts only the second person's name. If the condition is inside multiple nested `FOR` loops, it uses the index of the innermost `FOR`, even when additional `IF` blocks are nested around it. This works in DOCX and PPTX, including with a custom `runJs` executor.
|
|
404
|
-
|
|
405
|
-
Since JavaScript expressions are supported, you can for example filter the loop domain:
|
|
406
|
-
|
|
407
|
-
```
|
|
408
|
-
{{FOR person IN project.people.filter(person => person.since > 2013)}}
|
|
409
|
-
...
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
`FOR` loops also work over table rows:
|
|
413
|
-
|
|
414
|
-
```
|
|
415
|
-
----------------------------------------------------------
|
|
416
|
-
| Name | Since |
|
|
417
|
-
----------------------------------------------------------
|
|
418
|
-
| {{FOR person IN | |
|
|
419
|
-
| project.people}} | |
|
|
420
|
-
----------------------------------------------------------
|
|
421
|
-
| {{INS $person.name}} | {{INS $person.since}} |
|
|
422
|
-
----------------------------------------------------------
|
|
423
|
-
| {{END-FOR person}} | |
|
|
424
|
-
----------------------------------------------------------
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
And let you dynamically generate columns:
|
|
428
|
-
|
|
429
|
-
```
|
|
430
|
-
+-------------------------------+--------------------+------------------------+
|
|
431
|
-
| {{ FOR row IN rows}} | | |
|
|
432
|
-
+===============================+====================+========================+
|
|
433
|
-
| {{ FOR column IN columns }} | {{INS $row}} | {{ END-FOR column }} |
|
|
434
|
-
| | | |
|
|
435
|
-
| | Some cell content | |
|
|
436
|
-
| | | |
|
|
437
|
-
| | {{INS $column}} | |
|
|
438
|
-
+-------------------------------+--------------------+------------------------+
|
|
439
|
-
| {{ END-FOR row}} | | |
|
|
440
|
-
+-------------------------------+--------------------+------------------------+
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
Finally, you can nest loops (this example assumes a different data set):
|
|
444
|
-
|
|
445
|
-
```
|
|
446
|
-
{{FOR company IN companies}}
|
|
447
|
-
{{INS $company.name}}
|
|
448
|
-
{{FOR person IN $company.people}}
|
|
449
|
-
* {{INS $person.firstName}}
|
|
450
|
-
{{FOR project IN $person.projects}}
|
|
451
|
-
- {{INS $project.name}}
|
|
452
|
-
{{END-FOR project}}
|
|
453
|
-
{{END-FOR person}}
|
|
454
|
-
|
|
455
|
-
{{END-FOR company}}
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
## `IF` and `END-IF`
|
|
459
|
-
|
|
460
|
-
Include contents conditionally (depending on the evaluation of a JavaScript expression):
|
|
461
|
-
|
|
462
|
-
```
|
|
463
|
-
{{IF person.name === 'Guillermo'}}
|
|
464
|
-
{{= person.fullName }}
|
|
465
|
-
{{END-IF}}
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
You can nest `IF` and `FOR` commands within the same paragraph or table row, including on a single line. Close each block in reverse order of opening it:
|
|
469
|
-
|
|
470
|
-
```text
|
|
471
|
-
{{ IF visible }}{{ FOR item IN items }}{{ IF $item.enabled }}{{ $item.name }} {{ END-IF }}{{ END-FOR item }}{{ END-IF }}
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
This works in both DOCX and PPTX. When an outer condition is false or an outer loop is empty, expressions inside the skipped block are not evaluated.
|
|
475
|
-
|
|
476
|
-
Similarly to the `FOR` command, it also works over table rows. You can also nest `IF` commands
|
|
477
|
-
and mix & match `IF` and `FOR` commands. In fact, for the technically inclined: the `IF` command
|
|
478
|
-
is implemented as a `FOR` command with 1 or 0 iterations, depending on the expression value.
|
|
479
|
-
|
|
480
|
-
## `ALIAS` (and alias resolution with `*`)
|
|
481
|
-
|
|
482
|
-
Define a name for a complete command (especially useful for formatting tables):
|
|
483
|
-
|
|
484
|
-
```
|
|
485
|
-
{{ALIAS name INS $person.name}}
|
|
486
|
-
{{ALIAS since INS $person.since}}
|
|
487
|
-
|
|
488
|
-
----------------------------------------------------------
|
|
489
|
-
| Name | Since |
|
|
490
|
-
----------------------------------------------------------
|
|
491
|
-
| {{FOR person IN | |
|
|
492
|
-
| project.people}} | |
|
|
493
|
-
----------------------------------------------------------
|
|
494
|
-
| {{*name}} | {{*since}} |
|
|
495
|
-
----------------------------------------------------------
|
|
496
|
-
| {{END-FOR person}} | |
|
|
497
|
-
----------------------------------------------------------
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
## Literal XML
|
|
501
|
-
- You can also directly insert Office Open XML markup into the document using the `literalXmlDelimiter`, which is by default set to `||`.
|
|
502
|
-
- Examples of usage for this feature can be
|
|
503
|
-
|
|
504
|
-
E.g. if you have a template like this:
|
|
505
|
-
|
|
506
|
-
```
|
|
507
|
-
{{INS text}}
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
```js
|
|
511
|
-
await createReport({
|
|
512
|
-
template,
|
|
513
|
-
data: { text: 'foo||<w:br/>||bar' },
|
|
514
|
-
}
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
# Error handling
|
|
519
|
-
|
|
520
|
-
- By default, the Promise returned by `createReport` will reject with an error immediately when a problem is encountered in the template, such as a bad command (i.e. it 'fails fast'). In some cases, however, you may want to collect all errors that may exist in the template before failing. For example, this is useful when you are letting your users create templates interactively. You can disable fast-failing by providing the `failFast: false` parameter as shown below. This will make `createReport` reject with an array of errors instead of a single error so you can get a more complete picture of what is wrong with the template.
|
|
521
|
-
|
|
522
|
-
```typescript
|
|
523
|
-
try {
|
|
524
|
-
const report = await createReport({
|
|
525
|
-
template,
|
|
526
|
-
data: {
|
|
527
|
-
name: 'John',
|
|
528
|
-
surname: 'Appleseed',
|
|
529
|
-
},
|
|
530
|
-
failFast: false,
|
|
531
|
-
});
|
|
532
|
-
} catch (errors) {
|
|
533
|
-
if (Array.isArray(errors)) {
|
|
534
|
-
// An array of errors likely caused by bad commands in the template.
|
|
535
|
-
console.log(errors);
|
|
536
|
-
} else {
|
|
537
|
-
// Not an array of template errors, indicating something more serious.
|
|
538
|
-
throw errors;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
## Error types
|
|
544
|
-
The library exposes the following error types. See the `errors.ts` module for details.
|
|
545
|
-
|
|
546
|
-
```
|
|
547
|
-
NullishCommandResultError // thrown when rejectNullish is set to true and a command returns null or undefined
|
|
548
|
-
ObjectCommandResultError // thrown when the result of an `INS` command is an Object. This ensures you don't accidentally put `'[object Object]'` in your report.
|
|
549
|
-
CommandSyntaxError
|
|
550
|
-
InvalidCommandError
|
|
551
|
-
CommandExecutionError
|
|
552
|
-
ImageError
|
|
553
|
-
InternalError
|
|
554
|
-
TemplateParseError
|
|
555
|
-
IncompleteConditionalStatementError // thrown when an IF-statement has no corresponding END-IF command
|
|
556
|
-
UnterminatedForLoopError // thrown when a `FOR` loop is not properly terminated with an `END-FOR`
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
## Custom error handler
|
|
560
|
-
A custom error handler callback can be provided to handle any errors that may occur when executing commands from a template. The value returned by this callback will be inserted into the rendered document instead. The callback is provided with two arguments: the error that was caught and the raw code of the command.
|
|
561
|
-
|
|
562
|
-
```typescript
|
|
563
|
-
const report = await createReport({
|
|
564
|
-
template,
|
|
565
|
-
data: {
|
|
566
|
-
name: 'John',
|
|
567
|
-
surname: 'Appleseed',
|
|
568
|
-
},
|
|
569
|
-
errorHandler: (err, command_code) => {
|
|
570
|
-
return 'command failed!';
|
|
571
|
-
},
|
|
572
|
-
});
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
Using a custom `errorHandler` in combination with `rejectNullish = true` allows users to intelligently replace the result of commands that returned `null` or `undefined` (make sure to check for `NullishCommandResultError`).
|
|
576
|
-
|
|
577
|
-
# Inspecting templates
|
|
578
|
-
The `listCommands` function lets you list all the commands in a docx or pptx template using the same parser as `createReport`.
|
|
579
|
-
|
|
580
|
-
```typescript
|
|
581
|
-
import { listCommands } from 'office-templates';
|
|
582
|
-
const template_buffer = fs.readFileSync('template.docx');
|
|
583
|
-
const commands = await listCommands(template_buffer);
|
|
584
|
-
|
|
585
|
-
// `commands` will contain something like:
|
|
586
|
-
[
|
|
587
|
-
{ raw: 'INS some_variable', code: 'some_variable', type: 'INS' },
|
|
588
|
-
{ raw: 'IMAGE svgImgFile()', code: 'svgImgFile()', type: 'IMAGE' },
|
|
589
|
-
]
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
The `getMetadata` function lets you extract the metadata fields from a document, such as the number of pages or words. Note that this feature has a few limitations:
|
|
593
|
-
- Not all fields may be available, depending on the document.
|
|
594
|
-
- These metadata fields, including the number of pages, are only updated by MS Word (or LibreOffice) when saving the document. Office-templates does not alter these metadata fields, so the number of pages may not reflect the actual size of your rendered document. Office-templates can not reliably determine the number of pages in a document, as this requires a full-fledged docx renderer (e.g. MS Word).
|
|
595
|
-
|
|
596
|
-
```typescript
|
|
597
|
-
import { getMetadata } from 'office-templates';
|
|
598
|
-
const template = fs.readFileSync('template.docx');
|
|
599
|
-
await getMetadata(template)
|
|
600
|
-
// result:
|
|
601
|
-
Object {
|
|
602
|
-
"category": undefined,
|
|
603
|
-
"characters": 24,
|
|
604
|
-
"company": undefined,
|
|
605
|
-
"created": "2015-08-16T18:55:00Z",
|
|
606
|
-
"creator": "Someone Else",
|
|
607
|
-
"description": undefined,
|
|
608
|
-
"lastModifiedBy": "Grau Panea, Guillermo",
|
|
609
|
-
"lastPrinted": undefined,
|
|
610
|
-
"lines": 1,
|
|
611
|
-
"modified": "2016-12-15T11:21:00Z",
|
|
612
|
-
"pages": 1,
|
|
613
|
-
"paragraphs": 1,
|
|
614
|
-
"revision": "32",
|
|
615
|
-
"subject": undefined,
|
|
616
|
-
"template": "Normal.dotm",
|
|
617
|
-
"title": undefined,
|
|
618
|
-
"words": 4,
|
|
619
|
-
}
|
|
620
|
-
```
|
|
621
|
-
|
|
622
|
-
# JavaScript execution and custom sandboxes
|
|
623
|
-
|
|
624
|
-
- By default, template commands execute directly through JavaScript's `Function` and `eval`, without isolation.
|
|
625
|
-
- The library does not include a VM or a sandbox.
|
|
626
|
-
- Only render trusted templates with the default executor.
|
|
627
|
-
- To provide your own sandbox, pass a `runJs` callback to `createReport`. The callback receives `{ sandbox, ctx }`:
|
|
628
|
-
- `sandbox.__code__` contains the JavaScript snippet to evaluate.
|
|
629
|
-
- `sandbox` also contains the report data, `additionalJsContext`, saved bindings from prior commands, and current loop variables such as `$item` and `$idx`.
|
|
630
|
-
- `ctx` contains the template execution context.
|
|
631
|
-
|
|
632
|
-
Return `{ modifiedSandbox, result }`. `modifiedSandbox` must contain the bindings you want to retain for later commands. The callback itself and its `result` may both be promises; the library awaits them. Each document part starts with a fresh execution context. The library resets the internal `__code__` and `__result__` fields before the next command.
|
|
633
|
-
|
|
634
|
-
- For example, if your sandbox adapter exposes an asynchronous `evaluate(code, bindings)` method returning `{ bindings, value }`, connect it like this:
|
|
635
|
-
|
|
636
|
-
```js
|
|
637
|
-
import createReport from 'office-templates';
|
|
638
|
-
import { sandboxEngine } from './my-sandbox.js';
|
|
639
|
-
|
|
640
|
-
const report = await createReport({
|
|
641
|
-
template,
|
|
642
|
-
data: { name: 'John' },
|
|
643
|
-
runJs: async ({ sandbox }) => {
|
|
644
|
-
const { __code__, __result__, ...bindings } = sandbox;
|
|
645
|
-
const evaluation = await sandboxEngine.evaluate(__code__, bindings);
|
|
646
|
-
return {
|
|
647
|
-
modifiedSandbox: {
|
|
648
|
-
...evaluation.bindings,
|
|
649
|
-
__code__: undefined,
|
|
650
|
-
__result__: undefined,
|
|
651
|
-
},
|
|
652
|
-
result: evaluation.value,
|
|
653
|
-
};
|
|
654
|
-
},
|
|
655
|
-
});
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
- Adapt the callback to your sandbox's API. Errors thrown by the callback and rejected promises follow the same `errorHandler` and `failFast` behavior as the default executor.
|
|
659
|
-
- Your chosen sandbox is responsible for isolation, exposed capabilities, and execution limits.
|
|
660
|
-
|
|
661
|
-
# License (MIT)
|
|
662
|
-
- This Project is licensed under the MIT License. See [LICENSE](LICENSE) for more information.
|
|
1
|
+
# Office Templates
|
|
2
|
+
|
|
3
|
+
- Fork of the [docx-templates]() project.
|
|
4
|
+
- Template-based docx and pptx report creation for both Node and the browser.
|
|
5
|
+
- The documents are created using Word or Powerpoint, placeholders can be added directly in the file.
|
|
6
|
+
- Execute JavaScript snippets (`EXEC` command, or `!` for short)
|
|
7
|
+
- Note that the JavaScript code in your .docx or .pptx template will be run as-is by the browser.
|
|
8
|
+
Transpilers can't see this code, and won't be able to polyfill it.
|
|
9
|
+
- Embed images, hyperlinks and even HTML dynamically (`IMAGE`, `LINK`, `HTML`).
|
|
10
|
+
- Logic loops with `FOR`/`END-FOR` commands, with support for table rows, nested loops amnd conditionals using `IF`.
|
|
11
|
+
- Possible to isolate the javascript execution with your own sandbox through `runJs`.
|
|
12
|
+
- Include **literal XML** for custom attributes (e.g. color changes)
|
|
13
|
+
- Supports `.docx` and `.pptx`, as well as macro-enabled `.docm` and `.pptm` templates.
|
|
14
|
+
- Works in node.js and browser
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
- To install from npm run `npm install office-templates`
|
|
19
|
+
- Here is a simple example, with report data injected directly as an object:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import createReport from 'office-templates';
|
|
23
|
+
import fs from 'fs';
|
|
24
|
+
|
|
25
|
+
const template = fs.readFileSync('myTemplate.docx');
|
|
26
|
+
|
|
27
|
+
const buffer = await createReport({
|
|
28
|
+
template,
|
|
29
|
+
data: {
|
|
30
|
+
name: 'John',
|
|
31
|
+
surname: 'Appleseed',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
fs.writeFileSync('report.docx', buffer)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- You can also **provide a sync or Promise-returning data resolver function** instead of a `data` object:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
const report = await createReport({
|
|
42
|
+
template,
|
|
43
|
+
data: async () => fetchReportData(),
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Options
|
|
48
|
+
|
|
49
|
+
- The resolver is called once without arguments; its resolved data is used throughout the template.
|
|
50
|
+
- Other options (with defaults):
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
const report = await createReport({
|
|
54
|
+
/* Additional data to be provided to the template.
|
|
55
|
+
*
|
|
56
|
+
* All of these will be available to JS snippets in your template commands.
|
|
57
|
+
*/
|
|
58
|
+
additionalJsContext: {
|
|
59
|
+
foo: 'bar',
|
|
60
|
+
qrCode: async url => {
|
|
61
|
+
/* build QR and return image data */
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
* Delimiter for template commands
|
|
67
|
+
*/
|
|
68
|
+
cmdDelimiter: ['{{', '}}'],
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
* Delimiter to wrap literal XML that will be injected in the model.
|
|
72
|
+
*/
|
|
73
|
+
literalXmlDelimiter: '||',
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
* Process line breaks
|
|
77
|
+
*/
|
|
78
|
+
processLineBreaks: true,
|
|
79
|
+
|
|
80
|
+
/* Whether to fail on the first error encountered in the template.
|
|
81
|
+
*
|
|
82
|
+
* Can be used to collect all errors in a template (e.g. misspelled commands) before failing.
|
|
83
|
+
*/
|
|
84
|
+
failFast: true,
|
|
85
|
+
|
|
86
|
+
/* This setting ensures createReport throws an error when the result of an INS, HTML, IMAGE, or LINK command turns out to be null or undefined, as this usually indicates a mistake in the template or the invoking code.
|
|
87
|
+
*/
|
|
88
|
+
rejectNullish: false,
|
|
89
|
+
|
|
90
|
+
/* MS Word usually autocorrects JS string literal quotes with unicode 'smart' quotes ('curly' quotes). E.g. 'aubergine' -> ‘aubergine’.
|
|
91
|
+
*
|
|
92
|
+
* This causes an error when evaluating commands containing these smart quotes, as they are not valid JavaScript.
|
|
93
|
+
*
|
|
94
|
+
* If you set fixSmartQuotes to 'true', these smart quotes will automatically get replaced with straight quotes (') before command evaluation.
|
|
95
|
+
*/
|
|
96
|
+
fixSmartQuotes: false,
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Maximum loop iterations allowed when walking through the template.
|
|
100
|
+
*
|
|
101
|
+
* You can increase this to generate reports with large amount of FOR loop elements.
|
|
102
|
+
*
|
|
103
|
+
* Tip: You can disable infinite loop protection by using the `Infinity` constant.
|
|
104
|
+
*
|
|
105
|
+
* This may be useful if you implement a process timeout instead.
|
|
106
|
+
*/
|
|
107
|
+
maximumWalkingDepth: 1_000_000,
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether to indent the generated XML to make it more human-readable.
|
|
111
|
+
*
|
|
112
|
+
* Tip: Set this to true if you want to inspect the generated XML or if you want to use a diff tool to compare the generated Office file with another one.
|
|
113
|
+
*
|
|
114
|
+
* Leaving this option to false will result in a smaller file size.
|
|
115
|
+
*/
|
|
116
|
+
indentXml: true,
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Whether to preserve whitespace in the generated XML.
|
|
120
|
+
*
|
|
121
|
+
* Tip: Set this to true if your template contains significant whitespace that you want to preserve in the output document.
|
|
122
|
+
*
|
|
123
|
+
* Leaving this option to false will result in a smaller file size.
|
|
124
|
+
*/
|
|
125
|
+
preserveSpace: true,
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Compression level for the generated Office file.
|
|
129
|
+
*
|
|
130
|
+
* Integer between 0 (no compression, fastest) and 9 (maximum compression, slowest).
|
|
131
|
+
*/
|
|
132
|
+
compressionLevel: 1,
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Templates
|
|
137
|
+
|
|
138
|
+
The format is detected from the package manifest. The same API accepts `.docx` and `.pptx` templates and returns the same format as the input:
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
const template = fs.readFileSync('myTemplate.pptx');
|
|
142
|
+
const buffer = await createReport({ template, data: { title: 'Quarterly update' } });
|
|
143
|
+
fs.writeFileSync('report.pptx', buffer);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- Place `{{ title }}` or `{{ INS title }}` in a slide text box. Commands are processed in slides, speaker notes, layouts, and masters, including commands split across formatted text runs. `INS`, `EXEC`, `ALIAS`, `FOR`/`END-FOR`, `IF`/`END-IF`, `IMAGE`, and `LINK` work in PowerPoint.
|
|
147
|
+
- Loops repeat paragraphs or table rows within a part; they do not duplicate whole slides and cannot span slides. JavaScript context starts fresh for each part.
|
|
148
|
+
- Use a dedicated text shape for an `IMAGE` command: the shape is replaced by a picture at the same position, with the requested dimensions. Image captions are Word-only. `LINK` creates a clickable text run. `HTML` is Word-only and raises an error in PowerPoint, which has no equivalent of Word's HTML import element.
|
|
149
|
+
- For browser downloads, use the `.pptx` extension and `application/vnd.openxmlformats-officedocument.presentationml.presentation` MIME type.
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
## Supported Commands
|
|
153
|
+
|
|
154
|
+
## `INS` or `=`
|
|
155
|
+
|
|
156
|
+
Inserts the result of a given JavaScript snippet as follows.
|
|
157
|
+
|
|
158
|
+
Using code like this:
|
|
159
|
+
```js
|
|
160
|
+
const report = await createReport({
|
|
161
|
+
template,
|
|
162
|
+
data: { name: 'John', surname: 'Appleseed' },
|
|
163
|
+
cmdDelimiter: ['{{', '}}'],
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
And a template like this:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
{{name}} {{surname}}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Will produce a result docx file that looks like this:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
John Appleseed
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Alternatively, you can use the more explicit `INS` (insert) command syntax.
|
|
179
|
+
```
|
|
180
|
+
{{INS name}} {{INS surname}}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Note that the last evaluated expression is inserted into the document, so you can include more complex code if you wish:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
{{INS
|
|
187
|
+
const a = Math.random();
|
|
188
|
+
const b = Math.round((a - 0.5) * 20);
|
|
189
|
+
`A number between -10 and 10: ${b}.`
|
|
190
|
+
}}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
You can also use `=` as shorthand notation instead of `INS`:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
{{= name}} {{= surname}}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Even shorter (and with custom `cmdDelimiter: ['{', '}']`):
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
{name} {surname}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`INS` automatically waits when its expression returns a promise, then inserts the resolved value. This also applies to `=` and omitted-command shorthand, promise-valued data fields, and async functions from `additionalJsContext`. You do not need to write `await` in the template.
|
|
206
|
+
|
|
207
|
+
For example, place these commands in your template:
|
|
208
|
+
|
|
209
|
+
```text
|
|
210
|
+
{{ INS customerName }}
|
|
211
|
+
{{ INS loadSummary() }}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
const report = await createReport({
|
|
216
|
+
template,
|
|
217
|
+
data: { customerName: Promise.resolve('John') },
|
|
218
|
+
additionalJsContext: {
|
|
219
|
+
loadSummary: async () => 'Quarterly summary',
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
This behavior works in DOCX and PPTX with the default executor or a custom `runJs`. Rejected promises follow the usual command error handling. Validation such as `rejectNullish` applies to the resolved value.
|
|
225
|
+
|
|
226
|
+
Use JavaScript's ternary operator to implement an _if-else_ structure:
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
{{= $details.year != null ? `(${$details.year})` : ''}}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## `EXEC` (`!`)
|
|
233
|
+
|
|
234
|
+
Executes a given JavaScript snippet, just like `INS` or `=`, but doesn't insert anything in the document. You can use `EXEC`, for example, to define functions or constants before using them elsewhere in your template.
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
{{EXEC
|
|
238
|
+
this.myFun = () => Math.random();
|
|
239
|
+
this.MY_CONSTANT = 3;
|
|
240
|
+
}}
|
|
241
|
+
|
|
242
|
+
{{! this.ANOTHER_CONSTANT = 5; }}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Usage elsewhere will then look like
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
{{= MY_CONSTANT }}
|
|
249
|
+
{{= ANOTHER_CONSTANT }}
|
|
250
|
+
{{= myFun() }}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**A note on scoping:**
|
|
254
|
+
|
|
255
|
+
By default, snippets execute with `with(this) { return eval(__code__); }`, using an execution context that is retained between commands within each document part. Use explicit context assignments such as `this.MY_CONSTANT = 3` to make new values available to later commands. Local declarations such as `const`, `let`, and function declarations do not persist between evaluations. A custom `runJs` implementation determines its own scoping behavior.
|
|
256
|
+
|
|
257
|
+
This example declares the test function in the context object, making it callable from another snippet.
|
|
258
|
+
```js
|
|
259
|
+
this.test = () => {};
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
While the below example only declares test in the local scope of the snippet, meaning it gets garbage collected after the snippet has executed.
|
|
263
|
+
```js
|
|
264
|
+
function test() {};
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## `IMAGE`
|
|
268
|
+
|
|
269
|
+
Includes an image with the data resulting from evaluating a JavaScript snippet:
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
{{IMAGE qrCode(project.url)}}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
In this case, we use a function from `additionalJsContext` object passed to `createReport()` that looks like this:
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
additionalJsContext: {
|
|
279
|
+
qrCode: url => {
|
|
280
|
+
const dataUrl = createQrImage(url, { size: 500 });
|
|
281
|
+
const data = dataUrl.slice('data:image/gif;base64,'.length);
|
|
282
|
+
return { width: 6, height: 6, data, extension: '.gif' };
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`IMAGE` also automatically waits for a promise and inserts the resolved image object. This works with promise-valued data fields and functions returning promises, without `await` in the template:
|
|
288
|
+
|
|
289
|
+
```text
|
|
290
|
+
{{ IMAGE loadImage() }}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
const report = await createReport({
|
|
295
|
+
template,
|
|
296
|
+
additionalJsContext: {
|
|
297
|
+
loadImage: async () => ({
|
|
298
|
+
data: await fs.promises.readFile('photo.png'),
|
|
299
|
+
extension: '.png',
|
|
300
|
+
width: 6,
|
|
301
|
+
height: 4,
|
|
302
|
+
}),
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The resolved image object is validated before insertion. Rejected promises follow the usual command error handling, and `rejectNullish` applies after resolution. For PPTX, place the command in a dedicated text shape as described above.
|
|
308
|
+
|
|
309
|
+
The JS snippet must return an _image object_ or a promise resolving to an _image object_, containing:
|
|
310
|
+
|
|
311
|
+
* `width`: desired width of the image on the page _in cm_. Note that the aspect ratio should match that of the input image to avoid stretching.
|
|
312
|
+
* `height` desired height of the image on the page _in cm_.
|
|
313
|
+
* `data`: either an ArrayBuffer or a base64 string with the image data
|
|
314
|
+
* `extension`: one of `'.png'`, `'.gif'`, `'.jpg'`, `'.jpeg'`, `'.svg'`.
|
|
315
|
+
* `thumbnail` _[optional]_: when injecting an SVG image, a fallback non-SVG (png/jpg/gif, etc.) image can be provided. This thumbnail is used when SVG images are not supported (e.g. older versions of Word) or when the document is previewed by e.g. Windows Explorer. See usage example below.
|
|
316
|
+
* `alt` _[optional]_: optional alt text.
|
|
317
|
+
* `rotation` _[optional]_: optional rotation in degrees, with positive angles moving clockwise.
|
|
318
|
+
* `caption` _[optional]_: optional caption displayed below the image
|
|
319
|
+
|
|
320
|
+
In the .docx template:
|
|
321
|
+
```
|
|
322
|
+
{{IMAGE injectSvg()}}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Note that you can center the image by centering the IMAGE command in the template.
|
|
326
|
+
|
|
327
|
+
In the `createReport` call:
|
|
328
|
+
```js
|
|
329
|
+
additionalJsContext: {
|
|
330
|
+
injectSvg: () => {
|
|
331
|
+
const svg_data = Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
332
|
+
<rect x="10" y="10" height="100" width="100" style="stroke:#ff0000; fill: #0000ff"/>
|
|
333
|
+
</svg>`, 'utf-8');
|
|
334
|
+
|
|
335
|
+
// Providing a thumbnail is technically optional, as newer versions of Word will just ignore it.
|
|
336
|
+
const thumbnail = {
|
|
337
|
+
data: fs.readFileSync('sample.png'),
|
|
338
|
+
extension: '.png',
|
|
339
|
+
};
|
|
340
|
+
return { width: 6, height: 6, data: svg_data, extension: '.svg', thumbnail };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## `LINK`
|
|
346
|
+
|
|
347
|
+
Includes a hyperlink with the data resulting from evaluating a JavaScript snippet:
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
{{LINK ({ url: project.url, label: project.name })}}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
If the `label` is not specified, the URL is used as a label.
|
|
354
|
+
|
|
355
|
+
## `HTML`
|
|
356
|
+
|
|
357
|
+
Takes the HTML resulting from evaluating a JavaScript snippet and converts it to Word contents.
|
|
358
|
+
|
|
359
|
+
**Important:** This uses altchunk, which is only supported in Microsoft Word, and not in e.g. LibreOffice or Google Docs.
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
{{HTML `
|
|
363
|
+
<meta charset="UTF-8">
|
|
364
|
+
<body>
|
|
365
|
+
<h1>${film.title}</h1>
|
|
366
|
+
<h3>${film.releaseDate.slice(0, 4)}</h3>
|
|
367
|
+
<p>
|
|
368
|
+
<strong style="color: red;">This paragraph should be red and strong</strong>
|
|
369
|
+
</p>
|
|
370
|
+
</body>
|
|
371
|
+
`}}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## `FOR` and `END-FOR`
|
|
375
|
+
|
|
376
|
+
Loop over a group of elements (resulting from the evaluation of a JavaScript expression):
|
|
377
|
+
|
|
378
|
+
```
|
|
379
|
+
{{FOR person IN project.people}}
|
|
380
|
+
{{INS $person.name}} (since {{INS $person.since}})
|
|
381
|
+
{{END-FOR person}}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Note that inside the loop, the variable relative to the current element being processed must be prefixed with `$`.
|
|
385
|
+
|
|
386
|
+
The `$idx` variable is the zero-based index of the innermost active `FOR` loop. It remains available in `IF` conditions and inside their contents; `IF` blocks do not replace or reset it. When a nested `FOR` finishes, `$idx` refers to the enclosing `FOR` again. For example:
|
|
387
|
+
```
|
|
388
|
+
{{FOR company IN companies}}
|
|
389
|
+
Company ({{$idx}}): {{INS $company.name}}
|
|
390
|
+
Executives:
|
|
391
|
+
{{FOR executive IN $company.executives}}
|
|
392
|
+
- {{$idx}} {{$executive}}
|
|
393
|
+
{{END-FOR executive}}
|
|
394
|
+
{{END-FOR company}}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Use `$idx` in a condition to select specific iterations:
|
|
398
|
+
|
|
399
|
+
```text
|
|
400
|
+
{{ FOR person IN people }}{{ IF $idx === 1 }}{{ $person.name }} (index {{ $idx }}){{ END-IF }}{{ END-FOR person }}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
This inserts only the second person's name. If the condition is inside multiple nested `FOR` loops, it uses the index of the innermost `FOR`, even when additional `IF` blocks are nested around it. This works in DOCX and PPTX, including with a custom `runJs` executor.
|
|
404
|
+
|
|
405
|
+
Since JavaScript expressions are supported, you can for example filter the loop domain:
|
|
406
|
+
|
|
407
|
+
```
|
|
408
|
+
{{FOR person IN project.people.filter(person => person.since > 2013)}}
|
|
409
|
+
...
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
`FOR` loops also work over table rows:
|
|
413
|
+
|
|
414
|
+
```
|
|
415
|
+
----------------------------------------------------------
|
|
416
|
+
| Name | Since |
|
|
417
|
+
----------------------------------------------------------
|
|
418
|
+
| {{FOR person IN | |
|
|
419
|
+
| project.people}} | |
|
|
420
|
+
----------------------------------------------------------
|
|
421
|
+
| {{INS $person.name}} | {{INS $person.since}} |
|
|
422
|
+
----------------------------------------------------------
|
|
423
|
+
| {{END-FOR person}} | |
|
|
424
|
+
----------------------------------------------------------
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
And let you dynamically generate columns:
|
|
428
|
+
|
|
429
|
+
```
|
|
430
|
+
+-------------------------------+--------------------+------------------------+
|
|
431
|
+
| {{ FOR row IN rows}} | | |
|
|
432
|
+
+===============================+====================+========================+
|
|
433
|
+
| {{ FOR column IN columns }} | {{INS $row}} | {{ END-FOR column }} |
|
|
434
|
+
| | | |
|
|
435
|
+
| | Some cell content | |
|
|
436
|
+
| | | |
|
|
437
|
+
| | {{INS $column}} | |
|
|
438
|
+
+-------------------------------+--------------------+------------------------+
|
|
439
|
+
| {{ END-FOR row}} | | |
|
|
440
|
+
+-------------------------------+--------------------+------------------------+
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Finally, you can nest loops (this example assumes a different data set):
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
{{FOR company IN companies}}
|
|
447
|
+
{{INS $company.name}}
|
|
448
|
+
{{FOR person IN $company.people}}
|
|
449
|
+
* {{INS $person.firstName}}
|
|
450
|
+
{{FOR project IN $person.projects}}
|
|
451
|
+
- {{INS $project.name}}
|
|
452
|
+
{{END-FOR project}}
|
|
453
|
+
{{END-FOR person}}
|
|
454
|
+
|
|
455
|
+
{{END-FOR company}}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## `IF` and `END-IF`
|
|
459
|
+
|
|
460
|
+
Include contents conditionally (depending on the evaluation of a JavaScript expression):
|
|
461
|
+
|
|
462
|
+
```
|
|
463
|
+
{{IF person.name === 'Guillermo'}}
|
|
464
|
+
{{= person.fullName }}
|
|
465
|
+
{{END-IF}}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
You can nest `IF` and `FOR` commands within the same paragraph or table row, including on a single line. Close each block in reverse order of opening it:
|
|
469
|
+
|
|
470
|
+
```text
|
|
471
|
+
{{ IF visible }}{{ FOR item IN items }}{{ IF $item.enabled }}{{ $item.name }} {{ END-IF }}{{ END-FOR item }}{{ END-IF }}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
This works in both DOCX and PPTX. When an outer condition is false or an outer loop is empty, expressions inside the skipped block are not evaluated.
|
|
475
|
+
|
|
476
|
+
Similarly to the `FOR` command, it also works over table rows. You can also nest `IF` commands
|
|
477
|
+
and mix & match `IF` and `FOR` commands. In fact, for the technically inclined: the `IF` command
|
|
478
|
+
is implemented as a `FOR` command with 1 or 0 iterations, depending on the expression value.
|
|
479
|
+
|
|
480
|
+
## `ALIAS` (and alias resolution with `*`)
|
|
481
|
+
|
|
482
|
+
Define a name for a complete command (especially useful for formatting tables):
|
|
483
|
+
|
|
484
|
+
```
|
|
485
|
+
{{ALIAS name INS $person.name}}
|
|
486
|
+
{{ALIAS since INS $person.since}}
|
|
487
|
+
|
|
488
|
+
----------------------------------------------------------
|
|
489
|
+
| Name | Since |
|
|
490
|
+
----------------------------------------------------------
|
|
491
|
+
| {{FOR person IN | |
|
|
492
|
+
| project.people}} | |
|
|
493
|
+
----------------------------------------------------------
|
|
494
|
+
| {{*name}} | {{*since}} |
|
|
495
|
+
----------------------------------------------------------
|
|
496
|
+
| {{END-FOR person}} | |
|
|
497
|
+
----------------------------------------------------------
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Literal XML
|
|
501
|
+
- You can also directly insert Office Open XML markup into the document using the `literalXmlDelimiter`, which is by default set to `||`.
|
|
502
|
+
- Examples of usage for this feature can be
|
|
503
|
+
|
|
504
|
+
E.g. if you have a template like this:
|
|
505
|
+
|
|
506
|
+
```
|
|
507
|
+
{{INS text}}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
```js
|
|
511
|
+
await createReport({
|
|
512
|
+
template,
|
|
513
|
+
data: { text: 'foo||<w:br/>||bar' },
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# Error handling
|
|
519
|
+
|
|
520
|
+
- By default, the Promise returned by `createReport` will reject with an error immediately when a problem is encountered in the template, such as a bad command (i.e. it 'fails fast'). In some cases, however, you may want to collect all errors that may exist in the template before failing. For example, this is useful when you are letting your users create templates interactively. You can disable fast-failing by providing the `failFast: false` parameter as shown below. This will make `createReport` reject with an array of errors instead of a single error so you can get a more complete picture of what is wrong with the template.
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
try {
|
|
524
|
+
const report = await createReport({
|
|
525
|
+
template,
|
|
526
|
+
data: {
|
|
527
|
+
name: 'John',
|
|
528
|
+
surname: 'Appleseed',
|
|
529
|
+
},
|
|
530
|
+
failFast: false,
|
|
531
|
+
});
|
|
532
|
+
} catch (errors) {
|
|
533
|
+
if (Array.isArray(errors)) {
|
|
534
|
+
// An array of errors likely caused by bad commands in the template.
|
|
535
|
+
console.log(errors);
|
|
536
|
+
} else {
|
|
537
|
+
// Not an array of template errors, indicating something more serious.
|
|
538
|
+
throw errors;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## Error types
|
|
544
|
+
The library exposes the following error types. See the `errors.ts` module for details.
|
|
545
|
+
|
|
546
|
+
```
|
|
547
|
+
NullishCommandResultError // thrown when rejectNullish is set to true and a command returns null or undefined
|
|
548
|
+
ObjectCommandResultError // thrown when the result of an `INS` command is an Object. This ensures you don't accidentally put `'[object Object]'` in your report.
|
|
549
|
+
CommandSyntaxError
|
|
550
|
+
InvalidCommandError
|
|
551
|
+
CommandExecutionError
|
|
552
|
+
ImageError
|
|
553
|
+
InternalError
|
|
554
|
+
TemplateParseError
|
|
555
|
+
IncompleteConditionalStatementError // thrown when an IF-statement has no corresponding END-IF command
|
|
556
|
+
UnterminatedForLoopError // thrown when a `FOR` loop is not properly terminated with an `END-FOR`
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
## Custom error handler
|
|
560
|
+
A custom error handler callback can be provided to handle any errors that may occur when executing commands from a template. The value returned by this callback will be inserted into the rendered document instead. The callback is provided with two arguments: the error that was caught and the raw code of the command.
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
const report = await createReport({
|
|
564
|
+
template,
|
|
565
|
+
data: {
|
|
566
|
+
name: 'John',
|
|
567
|
+
surname: 'Appleseed',
|
|
568
|
+
},
|
|
569
|
+
errorHandler: (err, command_code) => {
|
|
570
|
+
return 'command failed!';
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Using a custom `errorHandler` in combination with `rejectNullish = true` allows users to intelligently replace the result of commands that returned `null` or `undefined` (make sure to check for `NullishCommandResultError`).
|
|
576
|
+
|
|
577
|
+
# Inspecting templates
|
|
578
|
+
The `listCommands` function lets you list all the commands in a docx or pptx template using the same parser as `createReport`.
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
import { listCommands } from 'office-templates';
|
|
582
|
+
const template_buffer = fs.readFileSync('template.docx');
|
|
583
|
+
const commands = await listCommands(template_buffer);
|
|
584
|
+
|
|
585
|
+
// `commands` will contain something like:
|
|
586
|
+
[
|
|
587
|
+
{ raw: 'INS some_variable', code: 'some_variable', type: 'INS' },
|
|
588
|
+
{ raw: 'IMAGE svgImgFile()', code: 'svgImgFile()', type: 'IMAGE' },
|
|
589
|
+
]
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
The `getMetadata` function lets you extract the metadata fields from a document, such as the number of pages or words. Note that this feature has a few limitations:
|
|
593
|
+
- Not all fields may be available, depending on the document.
|
|
594
|
+
- These metadata fields, including the number of pages, are only updated by MS Word (or LibreOffice) when saving the document. Office-templates does not alter these metadata fields, so the number of pages may not reflect the actual size of your rendered document. Office-templates can not reliably determine the number of pages in a document, as this requires a full-fledged docx renderer (e.g. MS Word).
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
import { getMetadata } from 'office-templates';
|
|
598
|
+
const template = fs.readFileSync('template.docx');
|
|
599
|
+
await getMetadata(template)
|
|
600
|
+
// result:
|
|
601
|
+
Object {
|
|
602
|
+
"category": undefined,
|
|
603
|
+
"characters": 24,
|
|
604
|
+
"company": undefined,
|
|
605
|
+
"created": "2015-08-16T18:55:00Z",
|
|
606
|
+
"creator": "Someone Else",
|
|
607
|
+
"description": undefined,
|
|
608
|
+
"lastModifiedBy": "Grau Panea, Guillermo",
|
|
609
|
+
"lastPrinted": undefined,
|
|
610
|
+
"lines": 1,
|
|
611
|
+
"modified": "2016-12-15T11:21:00Z",
|
|
612
|
+
"pages": 1,
|
|
613
|
+
"paragraphs": 1,
|
|
614
|
+
"revision": "32",
|
|
615
|
+
"subject": undefined,
|
|
616
|
+
"template": "Normal.dotm",
|
|
617
|
+
"title": undefined,
|
|
618
|
+
"words": 4,
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
# JavaScript execution and custom sandboxes
|
|
623
|
+
|
|
624
|
+
- By default, template commands execute directly through JavaScript's `Function` and `eval`, without isolation.
|
|
625
|
+
- The library does not include a VM or a sandbox.
|
|
626
|
+
- Only render trusted templates with the default executor.
|
|
627
|
+
- To provide your own sandbox, pass a `runJs` callback to `createReport`. The callback receives `{ sandbox, ctx }`:
|
|
628
|
+
- `sandbox.__code__` contains the JavaScript snippet to evaluate.
|
|
629
|
+
- `sandbox` also contains the report data, `additionalJsContext`, saved bindings from prior commands, and current loop variables such as `$item` and `$idx`.
|
|
630
|
+
- `ctx` contains the template execution context.
|
|
631
|
+
|
|
632
|
+
Return `{ modifiedSandbox, result }`. `modifiedSandbox` must contain the bindings you want to retain for later commands. The callback itself and its `result` may both be promises; the library awaits them. Each document part starts with a fresh execution context. The library resets the internal `__code__` and `__result__` fields before the next command.
|
|
633
|
+
|
|
634
|
+
- For example, if your sandbox adapter exposes an asynchronous `evaluate(code, bindings)` method returning `{ bindings, value }`, connect it like this:
|
|
635
|
+
|
|
636
|
+
```js
|
|
637
|
+
import createReport from 'office-templates';
|
|
638
|
+
import { sandboxEngine } from './my-sandbox.js';
|
|
639
|
+
|
|
640
|
+
const report = await createReport({
|
|
641
|
+
template,
|
|
642
|
+
data: { name: 'John' },
|
|
643
|
+
runJs: async ({ sandbox }) => {
|
|
644
|
+
const { __code__, __result__, ...bindings } = sandbox;
|
|
645
|
+
const evaluation = await sandboxEngine.evaluate(__code__, bindings);
|
|
646
|
+
return {
|
|
647
|
+
modifiedSandbox: {
|
|
648
|
+
...evaluation.bindings,
|
|
649
|
+
__code__: undefined,
|
|
650
|
+
__result__: undefined,
|
|
651
|
+
},
|
|
652
|
+
result: evaluation.value,
|
|
653
|
+
};
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
- Adapt the callback to your sandbox's API. Errors thrown by the callback and rejected promises follow the same `errorHandler` and `failFast` behavior as the default executor.
|
|
659
|
+
- Your chosen sandbox is responsible for isolation, exposed capabilities, and execution limits.
|
|
660
|
+
|
|
661
|
+
# License (MIT)
|
|
662
|
+
- This Project is licensed under the MIT License. See [LICENSE](LICENSE) for more information.
|