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 CHANGED
@@ -1,587 +1,637 @@
1
- # odf-kit
2
-
3
- Create and fill OpenDocument Format files (.odt) in TypeScript/JavaScript. Build documents from scratch with a clean API, or fill existing templates with data. Works in Node.js and browsers. No LibreOffice dependency — just spec-compliant ODF files.
4
-
5
- **Two ways to generate documents:**
6
-
7
- ```typescript
8
- // 1. Build from scratch
9
- import { OdtDocument } from "odf-kit";
10
-
11
- const doc = new OdtDocument();
12
- doc.addHeading("Quarterly Report", 1);
13
- doc.addParagraph("Revenue exceeded expectations across all divisions.");
14
- doc.addTable([
15
- ["Division", "Q4 Revenue", "Growth"],
16
- ["North", "$2.1M", "+12%"],
17
- ["South", "$1.8M", "+8%"],
18
- ["West", "$3.2M", "+15%"],
19
- ], { border: "0.5pt solid #000000" });
20
-
21
- const bytes = await doc.save();
22
- ```
23
-
24
- ```typescript
25
- // 2. Fill an existing template
26
- import { fillTemplate } from "odf-kit";
27
- import { readFileSync, writeFileSync } from "fs";
28
-
29
- const template = readFileSync("invoice-template.odt");
30
- const result = fillTemplate(template, {
31
- customer: "Acme Corp",
32
- date: "2026-02-23",
33
- items: [
34
- { product: "Widget", qty: 5, price: "$125" },
35
- { product: "Gadget", qty: 3, price: "$120" },
36
- ],
37
- showNotes: true,
38
- notes: "Net 30",
39
- });
40
- writeFileSync("invoice.odt", result);
41
- ```
42
-
43
- Generated `.odt` files open in LibreOffice, Apache OpenOffice, OnlyOffice, Collabora, Google Docs, Microsoft Office, and any ODF-compliant application.
44
-
45
- ## Why odf-kit?
46
-
47
- **ODF is the ISO standard (ISO/IEC 26300) for documents.** It's the default format for LibreOffice, the standard required by many governments, and the best choice for long-term document preservation. Until now, JavaScript developers had no maintained library for generating proper ODF files.
48
-
49
- odf-kit fills that gap with a single runtime dependency, full TypeScript types, and an API designed to feel familiar to anyone who has used a document-generation library.
50
-
51
- ## Installation
52
-
53
- ```bash
54
- npm install odf-kit
55
- ```
56
-
57
- Works in Node.js 18+ and modern browsers. ESM only.
58
-
59
- **Browser** — use any bundler (Vite, webpack, esbuild, Rollup):
60
-
61
- ```bash
62
- npm install odf-kit
63
- ```
64
-
65
- ```javascript
66
- import { OdtDocument } from "odf-kit";
67
- ```
68
-
69
- Your bundler resolves everything automatically — no extra configuration needed.
70
-
71
- ## Browser Usage
72
-
73
- odf-kit generates documents entirely client-side. No server required user data never leaves the browser.
74
-
75
- ```javascript
76
- import { OdtDocument } from "odf-kit";
77
-
78
- const doc = new OdtDocument();
79
- doc.addHeading("Generated in the Browser", 1);
80
- doc.addParagraph("This document was created without any server.");
81
-
82
- const bytes = await doc.save();
83
-
84
- // Trigger download
85
- const blob = new Blob([bytes], {
86
- type: "application/vnd.oasis.opendocument.text",
87
- });
88
- const url = URL.createObjectURL(blob);
89
- const a = document.createElement("a");
90
- a.href = url;
91
- a.download = "document.odt";
92
- a.click();
93
- URL.revokeObjectURL(url);
94
- ```
95
-
96
- Template filling works the same way — pass template bytes from a `<input type="file">` or `fetch()`:
97
-
98
- ```javascript
99
- import { fillTemplate } from "odf-kit";
100
-
101
- // From a file input
102
- const file = document.getElementById("template-input").files[0];
103
- const templateBytes = new Uint8Array(await file.arrayBuffer());
104
-
105
- const result = fillTemplate(templateBytes, {
106
- name: "Alice",
107
- company: "Acme Corp",
108
- });
109
-
110
- // Download the filled document
111
- const blob = new Blob([result], {
112
- type: "application/vnd.oasis.opendocument.text",
113
- });
114
- const url = URL.createObjectURL(blob);
115
- const a = document.createElement("a");
116
- a.href = url;
117
- a.download = "filled.odt";
118
- a.click();
119
- URL.revokeObjectURL(url);
120
- ```
121
-
122
- ## Features
123
-
124
- - **Template engine** — fill existing `.odt` templates with `{placeholders}`, loops, conditionals, and nested data
125
- - **Text** — paragraphs, headings (levels 1–6), bold, italic, underline, strikethrough, superscript, subscript, font size, font family, text color, highlight color
126
- - **Tables** — column widths, cell borders (per-table, per-cell, per-side), background colors, cell merging (colspan/rowspan), rich text in cells
127
- - **Page layout** — page size, margins, orientation, headers, footers, page numbers, page breaks
128
- - **Lists** — bullet lists, numbered lists, nesting up to 6 levels, formatted items
129
- - **Images** — embedded PNG/JPEG/GIF/SVG/WebP/BMP/TIFF, standalone or inline, configurable sizing and anchoring
130
- - **Links** — external hyperlinks, internal bookmark links, formatted link text
131
- - **Bookmarks** — named anchor points for internal navigation
132
- - **Tab stops** — left, center, right alignment with configurable positions
133
-
134
- ## Template Engine
135
-
136
- Create a `.odt` template in LibreOffice (or any ODF editor) with `{placeholders}` in the text, then fill it with data:
137
-
138
- ```typescript
139
- import { fillTemplate } from "odf-kit";
140
- import { readFileSync, writeFileSync } from "fs";
141
-
142
- const template = readFileSync("template.odt");
143
- const result = fillTemplate(template, {
144
- name: "Alice",
145
- company: { name: "Acme Corp", address: "123 Main St" },
146
- });
147
- writeFileSync("output.odt", result);
148
- ```
149
-
150
- ### Simple replacement
151
-
152
- Use `{tag}` for simple value substitution. Values are automatically XML-escaped.
153
-
154
- ```
155
- Dear {name},
156
-
157
- Your order #{orderNumber} has been shipped to {address}.
158
- ```
159
-
160
- ### Dot notation
161
-
162
- Access nested objects with `{object.property}`:
163
-
164
- ```
165
- Company: {company.name}
166
- City: {company.address.city}
167
- ```
168
-
169
- ### Loops
170
-
171
- Use `{#tag}...{/tag}` with an array to repeat content:
172
-
173
- ```
174
- {#items}
175
- Product: {product} — Qty: {qty} — Price: {price}
176
- {/items}
177
- ```
178
-
179
- ```typescript
180
- fillTemplate(template, {
181
- items: [
182
- { product: "Widget", qty: 5, price: "$125" },
183
- { product: "Gadget", qty: 3, price: "$120" },
184
- ],
185
- });
186
- ```
187
-
188
- Loop items inherit parent data, so you can reference top-level values inside a loop. Item properties override parent properties of the same name.
189
-
190
- ### Conditionals
191
-
192
- Use `{#tag}...{/tag}` with a truthy or falsy value to include or remove content:
193
-
194
- ```
195
- {#showDiscount}
196
- You qualify for a {percent}% discount!
197
- {/showDiscount}
198
- ```
199
-
200
- ```typescript
201
- fillTemplate(template, {
202
- showDiscount: true,
203
- percent: 10,
204
- });
205
- ```
206
-
207
- Falsy values (`false`, `null`, `undefined`, `0`, `""`, `[]`) remove the section. Truthy values include it.
208
-
209
- ### Nesting
210
-
211
- Loops and conditionals nest freely:
212
-
213
- ```typescript
214
- fillTemplate(template, {
215
- departments: [
216
- {
217
- name: "Engineering",
218
- members: [
219
- { name: "Alice", isLead: true },
220
- { name: "Bob", isLead: false },
221
- ],
222
- },
223
- {
224
- name: "Design",
225
- members: [{ name: "Carol", isLead: false }],
226
- },
227
- ],
228
- });
229
- ```
230
-
231
- ### How it works
232
-
233
- LibreOffice often fragments user-typed text like `{name}` across multiple XML elements due to editing history or spell check. odf-kit's template engine handles this automatically with a two-pass pipeline: first it reassembles fragmented placeholders, then it replaces them with your data. Headers and footers in `styles.xml` are processed alongside the document body.
234
-
235
- Template syntax follows [Mustache](https://mustache.github.io/) conventions, proven in document templating by [docxtemplater](https://docxtemplater.com/). odf-kit's engine is a clean-room implementation purpose-built for ODF.
236
-
237
- ## Quick Start Programmatic Creation
238
-
239
- ### Simple document
240
-
241
- ```typescript
242
- import { OdtDocument } from "odf-kit";
243
-
244
- const doc = new OdtDocument();
245
- doc.setMetadata({ title: "My Document", creator: "Jane Doe" });
246
- doc.addHeading("Introduction", 1);
247
- doc.addParagraph("This is a simple ODF text document.");
248
- doc.addParagraph("It opens in LibreOffice, Google Docs, and Microsoft Office.");
249
-
250
- const bytes = await doc.save(); // Uint8Array — valid .odt file
251
- ```
252
-
253
- ### Formatted text
254
-
255
- ```typescript
256
- doc.addParagraph((p) => {
257
- p.addText("This is ");
258
- p.addText("bold", { bold: true });
259
- p.addText(", ");
260
- p.addText("italic", { italic: true });
261
- p.addText(", and ");
262
- p.addText("red", { color: "red", fontSize: 16 });
263
- p.addText(".");
264
- });
265
-
266
- // Scientific notation
267
- doc.addParagraph((p) => {
268
- p.addText("H");
269
- p.addText("2", { subscript: true });
270
- p.addText("O is ");
271
- p.addText("essential", { underline: true, highlightColor: "yellow" });
272
- });
273
- ```
274
-
275
- ### Tables
276
-
277
- ```typescript
278
- // Simple — array of arrays
279
- doc.addTable([
280
- ["Name", "Age", "City"],
281
- ["Alice", "30", "Portland"],
282
- ["Bob", "25", "Seattle"],
283
- ]);
284
-
285
- // With options
286
- doc.addTable([
287
- ["Product", "Price"],
288
- ["Widget", "$9.99"],
289
- ], { columnWidths: ["8cm", "4cm"], border: "0.5pt solid #000000" });
290
-
291
- // Full control — builder callback
292
- doc.addTable((t) => {
293
- t.addRow((r) => {
294
- r.addCell("Name", { bold: true, backgroundColor: "#DDDDDD" });
295
- r.addCell("Status", { bold: true, backgroundColor: "#DDDDDD" });
296
- });
297
- t.addRow((r) => {
298
- r.addCell((c) => {
299
- c.addText("Project Alpha", { bold: true });
300
- });
301
- r.addCell("Complete", { color: "green" });
302
- });
303
- }, { columnWidths: ["8cm", "4cm"] });
304
- ```
305
-
306
- ### Page layout
307
-
308
- ```typescript
309
- doc.setPageLayout({
310
- orientation: "landscape",
311
- marginTop: "1.5cm",
312
- marginBottom: "1.5cm",
313
- });
314
-
315
- doc.setHeader((h) => {
316
- h.addText("Confidential", { bold: true, color: "gray" });
317
- h.addText(" — Page ");
318
- h.addPageNumber();
319
- });
320
-
321
- doc.setFooter("© 2026 Acme Corp — Page ###"); // ### = page number
322
-
323
- doc.addHeading("Chapter 1", 1);
324
- doc.addParagraph("First chapter content.");
325
- doc.addPageBreak();
326
- doc.addHeading("Chapter 2", 1);
327
- doc.addParagraph("Second chapter content.");
328
- ```
329
-
330
- ### Lists
331
-
332
- ```typescript
333
- // Simple
334
- doc.addList(["Apples", "Bananas", "Cherries"]);
335
- doc.addList(["First", "Second", "Third"], { type: "numbered" });
336
-
337
- // Nested with formatting
338
- doc.addList((l) => {
339
- l.addItem((p) => {
340
- p.addText("Important: ", { bold: true });
341
- p.addText("read the docs");
342
- });
343
- l.addItem("Main topic");
344
- l.addNested((sub) => {
345
- sub.addItem("Subtopic A");
346
- sub.addItem("Subtopic B");
347
- });
348
- });
349
- ```
350
-
351
- ### Images
352
-
353
- ```typescript
354
- import { readFile } from "fs/promises";
355
-
356
- const logo = await readFile("logo.png");
357
-
358
- // Standalone image
359
- doc.addImage(logo, {
360
- width: "10cm",
361
- height: "6cm",
362
- mimeType: "image/png",
363
- });
364
-
365
- // Inline image in text
366
- doc.addParagraph((p) => {
367
- p.addText("Company logo: ");
368
- p.addImage(logo, { width: "2cm", height: "1cm", mimeType: "image/png" });
369
- p.addText(" — Acme Corp");
370
- });
371
- ```
372
-
373
- In a browser, get image bytes from `fetch()` or a file input instead of `readFile()`:
374
-
375
- ```javascript
376
- const response = await fetch("logo.png");
377
- const logo = new Uint8Array(await response.arrayBuffer());
378
- ```
379
-
380
- ### Links and bookmarks
381
-
382
- ```typescript
383
- doc.addParagraph((p) => {
384
- p.addBookmark("introduction");
385
- p.addText("Welcome to the guide.");
386
- });
387
-
388
- doc.addParagraph((p) => {
389
- p.addText("Visit ");
390
- p.addLink("our website", "https://example.com", { bold: true });
391
- p.addText(" or go back to the ");
392
- p.addLink("introduction", "#introduction");
393
- p.addText(".");
394
- });
395
- ```
396
-
397
- ### Tab stops
398
-
399
- ```typescript
400
- doc.addParagraph((p) => {
401
- p.addText("Item");
402
- p.addTab();
403
- p.addText("Qty");
404
- p.addTab();
405
- p.addText("$100.00");
406
- }, {
407
- tabStops: [
408
- { position: "6cm" },
409
- { position: "12cm", type: "right" },
410
- ],
411
- });
412
- ```
413
-
414
- ### Method chaining
415
-
416
- Every method returns the document, so you can chain calls:
417
-
418
- ```typescript
419
- const bytes = await new OdtDocument()
420
- .setMetadata({ title: "Report" })
421
- .setPageLayout({ orientation: "landscape" })
422
- .setHeader("Confidential")
423
- .setFooter("Page ###")
424
- .addHeading("Summary", 1)
425
- .addParagraph("All systems operational.")
426
- .addTable([["System", "Status"], ["API", "OK"], ["DB", "OK"]])
427
- .addList(["No incidents", "No alerts"], { type: "numbered" })
428
- .save();
429
- ```
430
-
431
- ## API Reference
432
-
433
- ### OdtDocument
434
-
435
- | Method | Description |
436
- |--------|-------------|
437
- | `setMetadata(options)` | Set title, creator, description |
438
- | `setPageLayout(options)` | Set page size, margins, orientation |
439
- | `setHeader(content)` | Set page header (string or builder callback) |
440
- | `setFooter(content)` | Set page footer (string or builder callback) |
441
- | `addHeading(content, level?)` | Add a heading (string or builder callback, level 1–6) |
442
- | `addParagraph(content, options?)` | Add a paragraph (string or builder callback) |
443
- | `addTable(content, options?)` | Add a table (string[][] or builder callback) |
444
- | `addList(content, options?)` | Add a list (string[] or builder callback) |
445
- | `addImage(data, options)` | Add a standalone image |
446
- | `addPageBreak()` | Insert a page break |
447
- | `save()` | Generate .odt file as `Promise<Uint8Array>` |
448
-
449
- ### fillTemplate
450
-
451
- ```typescript
452
- function fillTemplate(templateBytes: Uint8Array, data: TemplateData): Uint8Array
453
- ```
454
-
455
- | Parameter | Type | Description |
456
- |-----------|------|-------------|
457
- | `templateBytes` | `Uint8Array` | Raw bytes of a `.odt` template file |
458
- | `data` | `TemplateData` | Key-value data for placeholder replacement |
459
- | **Returns** | `Uint8Array` | A new `.odt` file with all placeholders replaced |
460
-
461
- `TemplateData` is `Record<string, unknown>` — any JSON-serializable object.
462
-
463
- **Template syntax:**
464
-
465
- | Syntax | Description |
466
- |--------|-------------|
467
- | `{tag}` | Replace with value from data |
468
- | `{object.property}` | Dot notation for nested objects |
469
- | `{#tag}...{/tag}` | Loop (array) or conditional (truthy/falsy) |
470
-
471
- ### TextFormatting
472
-
473
- Accepted by `addText()`, `addLink()`, and `addCell()`:
474
-
475
- ```typescript
476
- {
477
- bold?: boolean,
478
- italic?: boolean,
479
- fontWeight?: "normal" | "bold",
480
- fontStyle?: "normal" | "italic",
481
- fontSize?: number | string, // 12 or "12pt"
482
- fontFamily?: string, // "Arial"
483
- color?: string, // "#FF0000" or "red"
484
- underline?: boolean,
485
- strikethrough?: boolean,
486
- superscript?: boolean,
487
- subscript?: boolean,
488
- highlightColor?: string, // "#FFFF00" or "yellow"
489
- }
490
- ```
491
-
492
- `fontSize` as a number assumes points. Both `{ bold: true }` and `{ fontWeight: "bold" }` work. When both are provided, the explicit property wins.
493
-
494
- ### ImageOptions
495
-
496
- ```typescript
497
- {
498
- width: string, // "10cm", "4in" — required
499
- height: string, // "6cm", "3in" — required
500
- mimeType: string, // "image/png", "image/jpeg" — required
501
- anchor?: "as-character" | "paragraph",
502
- }
503
- ```
504
-
505
- ### TableOptions
506
-
507
- ```typescript
508
- {
509
- columnWidths?: string[], // ["5cm", "3cm"]
510
- border?: string, // "0.5pt solid #000000"
511
- }
512
- ```
513
-
514
- ### CellOptions
515
-
516
- Extends `TextFormatting` with:
517
-
518
- ```typescript
519
- {
520
- backgroundColor?: string, // "#EEEEEE" or "lightgray"
521
- border?: string,
522
- borderTop?: string,
523
- borderBottom?: string,
524
- borderLeft?: string,
525
- borderRight?: string,
526
- colSpan?: number,
527
- rowSpan?: number,
528
- }
529
- ```
530
-
531
- ### PageLayout
532
-
533
- ```typescript
534
- {
535
- width?: string, // "21cm" (default: A4)
536
- height?: string, // "29.7cm"
537
- orientation?: "portrait" | "landscape",
538
- marginTop?: string, // "2cm" (default)
539
- marginBottom?: string,
540
- marginLeft?: string,
541
- marginRight?: string,
542
- }
543
- ```
544
-
545
- ## Platform Support
546
-
547
- odf-kit works anywhere that supports ES2022 and ESM:
548
-
549
- - **Node.js** 18 and later
550
- - **Browsers** Chrome, Firefox, Safari, Edge (all modern versions)
551
- - **Deno**, **Bun**, **Cloudflare Workers**
552
-
553
- The library uses only standard JavaScript APIs (`TextEncoder`, `Uint8Array`) plus [fflate](https://github.com/101arrowz/fflate) for ZIP packaging. The TypeScript build enforces this — Node-specific APIs cannot exist in the library source code, guaranteeing cross-platform compatibility.
554
-
555
- ## Status
556
-
557
- **v0.3.0** Template engine with loops, conditionals, dot notation, and automatic placeholder healing. 222 tests passing.
558
-
559
- **v0.2.0** — Migrated to fflate (zero transitive dependencies).
560
-
561
- **v0.1.0** Complete ODT programmatic creation: text, tables, page layout, lists, images, links, bookmarks. 102 tests.
562
-
563
- ODS (spreadsheets), ODP (presentations), and ODG (drawings) are planned for future releases.
564
-
565
- ## Specification Compliance
566
-
567
- odf-kit targets ODF 1.2 (ISO/IEC 26300). Generated files include proper ZIP packaging (mimetype stored uncompressed as the first entry), manifest, metadata, and all required namespace declarations.
568
-
569
- ## Contributing
570
-
571
- Issues and pull requests are welcome at [github.com/GitHubNewbie0/odf-kit](https://github.com/GitHubNewbie0/odf-kit).
572
-
573
- ```bash
574
- git clone https://github.com/GitHubNewbie0/odf-kit.git
575
- cd odf-kit
576
- npm install
577
- npm run build
578
- npm test
579
- ```
580
-
581
- ## Acknowledgments
582
-
583
- Template syntax follows [Mustache](https://mustache.github.io/) conventions, adapted for document templating by [docxtemplater](https://docxtemplater.com/). odf-kit's template engine is a clean-room implementation purpose-built for ODF no code from either project was used. We credit both projects for establishing the patterns that make document templates intuitive.
584
-
585
- ## License
586
-
587
- Apache 2.0 — see [LICENSE](LICENSE) for details.
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.