odf-kit 0.8.0 → 0.8.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/README.md +637 -587
- package/dist/odt/content.d.ts.map +1 -1
- package/dist/odt/content.js +7 -1
- package/dist/odt/content.js.map +1 -1
- package/dist/odt/document.d.ts.map +1 -1
- package/dist/odt/document.js +3 -0
- package/dist/odt/document.js.map +1 -1
- package/dist/odt/paragraph-builder.d.ts.map +1 -1
- package/dist/odt/paragraph-builder.js +3 -0
- package/dist/odt/paragraph-builder.js.map +1 -1
- package/dist/odt/types.d.ts +36 -2
- package/dist/odt/types.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,587 +1,637 @@
|
|
|
1
|
-
# odf-kit
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
**
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
```
|
|
76
|
-
import { OdtDocument } from "odf-kit";
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
doc.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
1
|
+
# odf-kit
|
|
2
|
+
|
|
3
|
+
Generate, fill, read, and convert OpenDocument Format files (.odt) in TypeScript and JavaScript. Works in Node.js and browsers. No LibreOffice dependency — pure spec-compliant ODF.
|
|
4
|
+
|
|
5
|
+
**[Documentation & examples →](https://githubnewbie0.github.io/odf-kit/)**
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install odf-kit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Four ways to work with ODT files
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// 1. Build a document from scratch
|
|
15
|
+
import { OdtDocument } from "odf-kit";
|
|
16
|
+
|
|
17
|
+
const doc = new OdtDocument();
|
|
18
|
+
doc.addHeading("Quarterly Report", 1);
|
|
19
|
+
doc.addParagraph("Revenue exceeded expectations.");
|
|
20
|
+
doc.addTable([
|
|
21
|
+
["Division", "Q4 Revenue", "Growth"],
|
|
22
|
+
["North", "$2.1M", "+12%"],
|
|
23
|
+
["South", "$1.8M", "+8%"],
|
|
24
|
+
]);
|
|
25
|
+
const bytes = await doc.save();
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// 2. Fill an existing .odt template with data
|
|
30
|
+
import { fillTemplate } from "odf-kit";
|
|
31
|
+
|
|
32
|
+
const template = readFileSync("invoice-template.odt");
|
|
33
|
+
const result = fillTemplate(template, {
|
|
34
|
+
customer: "Acme Corp",
|
|
35
|
+
date: "2026-03-19",
|
|
36
|
+
items: [
|
|
37
|
+
{ product: "Widget", qty: 5, price: "$125" },
|
|
38
|
+
{ product: "Gadget", qty: 3, price: "$120" },
|
|
39
|
+
],
|
|
40
|
+
showNotes: true,
|
|
41
|
+
notes: "Net 30",
|
|
42
|
+
});
|
|
43
|
+
writeFileSync("invoice.odt", result);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// 3. Read an existing .odt file
|
|
48
|
+
import { readOdt, odtToHtml } from "odf-kit/reader";
|
|
49
|
+
|
|
50
|
+
const bytes = readFileSync("report.odt");
|
|
51
|
+
const model = readOdt(bytes); // structured document model
|
|
52
|
+
const html = odtToHtml(bytes); // styled HTML string
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// 4. Convert .odt to Typst for PDF generation
|
|
57
|
+
import { odtToTypst } from "odf-kit/typst";
|
|
58
|
+
import { execSync } from "child_process";
|
|
59
|
+
|
|
60
|
+
const typst = odtToTypst(readFileSync("letter.odt"));
|
|
61
|
+
writeFileSync("letter.typ", typst);
|
|
62
|
+
execSync("typst compile letter.typ letter.pdf");
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install odf-kit
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Node.js 22+ required. ESM only. Three sub-exports:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { OdtDocument, fillTemplate } from "odf-kit"; // build + fill
|
|
77
|
+
import { readOdt, odtToHtml } from "odf-kit/reader"; // read + convert to HTML
|
|
78
|
+
import { odtToTypst, modelToTypst } from "odf-kit/typst"; // convert to Typst
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Works in Node.js, browsers, Deno, Bun, and Cloudflare Workers. The only runtime dependency is [fflate](https://github.com/101arrowz/fflate) for ZIP packaging — no transitive dependencies.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Browser usage
|
|
86
|
+
|
|
87
|
+
odf-kit generates and reads documents entirely client-side. No server required.
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
import { OdtDocument } from "odf-kit";
|
|
91
|
+
|
|
92
|
+
const doc = new OdtDocument();
|
|
93
|
+
doc.addHeading("Generated in the Browser", 1);
|
|
94
|
+
doc.addParagraph("Created without any server.");
|
|
95
|
+
|
|
96
|
+
const bytes = await doc.save();
|
|
97
|
+
const blob = new Blob([bytes], { type: "application/vnd.oasis.opendocument.text" });
|
|
98
|
+
const url = URL.createObjectURL(blob);
|
|
99
|
+
const a = document.createElement("a");
|
|
100
|
+
a.href = url;
|
|
101
|
+
a.download = "document.odt";
|
|
102
|
+
a.click();
|
|
103
|
+
URL.revokeObjectURL(url);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Template filling and reading work the same way — pass `Uint8Array` bytes from a `<input type="file">` or `fetch()`.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Build: programmatic document creation
|
|
111
|
+
|
|
112
|
+
### Text and formatting
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
doc.addHeading("Chapter 1", 1);
|
|
116
|
+
|
|
117
|
+
doc.addParagraph((p) => {
|
|
118
|
+
p.addText("This is ");
|
|
119
|
+
p.addText("bold", { bold: true });
|
|
120
|
+
p.addText(", ");
|
|
121
|
+
p.addText("italic", { italic: true });
|
|
122
|
+
p.addText(", and ");
|
|
123
|
+
p.addText("red", { color: "red", fontSize: 16 });
|
|
124
|
+
p.addText(".");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Scientific notation
|
|
128
|
+
doc.addParagraph((p) => {
|
|
129
|
+
p.addText("H");
|
|
130
|
+
p.addText("2", { subscript: true });
|
|
131
|
+
p.addText("O is ");
|
|
132
|
+
p.addText("essential", { underline: true, highlightColor: "yellow" });
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Tables
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Simple
|
|
140
|
+
doc.addTable([
|
|
141
|
+
["Name", "Age", "City"],
|
|
142
|
+
["Alice", "30", "Portland"],
|
|
143
|
+
["Bob", "25", "Seattle"],
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
// With column widths and borders
|
|
147
|
+
doc.addTable([
|
|
148
|
+
["Product", "Price"],
|
|
149
|
+
["Widget", "$9.99"],
|
|
150
|
+
], { columnWidths: ["8cm", "4cm"], border: "0.5pt solid #000000" });
|
|
151
|
+
|
|
152
|
+
// Full control — builder callback
|
|
153
|
+
doc.addTable((t) => {
|
|
154
|
+
t.addRow((r) => {
|
|
155
|
+
r.addCell("Name", { bold: true, backgroundColor: "#DDDDDD" });
|
|
156
|
+
r.addCell("Status", { bold: true, backgroundColor: "#DDDDDD" });
|
|
157
|
+
});
|
|
158
|
+
t.addRow((r) => {
|
|
159
|
+
r.addCell((c) => { c.addText("Project Alpha", { bold: true }); });
|
|
160
|
+
r.addCell("Complete", { color: "green" });
|
|
161
|
+
});
|
|
162
|
+
}, { columnWidths: ["8cm", "4cm"] });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Page layout, headers, footers
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
doc.setPageLayout({
|
|
169
|
+
orientation: "landscape",
|
|
170
|
+
marginTop: "1.5cm",
|
|
171
|
+
marginBottom: "1.5cm",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
doc.setHeader((h) => {
|
|
175
|
+
h.addText("Confidential", { bold: true, color: "gray" });
|
|
176
|
+
h.addText(" — Page ");
|
|
177
|
+
h.addPageNumber();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
doc.setFooter("© 2026 Acme Corp — Page ###"); // ### = page number
|
|
181
|
+
|
|
182
|
+
doc.addPageBreak();
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Lists
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
doc.addList(["Apples", "Bananas", "Cherries"]);
|
|
189
|
+
doc.addList(["First", "Second", "Third"], { type: "numbered" });
|
|
190
|
+
|
|
191
|
+
// Nested with formatting
|
|
192
|
+
doc.addList((l) => {
|
|
193
|
+
l.addItem((p) => {
|
|
194
|
+
p.addText("Important: ", { bold: true });
|
|
195
|
+
p.addText("read the docs");
|
|
196
|
+
});
|
|
197
|
+
l.addItem("Main topic");
|
|
198
|
+
l.addNested((sub) => {
|
|
199
|
+
sub.addItem("Subtopic A");
|
|
200
|
+
sub.addItem("Subtopic B");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Images
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { readFile } from "fs/promises";
|
|
209
|
+
|
|
210
|
+
const logo = await readFile("logo.png");
|
|
211
|
+
|
|
212
|
+
doc.addImage(logo, { width: "10cm", height: "6cm", mimeType: "image/png" });
|
|
213
|
+
|
|
214
|
+
// Inline image inside a paragraph
|
|
215
|
+
doc.addParagraph((p) => {
|
|
216
|
+
p.addText("Logo: ");
|
|
217
|
+
p.addImage(logo, { width: "2cm", height: "1cm", mimeType: "image/png" });
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
In a browser, use `fetch()` or a file input instead of `readFile()`:
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
const response = await fetch("logo.png");
|
|
225
|
+
const logo = new Uint8Array(await response.arrayBuffer());
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Links and bookmarks
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
doc.addParagraph((p) => {
|
|
232
|
+
p.addBookmark("introduction");
|
|
233
|
+
p.addText("Welcome to the guide.");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
doc.addParagraph((p) => {
|
|
237
|
+
p.addLink("our website", "https://example.com", { bold: true });
|
|
238
|
+
p.addText(" or go back to the ");
|
|
239
|
+
p.addLink("introduction", "#introduction");
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Tab stops
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
doc.addParagraph((p) => {
|
|
247
|
+
p.addText("Item"); p.addTab();
|
|
248
|
+
p.addText("Qty"); p.addTab();
|
|
249
|
+
p.addText("$100.00");
|
|
250
|
+
}, {
|
|
251
|
+
tabStops: [
|
|
252
|
+
{ position: "6cm" },
|
|
253
|
+
{ position: "12cm", type: "right" },
|
|
254
|
+
],
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Method chaining
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const bytes = await new OdtDocument()
|
|
262
|
+
.setMetadata({ title: "Report" })
|
|
263
|
+
.setPageLayout({ orientation: "landscape" })
|
|
264
|
+
.setHeader("Confidential")
|
|
265
|
+
.setFooter("Page ###")
|
|
266
|
+
.addHeading("Summary", 1)
|
|
267
|
+
.addParagraph("All systems operational.")
|
|
268
|
+
.addTable([["System", "Status"], ["API", "OK"], ["DB", "OK"]])
|
|
269
|
+
.save();
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Fill: template engine
|
|
275
|
+
|
|
276
|
+
Create a `.odt` template in LibreOffice with `{placeholders}`, then fill it programmatically.
|
|
277
|
+
|
|
278
|
+
### Simple replacement
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
Dear {name},
|
|
282
|
+
|
|
283
|
+
Your order #{orderNumber} has shipped to {address}.
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Dot notation
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
Company: {company.name}
|
|
290
|
+
City: {company.address.city}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Loops
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
{#items}
|
|
297
|
+
Product: {product} — Qty: {qty} — Price: {price}
|
|
298
|
+
{/items}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
fillTemplate(template, {
|
|
303
|
+
items: [
|
|
304
|
+
{ product: "Widget", qty: 5, price: "$125" },
|
|
305
|
+
{ product: "Gadget", qty: 3, price: "$120" },
|
|
306
|
+
],
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Conditionals
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
{#showDiscount}
|
|
314
|
+
You qualify for a {percent}% discount!
|
|
315
|
+
{/showDiscount}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Falsy values (`false`, `null`, `undefined`, `0`, `""`, `[]`) remove the block. Truthy values include it. Loops and conditionals nest freely.
|
|
319
|
+
|
|
320
|
+
### How it works
|
|
321
|
+
|
|
322
|
+
LibreOffice often fragments typed text like `{name}` across multiple XML elements due to editing history or spell check. odf-kit handles this automatically with a two-pass pipeline: first it reassembles fragmented placeholders, then replaces them with data. Headers and footers in `styles.xml` are processed alongside the document body.
|
|
323
|
+
|
|
324
|
+
Template syntax follows [Mustache](https://mustache.github.io/) conventions, established for document templating by [docxtemplater](https://docxtemplater.com/). odf-kit's engine is a clean-room implementation built for ODF — no code from either project was used.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Read: ODT document model
|
|
329
|
+
|
|
330
|
+
`odf-kit/reader` parses `.odt` files into a structured model and renders to HTML.
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { readOdt, odtToHtml } from "odf-kit/reader";
|
|
334
|
+
import { readFileSync } from "fs";
|
|
335
|
+
|
|
336
|
+
const bytes = readFileSync("report.odt");
|
|
337
|
+
|
|
338
|
+
// Structured model
|
|
339
|
+
const model = readOdt(bytes);
|
|
340
|
+
console.log(model.body); // BodyNode[]
|
|
341
|
+
console.log(model.pageLayout); // PageLayout
|
|
342
|
+
console.log(model.header); // HeaderFooterContent
|
|
343
|
+
|
|
344
|
+
// Styled HTML
|
|
345
|
+
const html = odtToHtml(bytes);
|
|
346
|
+
|
|
347
|
+
// With tracked changes mode
|
|
348
|
+
const final = odtToHtml(bytes, {}, { trackedChanges: "final" });
|
|
349
|
+
const original = odtToHtml(bytes, {}, { trackedChanges: "original" });
|
|
350
|
+
const marked = odtToHtml(bytes, {}, { trackedChanges: "changes" });
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### What the reader extracts
|
|
354
|
+
|
|
355
|
+
**Tier 1 — Structure:** paragraphs, headings, tables, lists, images, notes, bookmarks, fields, hyperlinks, tracked changes (all three ODF-defined modes: final/original/changes).
|
|
356
|
+
|
|
357
|
+
**Tier 2 — Styling:** span styles (bold, italic, font, color, highlight, underline, strikethrough, superscript, subscript), image float/wrap mode, footnotes/endnotes, cell and row background colors, style inheritance and resolution.
|
|
358
|
+
|
|
359
|
+
**Tier 3 — Layout:** paragraph styles (alignment, margins, padding, line height), table column widths, page geometry (size, margins, orientation), headers and footers (all four zones: default, first page, left/right), sections, tracked change metadata (author, date).
|
|
360
|
+
|
|
361
|
+
### Document model types
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
import type {
|
|
365
|
+
OdtDocumentModel,
|
|
366
|
+
BodyNode, // ParagraphNode | HeadingNode | TableNode | ListNode |
|
|
367
|
+
// ImageNode | SectionNode | TrackedChangeNode
|
|
368
|
+
ParagraphNode,
|
|
369
|
+
HeadingNode,
|
|
370
|
+
TableNode,
|
|
371
|
+
ListNode,
|
|
372
|
+
ImageNode,
|
|
373
|
+
SectionNode,
|
|
374
|
+
TrackedChangeNode,
|
|
375
|
+
InlineNode, // TextNode | SpanNode | ImageNode | NoteNode |
|
|
376
|
+
// BookmarkNode | FieldNode | LinkNode
|
|
377
|
+
PageLayout,
|
|
378
|
+
ReadOdtOptions,
|
|
379
|
+
} from "odf-kit/reader";
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Typst: ODT to PDF
|
|
385
|
+
|
|
386
|
+
`odf-kit/typst` converts `.odt` files to [Typst](https://typst.app/) markup for PDF generation. No LibreOffice, no headless browser — just the Typst CLI.
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import { odtToTypst, modelToTypst } from "odf-kit/typst";
|
|
390
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
391
|
+
import { execSync } from "child_process";
|
|
392
|
+
|
|
393
|
+
// Convenience wrapper — ODT bytes → Typst string
|
|
394
|
+
const typst = odtToTypst(readFileSync("letter.odt"));
|
|
395
|
+
writeFileSync("letter.typ", typst);
|
|
396
|
+
execSync("typst compile letter.typ letter.pdf");
|
|
397
|
+
|
|
398
|
+
// From a model (if you already have one from readOdt)
|
|
399
|
+
import { readOdt } from "odf-kit/reader";
|
|
400
|
+
const model = readOdt(readFileSync("letter.odt"));
|
|
401
|
+
const typst2 = modelToTypst(model);
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Both functions return a plain string — no filesystem access, no CLI dependency, no side effects. You control how the `.typ` file is compiled. Works in any JavaScript environment including browsers.
|
|
405
|
+
|
|
406
|
+
### Tracked changes in Typst output
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import type { TypstEmitOptions } from "odf-kit/typst";
|
|
410
|
+
|
|
411
|
+
const options: TypstEmitOptions = { trackedChanges: "final" }; // accepted text only
|
|
412
|
+
const options2: TypstEmitOptions = { trackedChanges: "original" }; // before changes
|
|
413
|
+
const options3: TypstEmitOptions = { trackedChanges: "changes" }; // annotated markup
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
See the [complete ODT to PDF with Typst guide](https://githubnewbie0.github.io/odf-kit/guides/odt-to-typst-pdf.html) for installation, font setup, and real-world examples.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## API Reference
|
|
421
|
+
|
|
422
|
+
### OdtDocument
|
|
423
|
+
|
|
424
|
+
| Method | Description |
|
|
425
|
+
|--------|-------------|
|
|
426
|
+
| `setMetadata(options)` | Set title, creator, description |
|
|
427
|
+
| `setPageLayout(options)` | Set page size, margins, orientation |
|
|
428
|
+
| `setHeader(content)` | Set page header (string or builder) |
|
|
429
|
+
| `setFooter(content)` | Set page footer (string or builder) |
|
|
430
|
+
| `addHeading(content, level?)` | Add heading (level 1–6) |
|
|
431
|
+
| `addParagraph(content, options?)` | Add paragraph (string or builder) |
|
|
432
|
+
| `addTable(content, options?)` | Add table (string[][] or builder) |
|
|
433
|
+
| `addList(content, options?)` | Add list (string[] or builder) |
|
|
434
|
+
| `addImage(data, options)` | Add standalone image |
|
|
435
|
+
| `addPageBreak()` | Insert page break |
|
|
436
|
+
| `save()` | Generate `.odt` as `Promise<Uint8Array>` |
|
|
437
|
+
|
|
438
|
+
### fillTemplate
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
function fillTemplate(templateBytes: Uint8Array, data: TemplateData): Uint8Array
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
`TemplateData` is `Record<string, unknown>` — any JSON-serializable value.
|
|
445
|
+
|
|
446
|
+
| Syntax | Description |
|
|
447
|
+
|--------|-------------|
|
|
448
|
+
| `{tag}` | Replace with value |
|
|
449
|
+
| `{object.property}` | Dot notation for nested objects |
|
|
450
|
+
| `{#tag}...{/tag}` | Loop (array) or conditional (truthy/falsy) |
|
|
451
|
+
|
|
452
|
+
### readOdt / odtToHtml
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
function readOdt(bytes: Uint8Array, options?: ReadOdtOptions): OdtDocumentModel
|
|
456
|
+
function odtToHtml(
|
|
457
|
+
bytes: Uint8Array,
|
|
458
|
+
htmlOptions?: HtmlOptions,
|
|
459
|
+
readOptions?: ReadOdtOptions
|
|
460
|
+
): string
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### odtToTypst / modelToTypst
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
function odtToTypst(bytes: Uint8Array, options?: TypstEmitOptions): string
|
|
467
|
+
function modelToTypst(model: OdtDocumentModel, options?: TypstEmitOptions): string
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### TextFormatting
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
{
|
|
474
|
+
bold?: boolean,
|
|
475
|
+
italic?: boolean,
|
|
476
|
+
fontSize?: number | string, // 12 or "12pt"
|
|
477
|
+
fontFamily?: string,
|
|
478
|
+
color?: string, // "#FF0000" or "red"
|
|
479
|
+
underline?: boolean,
|
|
480
|
+
strikethrough?: boolean,
|
|
481
|
+
superscript?: boolean,
|
|
482
|
+
subscript?: boolean,
|
|
483
|
+
highlightColor?: string,
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### TableOptions / CellOptions
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
// TableOptions
|
|
491
|
+
{ columnWidths?: string[], border?: string }
|
|
492
|
+
|
|
493
|
+
// CellOptions (extends TextFormatting)
|
|
494
|
+
{
|
|
495
|
+
backgroundColor?: string,
|
|
496
|
+
border?: string,
|
|
497
|
+
borderTop?: string, borderBottom?: string,
|
|
498
|
+
borderLeft?: string, borderRight?: string,
|
|
499
|
+
colSpan?: number,
|
|
500
|
+
rowSpan?: number,
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### PageLayout
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
{
|
|
508
|
+
width?: string, // "21cm" (A4 default)
|
|
509
|
+
height?: string, // "29.7cm"
|
|
510
|
+
orientation?: "portrait" | "landscape",
|
|
511
|
+
marginTop?: string, // "2cm" default
|
|
512
|
+
marginBottom?: string,
|
|
513
|
+
marginLeft?: string,
|
|
514
|
+
marginRight?: string,
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## Platform support
|
|
521
|
+
|
|
522
|
+
| Platform | Support |
|
|
523
|
+
|----------|---------|
|
|
524
|
+
| Node.js 22+ | ✅ Full |
|
|
525
|
+
| Chrome, Firefox, Safari, Edge | ✅ Full |
|
|
526
|
+
| Deno, Bun | ✅ Full |
|
|
527
|
+
| Cloudflare Workers | ✅ Full |
|
|
528
|
+
|
|
529
|
+
ESM only. Zero Node-specific APIs in the library source — enforced at the TypeScript level, guaranteeing cross-platform compatibility.
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Why odf-kit?
|
|
534
|
+
|
|
535
|
+
**ODF is the ISO standard (ISO/IEC 26300) for documents.** It's the default format for LibreOffice, mandatory for many governments and public sector organisations, and the best choice for long-term document preservation.
|
|
536
|
+
|
|
537
|
+
- **Single runtime dependency** — fflate for ZIP. No transitive dependencies.
|
|
538
|
+
- **Spec-compliant output** — every generated file passes the OASIS ODF validator. Enforced on every commit by CI.
|
|
539
|
+
- **Four complete capability modes** — build, fill, read, convert. Not just generation.
|
|
540
|
+
- **Zero-dependency Typst emitter** — the only JavaScript library with built-in ODT→Typst conversion for PDF generation.
|
|
541
|
+
- **TypeScript-first** — full types across all three sub-exports.
|
|
542
|
+
- **Apache 2.0** — use freely in commercial and open source projects.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## Comparison
|
|
547
|
+
|
|
548
|
+
| Feature | odf-kit | simple-odf | docxtemplater |
|
|
549
|
+
|---------|---------|------------|---------------|
|
|
550
|
+
| Generate .odt from scratch | ✅ | ⚠️ flat XML only | ❌ |
|
|
551
|
+
| Fill .odt templates | ✅ | ❌ | ✅ .docx only |
|
|
552
|
+
| Read .odt files | ✅ | ❌ | ❌ |
|
|
553
|
+
| Convert to HTML | ✅ | ❌ | ❌ |
|
|
554
|
+
| Convert to Typst / PDF | ✅ | ❌ | ❌ |
|
|
555
|
+
| Browser support | ✅ | ❌ | ✅ |
|
|
556
|
+
| Maintained | ✅ | ❌ abandoned 2021 | ✅ |
|
|
557
|
+
| Open source | ✅ Apache 2.0 | ✅ MIT | ⚠️ paid for advanced features |
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## Specification compliance
|
|
562
|
+
|
|
563
|
+
odf-kit targets ODF 1.2 (ISO/IEC 26300). Generated files include proper ZIP packaging (mimetype stored uncompressed as the first entry per spec), manifest, metadata, and all required namespace declarations. The OASIS ODF validator runs on every push via GitHub Actions.
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## Version history
|
|
568
|
+
|
|
569
|
+
**v0.8.0** — `odf-kit/typst` sub-export: `odtToTypst()` and `modelToTypst()`. Zero-dependency ODT→Typst emitter for PDF generation via Typst CLI. 650+ tests passing.
|
|
570
|
+
|
|
571
|
+
**v0.7.0** — Tier 3 reader: paragraph styles, page geometry, headers/footers (all four zones), sections, tracked changes (all three ODF modes). `SectionNode`, `TrackedChangeNode` added to `BodyNode` union.
|
|
572
|
+
|
|
573
|
+
**v0.6.0** — Tier 2 reader: span styles, image float/wrap, footnotes/endnotes, bookmarks, fields, cell/row styles, full style inheritance.
|
|
574
|
+
|
|
575
|
+
**v0.5.0** — `odf-kit/reader` sub-export: `readOdt()`, `odtToHtml()`. Tier 1: paragraphs, headings, tables, lists, images, notes, tracked changes.
|
|
576
|
+
|
|
577
|
+
**v0.4.0** — Generation repair: 16 spec compliance gaps fixed, OASIS ODF validator added to CI.
|
|
578
|
+
|
|
579
|
+
**v0.3.0** — Template engine: loops, conditionals, dot notation, automatic XML fragment healing.
|
|
580
|
+
|
|
581
|
+
**v0.2.0** — Migrated to fflate (zero transitive dependencies).
|
|
582
|
+
|
|
583
|
+
**v0.1.0** — Programmatic ODT creation: text, tables, page layout, lists, images, links, bookmarks.
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## Guides
|
|
588
|
+
|
|
589
|
+
Full walkthroughs and real-world examples on the documentation site:
|
|
590
|
+
|
|
591
|
+
- [Generate ODT files in Node.js](https://githubnewbie0.github.io/odf-kit/guides/generate-odt-nodejs.html)
|
|
592
|
+
- [Generate ODT files in the browser](https://githubnewbie0.github.io/odf-kit/guides/generate-odt-browser.html)
|
|
593
|
+
- [Fill ODT templates in JavaScript](https://githubnewbie0.github.io/odf-kit/guides/fill-odt-template-javascript.html)
|
|
594
|
+
- [Convert ODT to HTML in JavaScript](https://githubnewbie0.github.io/odf-kit/guides/odt-to-html-javascript.html)
|
|
595
|
+
- [ODT to PDF via Typst](https://githubnewbie0.github.io/odf-kit/guides/odt-to-typst-pdf.html)
|
|
596
|
+
- [Generate ODT without LibreOffice](https://githubnewbie0.github.io/odf-kit/guides/generate-odt-without-libreoffice.html)
|
|
597
|
+
- [ODF government compliance](https://githubnewbie0.github.io/odf-kit/guides/odf-government-compliance.html)
|
|
598
|
+
- [simple-odf alternative](https://githubnewbie0.github.io/odf-kit/guides/simple-odf-alternative.html)
|
|
599
|
+
- [docxtemplater alternative for ODF](https://githubnewbie0.github.io/odf-kit/guides/docxtemplater-odf-alternative.html)
|
|
600
|
+
- [ODT JavaScript ecosystem](https://githubnewbie0.github.io/odf-kit/guides/odt-javascript-ecosystem.html)
|
|
601
|
+
- [Free ODT to HTML converter (online tool)](https://githubnewbie0.github.io/odf-kit/tools/odt-to-html.html)
|
|
602
|
+
- [Free ODT to PDF converter (online tool)](https://githubnewbie0.github.io/odf-kit/tools/odt-to-pdf.html)
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## Contributing
|
|
607
|
+
|
|
608
|
+
Issues and pull requests welcome at [github.com/GitHubNewbie0/odf-kit](https://github.com/GitHubNewbie0/odf-kit).
|
|
609
|
+
|
|
610
|
+
```bash
|
|
611
|
+
git clone https://github.com/GitHubNewbie0/odf-kit.git
|
|
612
|
+
cd odf-kit
|
|
613
|
+
npm install
|
|
614
|
+
npm run build
|
|
615
|
+
npm test
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
Full pipeline before submitting a PR:
|
|
619
|
+
|
|
620
|
+
```bash
|
|
621
|
+
npm run format:check
|
|
622
|
+
npm run lint
|
|
623
|
+
npm run build
|
|
624
|
+
npm test
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
## Acknowledgments
|
|
630
|
+
|
|
631
|
+
Template syntax follows [Mustache](https://mustache.github.io/) conventions, established for document templating by [docxtemplater](https://docxtemplater.com/). odf-kit's engine is a clean-room implementation purpose-built for ODF — no code from either project was used.
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## License
|
|
636
|
+
|
|
637
|
+
Apache 2.0 — see [LICENSE](LICENSE) for details.
|