@zola_do/docx 0.2.5 → 0.2.6
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/README.md +387 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
# @zola_do/docx
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@zola_do/docx)
|
|
4
|
+
[](https://www.npmjs.com/package/@zola_do/docx)
|
|
5
|
+
[](https://opensource.org/licenses/ISC)
|
|
6
|
+
|
|
3
7
|
DOCX template processing for NestJS using docx-templates.
|
|
4
8
|
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`@zola_do/docx` provides:
|
|
12
|
+
|
|
13
|
+
- **Template Population** — Fill DOCX templates with data
|
|
14
|
+
- **Placeholder Validation** — Check for required placeholders
|
|
15
|
+
- **Handlebars Syntax** — Supports loops, conditionals, images
|
|
16
|
+
- **Buffer Output** — Returns generated DOCX as Buffer
|
|
17
|
+
|
|
5
18
|
## Installation
|
|
6
19
|
|
|
7
20
|
```bash
|
|
@@ -12,13 +25,43 @@ npm install @zola_do/docx
|
|
|
12
25
|
npm install @zola_do/nestjs-shared
|
|
13
26
|
```
|
|
14
27
|
|
|
15
|
-
|
|
28
|
+
### Dependencies
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install docx-templates
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
16
35
|
|
|
17
|
-
###
|
|
36
|
+
### 1. Create a Template
|
|
37
|
+
|
|
38
|
+
Create a Word document with placeholders:
|
|
39
|
+
|
|
40
|
+
```docx
|
|
41
|
+
Dear {{name}},
|
|
42
|
+
|
|
43
|
+
Thank you for your order #{{orderNumber}}.
|
|
44
|
+
|
|
45
|
+
Order Details:
|
|
46
|
+
{{#each items}}
|
|
47
|
+
- {{this.name}}: ${{this.price}}
|
|
48
|
+
{{/each}}
|
|
49
|
+
|
|
50
|
+
Total: ${{total}}
|
|
51
|
+
|
|
52
|
+
{{#if isPremium}}
|
|
53
|
+
As a premium member, you get 10% off your next order!
|
|
54
|
+
{{/if}}
|
|
55
|
+
|
|
56
|
+
Best regards,
|
|
57
|
+
{{companyName}}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Register Module
|
|
18
61
|
|
|
19
62
|
```typescript
|
|
20
|
-
import { Module } from
|
|
21
|
-
import { DocxModule } from
|
|
63
|
+
import { Module } from "@nestjs/common";
|
|
64
|
+
import { DocxModule } from "@zola_do/docx";
|
|
22
65
|
|
|
23
66
|
@Module({
|
|
24
67
|
imports: [DocxModule],
|
|
@@ -26,54 +69,372 @@ import { DocxModule } from '@zola_do/docx';
|
|
|
26
69
|
export class AppModule {}
|
|
27
70
|
```
|
|
28
71
|
|
|
29
|
-
###
|
|
30
|
-
|
|
31
|
-
Fill a DOCX template with data. Use `{placeholder}` syntax in your Word document:
|
|
72
|
+
### 3. Generate Document
|
|
32
73
|
|
|
33
74
|
```typescript
|
|
34
|
-
import { Injectable } from
|
|
35
|
-
import { DocxService } from
|
|
75
|
+
import { Injectable } from "@nestjs/common";
|
|
76
|
+
import { DocxService } from "@zola_do/docx";
|
|
36
77
|
|
|
37
78
|
@Injectable()
|
|
38
79
|
export class ReportService {
|
|
39
80
|
constructor(private readonly docxService: DocxService) {}
|
|
40
81
|
|
|
41
|
-
async
|
|
42
|
-
|
|
43
|
-
|
|
82
|
+
async generateInvoice(order: Order) {
|
|
83
|
+
const templateBuffer = await this.loadTemplate("invoice.docx");
|
|
84
|
+
|
|
85
|
+
const result = await this.docxService.generateDocx(templateBuffer, {
|
|
86
|
+
name: order.customerName,
|
|
87
|
+
orderNumber: order.number,
|
|
88
|
+
items: order.items,
|
|
89
|
+
total: order.total,
|
|
90
|
+
isPremium: order.customer.isPremium,
|
|
91
|
+
companyName: "My Company",
|
|
92
|
+
});
|
|
93
|
+
|
|
44
94
|
return result; // Buffer of filled DOCX
|
|
45
95
|
}
|
|
46
96
|
}
|
|
47
97
|
```
|
|
48
98
|
|
|
49
|
-
|
|
99
|
+
## Template Syntax
|
|
100
|
+
|
|
101
|
+
The package uses [docx-templates](https://github.com/guigrpa/docx-templates) for template rendering.
|
|
102
|
+
|
|
103
|
+
### Basic Placeholders
|
|
104
|
+
|
|
105
|
+
```handlebars
|
|
106
|
+
{{variableName}}
|
|
107
|
+
|
|
108
|
+
{{nested.property}}
|
|
109
|
+
|
|
110
|
+
{{array.index}}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Conditionals
|
|
114
|
+
|
|
115
|
+
```handlebars
|
|
116
|
+
{{#if condition}}
|
|
117
|
+
Content when true
|
|
118
|
+
{{else}}
|
|
119
|
+
Content when false
|
|
120
|
+
{{/if}}
|
|
121
|
+
|
|
122
|
+
{{#unless condition}}
|
|
123
|
+
Content when false
|
|
124
|
+
{{/unless}}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Loops
|
|
128
|
+
|
|
129
|
+
```handlebars
|
|
130
|
+
{{#each items}}
|
|
131
|
+
{{this.name}}
|
|
132
|
+
- ${{this.price}}
|
|
133
|
+
{{/each}}
|
|
134
|
+
|
|
135
|
+
{{#each orders}}
|
|
136
|
+
Order #{{this.number}}:
|
|
137
|
+
{{#each this.items}}
|
|
138
|
+
-
|
|
139
|
+
{{this.name}}
|
|
140
|
+
{{/each}}
|
|
141
|
+
{{/each}}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Nested Data
|
|
145
|
+
|
|
146
|
+
```handlebars
|
|
147
|
+
{{customer.name}}
|
|
148
|
+
{{customer.address.city}}
|
|
149
|
+
{{customer.address.zip}}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Helpers
|
|
153
|
+
|
|
154
|
+
```handlebars
|
|
155
|
+
{{uppercase name}}
|
|
156
|
+
{{lowercase email}}
|
|
157
|
+
{{formatDate date "MMMM YYYY"}}
|
|
158
|
+
{{currency amount}}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Images
|
|
162
|
+
|
|
163
|
+
```handlebars
|
|
164
|
+
{{#image filename width height}}
|
|
165
|
+
logo.png
|
|
166
|
+
{{/image}}
|
|
167
|
+
|
|
168
|
+
{{#imageUrl url width height}}
|
|
169
|
+
https://example.com/logo.png
|
|
170
|
+
{{/imageUrl}}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Advanced Templates
|
|
174
|
+
|
|
175
|
+
### Table with Rows
|
|
176
|
+
|
|
177
|
+
```handlebars
|
|
178
|
+
| Product | Quantity | Price | |---------|----------|-------|
|
|
179
|
+
{{#each items}}
|
|
180
|
+
|
|
|
181
|
+
{{this.name}}
|
|
182
|
+
|
|
|
183
|
+
{{this.quantity}}
|
|
184
|
+
| ${{this.price}}
|
|
185
|
+
|
|
|
186
|
+
{{/each}}
|
|
187
|
+
| **Total** | | **${{total}}** |
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Nested Tables
|
|
191
|
+
|
|
192
|
+
```handlebars
|
|
193
|
+
{{#each orders}}
|
|
194
|
+
Order #{{this.number}}
|
|
195
|
+
-
|
|
196
|
+
{{this.date}}
|
|
197
|
+
|
|
198
|
+
| Item | Qty | Price | |------|-----|-------|
|
|
199
|
+
{{#each this.items}}
|
|
200
|
+
|
|
|
201
|
+
{{name}}
|
|
202
|
+
|
|
|
203
|
+
{{qty}}
|
|
204
|
+
| ${{price}}
|
|
205
|
+
|
|
|
206
|
+
{{/each}}
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
{{/each}}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Conditional Sections
|
|
213
|
+
|
|
214
|
+
```handlebars
|
|
215
|
+
{{#if hasDiscount}}
|
|
216
|
+
Original Price: ~~${{originalPrice}}~~ Discounted Price: ${{discountedPrice}}
|
|
217
|
+
{{else}}
|
|
218
|
+
Price: ${{price}}
|
|
219
|
+
{{/if}}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Validation
|
|
223
|
+
|
|
224
|
+
### Validate Document
|
|
225
|
+
|
|
226
|
+
Check that a template contains all required placeholders:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
async validateInvoiceTemplate() {
|
|
230
|
+
const templateBuffer = await this.loadTemplate('invoice.docx');
|
|
231
|
+
|
|
232
|
+
const missingProps = await this.docxService.validateDocument(
|
|
233
|
+
templateBuffer,
|
|
234
|
+
['name', 'orderNumber', 'items', 'total', 'companyName'],
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
if (missingProps.length > 0) {
|
|
238
|
+
throw new Error(`Missing placeholders: ${missingProps.join(', ')}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Validation Result
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
interface ValidationResult {
|
|
247
|
+
missing: string[]; // Placeholders not in template
|
|
248
|
+
unused: string[]; // Data properties not in template
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Real-World Examples
|
|
50
253
|
|
|
51
|
-
|
|
254
|
+
### Invoice Generation
|
|
52
255
|
|
|
53
256
|
```typescript
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
257
|
+
async generateInvoice(orderId: string): Promise<Buffer> {
|
|
258
|
+
const order = await this.orderService.findOne(orderId);
|
|
259
|
+
const template = await this.loadTemplate('invoice.docx');
|
|
260
|
+
|
|
261
|
+
const data = {
|
|
262
|
+
invoiceNumber: order.invoiceNumber,
|
|
263
|
+
date: new Date().toLocaleDateString(),
|
|
264
|
+
customer: {
|
|
265
|
+
name: order.customer.name,
|
|
266
|
+
email: order.customer.email,
|
|
267
|
+
address: `${order.customer.address}\n${order.customer.city}, ${order.customer.zip}`,
|
|
268
|
+
},
|
|
269
|
+
items: order.items.map(item => ({
|
|
270
|
+
name: item.productName,
|
|
271
|
+
quantity: item.quantity,
|
|
272
|
+
price: item.unitPrice.toFixed(2),
|
|
273
|
+
subtotal: item.subtotal.toFixed(2),
|
|
274
|
+
})),
|
|
275
|
+
subtotal: order.subtotal.toFixed(2),
|
|
276
|
+
tax: order.tax.toFixed(2),
|
|
277
|
+
total: order.total.toFixed(2),
|
|
278
|
+
isPaid: order.status === 'PAID',
|
|
279
|
+
paymentInfo: order.paymentInfo,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
return this.docxService.generateDocx(template, data);
|
|
60
283
|
}
|
|
61
284
|
```
|
|
62
285
|
|
|
63
|
-
|
|
286
|
+
### Certificate Generation
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
async generateCertificate(userId: string, courseId: string): Promise<Buffer> {
|
|
290
|
+
const [user, course] = await Promise.all([
|
|
291
|
+
this.userService.findOne(userId),
|
|
292
|
+
this.courseService.findOne(courseId),
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
const template = await this.loadTemplate('certificate.docx');
|
|
296
|
+
|
|
297
|
+
return this.docxService.generateDocx(template, {
|
|
298
|
+
recipientName: user.fullName,
|
|
299
|
+
courseName: course.name,
|
|
300
|
+
completionDate: new Date().toLocaleDateString('en-US', {
|
|
301
|
+
year: 'numeric',
|
|
302
|
+
month: 'long',
|
|
303
|
+
day: 'numeric',
|
|
304
|
+
}),
|
|
305
|
+
instructorName: course.instructor.name,
|
|
306
|
+
certificateId: generateCertificateId(),
|
|
307
|
+
companyName: 'My Organization',
|
|
308
|
+
logoPath: '/templates/logo.png',
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Template Preparation
|
|
314
|
+
|
|
315
|
+
### Creating Templates
|
|
316
|
+
|
|
317
|
+
1. Open Microsoft Word or LibreOffice
|
|
318
|
+
2. Create a new document
|
|
319
|
+
3. Add placeholders using `{{placeholderName}}` syntax
|
|
320
|
+
4. Save as `.docx` format
|
|
321
|
+
|
|
322
|
+
### Recommended Placeholder Names
|
|
323
|
+
|
|
324
|
+
```handlebars
|
|
325
|
+
{{firstName}}
|
|
326
|
+
{{lastName}}
|
|
327
|
+
{{email}}
|
|
328
|
+
{{phone}}
|
|
329
|
+
{{address}}
|
|
330
|
+
{{city}}
|
|
331
|
+
{{orderNumber}}
|
|
332
|
+
{{orderDate}}
|
|
333
|
+
{{totalAmount}}
|
|
334
|
+
{{status}}
|
|
335
|
+
{{companyName}}
|
|
336
|
+
{{logoPath}}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Testing Templates
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
const testData = {
|
|
343
|
+
name: "Test User",
|
|
344
|
+
orderNumber: "ORD-001",
|
|
345
|
+
items: [
|
|
346
|
+
{ name: "Product A", price: 10 },
|
|
347
|
+
{ name: "Product B", price: 20 },
|
|
348
|
+
],
|
|
349
|
+
total: 30,
|
|
350
|
+
isPremium: false,
|
|
351
|
+
companyName: "Test Company",
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const result = await docxService.generateDocx(templateBuffer, testData);
|
|
355
|
+
// Verify result is valid DOCX
|
|
356
|
+
```
|
|
64
357
|
|
|
65
|
-
|
|
358
|
+
## API Reference
|
|
66
359
|
|
|
67
|
-
|
|
360
|
+
### Module
|
|
68
361
|
|
|
69
|
-
|
|
70
|
-
|
|
362
|
+
```typescript
|
|
363
|
+
DocxModule.forRoot(options?: DocxModuleOptions)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Service
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
class DocxService {
|
|
370
|
+
async generateDocx(
|
|
371
|
+
template: Buffer,
|
|
372
|
+
data: Record<string, any>,
|
|
373
|
+
): Promise<Buffer>;
|
|
374
|
+
|
|
375
|
+
async validateDocument(
|
|
376
|
+
template: Buffer,
|
|
377
|
+
requiredPlaceholders: string[],
|
|
378
|
+
): Promise<string[]>;
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Troubleshooting
|
|
383
|
+
|
|
384
|
+
### Q: Placeholder not replaced?
|
|
385
|
+
|
|
386
|
+
Check for typos in template:
|
|
387
|
+
|
|
388
|
+
```handlebars
|
|
389
|
+
{{naem}}
|
|
390
|
+
← Wrong
|
|
391
|
+
{{name}}
|
|
392
|
+
← Correct
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Q: Loop not working?
|
|
396
|
+
|
|
397
|
+
Ensure data is an array:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// ❌ Won't iterate
|
|
401
|
+
items: "not an array";
|
|
402
|
+
|
|
403
|
+
// ✅ Will iterate
|
|
404
|
+
items: [{ name: "A" }, { name: "B" }];
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Q: Image not displaying?
|
|
408
|
+
|
|
409
|
+
Use correct image syntax:
|
|
410
|
+
|
|
411
|
+
```handlebars
|
|
412
|
+
{{#image path width height}}
|
|
413
|
+
relative/path/to/image.png
|
|
414
|
+
{{/image}}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Q: Conditional not working?
|
|
418
|
+
|
|
419
|
+
Ensure condition is boolean:
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
// ❌ String
|
|
423
|
+
isPremium: "true";
|
|
424
|
+
|
|
425
|
+
// ✅ Boolean
|
|
426
|
+
isPremium: true;
|
|
427
|
+
```
|
|
71
428
|
|
|
72
429
|
## Related Packages
|
|
73
430
|
|
|
74
|
-
- [@zola_do/document-manipulator](../document-manipulator) — PDF/DOCX merge and conversion
|
|
431
|
+
- [@zola_do/document-manipulator](../document-manipulator) — PDF/DOCX merge and conversion
|
|
75
432
|
- [@zola_do/minio](../minio) — Store generated documents
|
|
76
433
|
|
|
434
|
+
## License
|
|
435
|
+
|
|
436
|
+
ISC
|
|
437
|
+
|
|
77
438
|
## Community
|
|
78
439
|
|
|
79
440
|
- [Contributing](../../CONTRIBUTING.md)
|