odf-kit 0.9.6 → 0.9.8

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +306 -272
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/ods/content.d.ts +0 -9
  6. package/dist/ods/content.d.ts.map +1 -1
  7. package/dist/ods/content.js +274 -97
  8. package/dist/ods/content.js.map +1 -1
  9. package/dist/ods/document.d.ts +2 -42
  10. package/dist/ods/document.d.ts.map +1 -1
  11. package/dist/ods/document.js +7 -42
  12. package/dist/ods/document.js.map +1 -1
  13. package/dist/ods/index.d.ts +1 -1
  14. package/dist/ods/index.d.ts.map +1 -1
  15. package/dist/ods/settings.d.ts +13 -0
  16. package/dist/ods/settings.d.ts.map +1 -0
  17. package/dist/ods/settings.js +67 -0
  18. package/dist/ods/settings.js.map +1 -0
  19. package/dist/ods/sheet-builder.d.ts +42 -28
  20. package/dist/ods/sheet-builder.d.ts.map +1 -1
  21. package/dist/ods/sheet-builder.js +57 -42
  22. package/dist/ods/sheet-builder.js.map +1 -1
  23. package/dist/ods/types.d.ts +68 -30
  24. package/dist/ods/types.d.ts.map +1 -1
  25. package/dist/ods-reader/html-renderer.d.ts +19 -0
  26. package/dist/ods-reader/html-renderer.d.ts.map +1 -0
  27. package/dist/ods-reader/html-renderer.js +123 -0
  28. package/dist/ods-reader/html-renderer.js.map +1 -0
  29. package/dist/ods-reader/index.d.ts +19 -0
  30. package/dist/ods-reader/index.d.ts.map +1 -0
  31. package/dist/ods-reader/index.js +22 -0
  32. package/dist/ods-reader/index.js.map +1 -0
  33. package/dist/ods-reader/parser.d.ts +24 -0
  34. package/dist/ods-reader/parser.d.ts.map +1 -0
  35. package/dist/ods-reader/parser.js +544 -0
  36. package/dist/ods-reader/parser.js.map +1 -0
  37. package/dist/ods-reader/types.d.ts +139 -0
  38. package/dist/ods-reader/types.d.ts.map +1 -0
  39. package/dist/ods-reader/types.js +7 -0
  40. package/dist/ods-reader/types.js.map +1 -0
  41. package/package.json +22 -5
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # odf-kit
2
2
 
3
- Generate, fill, read, and convert OpenDocument Format files (.odt, .ods) in TypeScript and JavaScript. Convert HTML to ODT. Works in Node.js and browsers. No LibreOffice dependency — pure spec-compliant ODF.
3
+ Generate, fill, read, and convert OpenDocument Format files (.odt, .ods) in TypeScript and JavaScript. Convert HTML, Markdown, and TipTap JSON to ODT. Works in Node.js and browsers. No LibreOffice dependency — pure spec-compliant ODF.
4
4
 
5
5
  **[Documentation & examples →](https://githubnewbie0.github.io/odf-kit/)**
6
6
 
@@ -8,7 +8,7 @@ Generate, fill, read, and convert OpenDocument Format files (.odt, .ods) in Type
8
8
  npm install odf-kit
9
9
  ```
10
10
 
11
- ## Six ways to work with ODF files
11
+ ## Nine ways to work with ODF files
12
12
 
13
13
  ```typescript
14
14
  // 1. Build an ODT document from scratch
@@ -41,7 +41,43 @@ const bytes = await htmlToOdt(html, { pageFormat: "A4" });
41
41
  ```
42
42
 
43
43
  ```typescript
44
- // 3. Build an ODS spreadsheet from scratch
44
+ // 3. Convert Markdown to ODT
45
+ import { markdownToOdt } from "odf-kit";
46
+
47
+ const markdown = `
48
+ # Meeting Notes
49
+
50
+ Attendees: **Alice**, Bob, Carol
51
+
52
+ ## Action Items
53
+
54
+ - Send report by Friday
55
+ - Review budget on Monday
56
+ `;
57
+ const bytes = await markdownToOdt(markdown, { pageFormat: "A4" });
58
+ ```
59
+
60
+ ```typescript
61
+ // 4. Convert TipTap/ProseMirror JSON to ODT
62
+ import { tiptapToOdt } from "odf-kit";
63
+
64
+ // editor.getJSON() returns TipTap JSONContent
65
+ const bytes = await tiptapToOdt(editor.getJSON(), { pageFormat: "A4" });
66
+
67
+ // With pre-fetched images (e.g. from IPFS or S3)
68
+ const images = { [imageUrl]: await fetchImageBytes(imageUrl) };
69
+ const bytes2 = await tiptapToOdt(editor.getJSON(), { images });
70
+
71
+ // With custom node handler for app-specific extensions
72
+ const bytes3 = await tiptapToOdt(editor.getJSON(), {
73
+ unknownNodeHandler: (node, doc) => {
74
+ if (node.type === "callout") doc.addParagraph(`⚠️ ${extractText(node)}`);
75
+ },
76
+ });
77
+ ```
78
+
79
+ ```typescript
80
+ // 5. Build an ODS spreadsheet from scratch
45
81
  import { OdsDocument } from "odf-kit";
46
82
 
47
83
  const doc = new OdsDocument();
@@ -56,7 +92,7 @@ const bytes = await doc.save();
56
92
  ```
57
93
 
58
94
  ```typescript
59
- // 4. Fill an existing .odt template with data
95
+ // 6. Fill an existing .odt template with data
60
96
  import { fillTemplate } from "odf-kit";
61
97
 
62
98
  const template = readFileSync("invoice-template.odt");
@@ -74,7 +110,7 @@ writeFileSync("invoice.odt", result);
74
110
  ```
75
111
 
76
112
  ```typescript
77
- // 5. Read an existing .odt file
113
+ // 7. Read an existing .odt file
78
114
  import { readOdt, odtToHtml } from "odf-kit/reader";
79
115
 
80
116
  const bytes = readFileSync("report.odt");
@@ -83,7 +119,16 @@ const html = odtToHtml(bytes); // styled HTML string
83
119
  ```
84
120
 
85
121
  ```typescript
86
- // 6. Convert .odt to Typst for PDF generation
122
+ // 8. Read an existing .ods spreadsheet
123
+ import { readOds, odsToHtml } from "odf-kit/ods-reader";
124
+
125
+ const bytes = readFileSync("data.ods");
126
+ const model = readOds(bytes); // structured model — typed values
127
+ const html = odsToHtml(bytes); // HTML table string
128
+ ```
129
+
130
+ ```typescript
131
+ // 9. Convert .odt to Typst for PDF generation
87
132
  import { odtToTypst } from "odf-kit/typst";
88
133
  import { execSync } from "child_process";
89
134
 
@@ -103,12 +148,13 @@ npm install odf-kit
103
148
  Node.js 22+ required. ESM only. Sub-exports:
104
149
 
105
150
  ```typescript
106
- import { OdtDocument, OdsDocument, htmlToOdt, fillTemplate } from "odf-kit"; // build + convert + fill
107
- import { readOdt, odtToHtml } from "odf-kit/reader"; // read + HTML
108
- import { odtToTypst, modelToTypst } from "odf-kit/typst"; // Typst/PDF
151
+ import { OdtDocument, OdsDocument, htmlToOdt, markdownToOdt, tiptapToOdt, fillTemplate } from "odf-kit";
152
+ import { readOdt, odtToHtml } from "odf-kit/odt-reader";
153
+ import { readOds, odsToHtml } from "odf-kit/ods-reader";
154
+ import { odtToTypst, modelToTypst } from "odf-kit/typst";
109
155
  ```
110
156
 
111
- 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.
157
+ Works in Node.js, browsers, Deno, Bun, and Cloudflare Workers. Runtime dependencies: [fflate](https://github.com/101arrowz/fflate) for ZIP, [marked](https://marked.js.org/) for Markdown parsing.
112
158
 
113
159
  ---
114
160
 
@@ -248,13 +294,6 @@ doc.addParagraph((p) => {
248
294
  });
249
295
  ```
250
296
 
251
- In a browser, use `fetch()` or a file input instead of `readFile()`:
252
-
253
- ```javascript
254
- const response = await fetch("logo.png");
255
- const logo = new Uint8Array(await response.arrayBuffer());
256
- ```
257
-
258
297
  ### Links and bookmarks
259
298
 
260
299
  ```typescript
@@ -327,8 +366,6 @@ sheet.addRow([
327
366
 
328
367
  ### Row and cell formatting
329
368
 
330
- Options on `addRow()` apply to all cells in the row. Per-cell options inside an `OdsCellObject` override the row defaults.
331
-
332
369
  ```typescript
333
370
  // Bold header row with background
334
371
  sheet.addRow(["Month", "Revenue", "Notes"], {
@@ -340,32 +377,23 @@ sheet.addRow(["Month", "Revenue", "Notes"], {
340
377
  // Mixed: row default + per-cell override
341
378
  sheet.addRow([
342
379
  "January",
343
- { value: 12500, type: "float", color: "#006600" }, // green text on this cell only
380
+ { value: 12500, type: "float", color: "#006600" },
344
381
  "On track",
345
382
  ], { italic: true });
346
383
  ```
347
384
 
348
- Available formatting options: `bold`, `italic`, `fontSize`, `fontFamily`, `color`, `underline`, `backgroundColor`, `border`, `borderTop/Bottom/Left/Right`, `align`, `verticalAlign`, `padding`, `wrap`.
349
-
350
385
  ### Date formatting
351
386
 
352
387
  ```typescript
353
- // Document-level default
354
388
  doc.setDateFormat("DD/MM/YYYY"); // "YYYY-MM-DD" | "DD/MM/YYYY" | "MM/DD/YYYY"
355
-
356
- // Per-row or per-cell override
357
389
  sheet.addRow([{ value: new Date("2026-12-25"), type: "date", dateFormat: "MM/DD/YYYY" }]);
358
390
  ```
359
391
 
360
- The `office:date-value` attribute always stores the ISO date — display format is separate.
361
-
362
392
  ### Column widths and row heights
363
393
 
364
394
  ```typescript
365
395
  sheet.setColumnWidth(0, "4cm");
366
396
  sheet.setColumnWidth(1, "8cm");
367
-
368
- sheet.addRow(["Header"]);
369
397
  sheet.setRowHeight(0, "1.5cm");
370
398
  ```
371
399
 
@@ -373,51 +401,90 @@ sheet.setRowHeight(0, "1.5cm");
373
401
 
374
402
  ```typescript
375
403
  const doc = new OdsDocument();
376
- doc.setMetadata({ title: "Annual Report", creator: "Acme Corp" });
377
-
378
- const q1 = doc.addSheet("Q1");
404
+ const q1 = doc.addSheet("Q1").setTabColor("#4CAF50");
405
+ const q2 = doc.addSheet("Q2").setTabColor("#2196F3");
379
406
  q1.addRow(["Month", "Revenue"], { bold: true });
380
407
  q1.addRow(["January", 12500]);
381
- q1.addRow(["March", 14800]);
382
408
 
383
- const q2 = doc.addSheet("Q2");
384
- q2.addRow(["Month", "Revenue"], { bold: true });
385
- q2.addRow(["April", 15300]);
409
+ const q2sheet = doc.addSheet("Summary");
410
+ q2sheet.addRow(["Total", 27700]);
386
411
 
387
412
  const bytes = await doc.save();
388
413
  ```
389
414
 
390
- ---
415
+ ### Number formats
391
416
 
392
- ## Convert: HTML to ODT
417
+ ```typescript
418
+ sheet.addRow([{ value: 9999, type: "float", numberFormat: "integer" }]); // 9,999
419
+ sheet.addRow([{ value: 1234.567, type: "float", numberFormat: "decimal:2" }]); // 1,234.57
420
+ sheet.addRow([{ value: 0.1234, type: "percentage", numberFormat: "percentage" }]); // 12.34%
421
+ sheet.addRow([{ value: 0.075, type: "percentage", numberFormat: "percentage:1" }]);// 7.5%
422
+ sheet.addRow([{ value: 1234.56, type: "currency", numberFormat: "currency:EUR" }]);// €1,234.56
423
+ sheet.addRow([{ value: 99.99, type: "currency", numberFormat: "currency:USD:0" }]);// $100
393
424
 
394
- `htmlToOdt()` converts an HTML string to a `.odt` file. This is the missing conversion direction the entire industry converts ODT→HTML for web display. `htmlToOdt` brings content back into the open standard with no LibreOffice, no Pandoc, and no server-side dependencies.
425
+ // Row-level number formatapplies to all cells in the row
426
+ sheet.addRow([1000, 2000, 3000], { numberFormat: "integer" });
427
+ ```
395
428
 
396
- The primary use case is Nextcloud Text ODT export: Nextcloud Text produces clean, well-formed HTML that maps directly to ODT elements.
429
+ ### Merged cells
397
430
 
398
- ### Basic usage
431
+ ```typescript
432
+ // Span across 3 columns
433
+ sheet.addRow([{ value: "Q1 Sales Report", type: "string", colSpan: 3, bold: true }]);
434
+ sheet.addRow(["Region", "Units", "Revenue"]);
435
+
436
+ // Span across 2 rows
437
+ sheet.addRow([{ value: "North", type: "string", rowSpan: 2 }, "Jan", 12500]);
438
+ sheet.addRow(["Feb", 14200]); // "North" continues from above
439
+
440
+ // Combined colSpan + rowSpan
441
+ sheet.addRow([{ value: "Big Cell", type: "string", colSpan: 2, rowSpan: 2 }, "C"]);
442
+ ```
443
+
444
+ ### Freeze rows and columns
399
445
 
400
446
  ```typescript
401
- import { htmlToOdt } from "odf-kit";
447
+ // Freeze the header row
448
+ sheet.addRow(["Name", "Amount", "Date"], { bold: true });
449
+ sheet.freezeRows(1);
402
450
 
403
- const html = `
404
- <h1>Meeting Notes</h1>
405
- <p>Attendees: <strong>Alice</strong>, Bob, Carol</p>
406
- <h2>Action Items</h2>
407
- <table>
408
- <tr><th>Owner</th><th>Task</th><th>Due</th></tr>
409
- <tr><td>Alice</td><td>Send report</td><td>Friday</td></tr>
410
- </table>
411
- `;
451
+ // Freeze first column
452
+ sheet.freezeColumns(1);
453
+
454
+ // Both
455
+ sheet.freezeRows(1).freezeColumns(1);
456
+ ```
412
457
 
413
- // A4 is the default — correct for Europe and most of the world
414
- const bytes = await htmlToOdt(html);
458
+ ### Hyperlinks in cells
415
459
 
416
- // US letter
417
- const bytes = await htmlToOdt(html, { pageFormat: "letter" });
460
+ ```typescript
461
+ sheet.addRow([{
462
+ value: "odf-kit on GitHub",
463
+ type: "string",
464
+ href: "https://github.com/GitHubNewbie0/odf-kit",
465
+ }]);
466
+ ```
467
+
468
+ ### Sheet tab color
469
+
470
+ ```typescript
471
+ doc.addSheet("Q1").setTabColor("#4CAF50"); // green
472
+ doc.addSheet("Q2").setTabColor("#2196F3"); // blue
473
+ doc.addSheet("Q3").setTabColor("#F44336"); // red
418
474
  ```
419
475
 
420
- Both fragment HTML (`<h1>...</h1><p>...</p>`) and full documents (`<html><body>...</body></html>`) are accepted.
476
+ ---
477
+
478
+ ## Convert: HTML to ODT
479
+
480
+ `htmlToOdt()` converts an HTML string to a `.odt` file. The primary use case is Nextcloud Text ODT export and any web-based editor that stores content as HTML.
481
+
482
+ ```typescript
483
+ import { htmlToOdt } from "odf-kit";
484
+
485
+ const bytes = await htmlToOdt(html); // A4 default
486
+ const bytes = await htmlToOdt(html, { pageFormat: "letter" }); // US letter
487
+ ```
421
488
 
422
489
  ### Page formats
423
490
 
@@ -429,31 +496,70 @@ Both fragment HTML (`<h1>...</h1><p>...</p>`) and full documents (`<html><body>.
429
496
  | `"A3"` | 29.7 × 42 cm | 2.5 cm | Large format |
430
497
  | `"A5"` | 14.8 × 21 cm | 2 cm | Small booklets |
431
498
 
499
+ ### Supported HTML elements
500
+
501
+ **Block:** `<h1>`–`<h6>`, `<p>`, `<ul>`, `<ol>`, `<li>` (nested), `<table>` / `<tr>` / `<td>` / `<th>`, `<blockquote>`, `<pre>`, `<hr>`, `<figure>` / `<figcaption>`, `<div>` / `<section>` (transparent).
502
+
503
+ **Inline:** `<strong>`, `<em>`, `<u>`, `<s>`, `<sup>`, `<sub>`, `<a href>`, `<code>`, `<mark>`, `<span style="">`, `<br>`.
504
+
505
+ ---
506
+
507
+ ## Convert: Markdown to ODT
508
+
509
+ `markdownToOdt()` converts any CommonMark Markdown string to ODT. Accepts the same options as `htmlToOdt()`.
510
+
432
511
  ```typescript
433
- // Landscape with custom margins
434
- const bytes = await htmlToOdt(html, {
435
- pageFormat: "A4",
436
- orientation: "landscape",
437
- marginTop: "1.5cm",
438
- marginBottom: "1.5cm",
439
- });
512
+ import { markdownToOdt } from "odf-kit";
440
513
 
441
- // With metadata
442
- const bytes = await htmlToOdt(html, {
514
+ const bytes = await markdownToOdt(markdownString, { pageFormat: "A4" });
515
+ const bytes = await markdownToOdt(markdownString, {
443
516
  pageFormat: "letter",
444
- metadata: { title: "Meeting Notes", creator: "Alice" },
517
+ metadata: { title: "My Document", creator: "Alice" },
445
518
  });
446
519
  ```
447
520
 
448
- ### Supported HTML elements
521
+ Supports headings, paragraphs, bold, italic, lists (nested), tables, links, blockquotes, code blocks, and horizontal rules.
522
+
523
+ ---
524
+
525
+ ## Convert: TipTap/ProseMirror JSON to ODT
526
+
527
+ `tiptapToOdt()` converts TipTap/ProseMirror `JSONContent` directly to ODT. No dependency on `@tiptap/core` — walks the JSON tree as a plain object. This is the most direct integration path for any TipTap-based editor (dDocs, Outline, Novel, BlockNote, etc.).
449
528
 
450
- **Block:** `<h1>`–`<h6>`, `<p>`, `<ul>`, `<ol>`, `<li>` (nested lists supported), `<table>` / `<thead>` / `<tbody>` / `<tr>` / `<td>` / `<th>`, `<blockquote>` (indented), `<pre>` (monospace), `<hr>` (paragraph with bottom border), `<figure>` / `<figcaption>`, `<div>` / `<section>` / `<article>` / `<main>` (transparent).
529
+ **Conversion happens entirely in your environment.** No document content is sent to external services unlike cloud-based ODT conversion APIs. Suitable for sensitive documents, air-gapped environments, and applications with GDPR or data sovereignty requirements.
451
530
 
452
- **Inline:** `<strong>` / `<b>`, `<em>` / `<i>`, `<u>`, `<s>` / `<del>`, `<sup>`, `<sub>`, `<a href>`, `<code>` (monospace), `<mark>` (highlight), `<span style="">` (inline CSS), `<br>` (line break).
531
+ ```typescript
532
+ import { tiptapToOdt } from "odf-kit";
533
+
534
+ // Basic usage
535
+ const bytes = await tiptapToOdt(editor.getJSON(), { pageFormat: "A4" });
536
+
537
+ // With pre-fetched images
538
+ const images = {
539
+ "https://example.com/photo.jpg": jpegBytes,
540
+ "ipfs://Qm...": ipfsImageBytes,
541
+ };
542
+ const bytes = await tiptapToOdt(editor.getJSON(), { images });
543
+
544
+ // With custom node handler for app-specific extensions
545
+ const bytes = await tiptapToOdt(editor.getJSON(), {
546
+ unknownNodeHandler: (node, doc) => {
547
+ if (node.type === "callout") {
548
+ doc.addParagraph(`⚠️ ${node.content?.[0]?.content?.[0]?.text ?? ""}`)
549
+ }
550
+ },
551
+ });
552
+ ```
553
+
554
+ ### Supported TipTap nodes
453
555
 
454
- **Inline CSS on `<span>`:** `color`, `font-size` (px/pt/em), `font-family`, `font-weight`, `font-style`, `text-decoration`.
556
+ **Block:** `doc`, `paragraph`, `heading` (1–6), `bulletList`, `orderedList`, `listItem` (nested), `blockquote`, `codeBlock`, `horizontalRule`, `hardBreak`, `image`, `table`, `tableRow`, `tableCell`, `tableHeader`.
455
557
 
456
- **Images:** skipped in v1 v2 will add an `images` option for pre-fetched bytes.
558
+ **Marks:** `bold`, `italic`, `underline`, `strike`, `code`, `link`, `textStyle` (color, fontSize, fontFamily), `highlight`, `superscript`, `subscript`.
559
+
560
+ **Images:** Data URIs are decoded and embedded directly. Other URLs are looked up in the `images` option. Unknown URLs emit a `[Image: alt]` placeholder paragraph.
561
+
562
+ **Unknown nodes:** Silently skipped by default. Provide `unknownNodeHandler` to handle custom extensions.
457
563
 
458
564
  ---
459
565
 
@@ -484,15 +590,6 @@ Product: {product} — Qty: {qty} — Price: {price}
484
590
  {/items}
485
591
  ```
486
592
 
487
- ```typescript
488
- fillTemplate(template, {
489
- items: [
490
- { product: "Widget", qty: 5, price: "$125" },
491
- { product: "Gadget", qty: 3, price: "$120" },
492
- ],
493
- });
494
- ```
495
-
496
593
  ### Conditionals
497
594
 
498
595
  ```
@@ -501,13 +598,7 @@ You qualify for a {percent}% discount!
501
598
  {/showDiscount}
502
599
  ```
503
600
 
504
- Falsy values (`false`, `null`, `undefined`, `0`, `""`, `[]`) remove the block. Truthy values include it. Loops and conditionals nest freely.
505
-
506
- ### How it works
507
-
508
- 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.
509
-
510
- 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.
601
+ Falsy values (`false`, `null`, `undefined`, `0`, `""`, `[]`) remove the block. Truthy values include it.
511
602
 
512
603
  ---
513
604
 
@@ -516,94 +607,133 @@ Template syntax follows [Mustache](https://mustache.github.io/) conventions, est
516
607
  `odf-kit/reader` parses `.odt` files into a structured model and renders to HTML.
517
608
 
518
609
  ```typescript
519
- import { readOdt, odtToHtml } from "odf-kit/reader";
520
- import { readFileSync } from "fs";
610
+ import { readOdt, odtToHtml } from "odf-kit/odt-reader";
521
611
 
522
612
  const bytes = readFileSync("report.odt");
523
-
524
- // Structured model
525
613
  const model = readOdt(bytes);
526
- console.log(model.body); // BodyNode[]
527
- console.log(model.pageLayout); // PageLayout
528
- console.log(model.header); // HeaderFooterContent
529
-
530
- // Styled HTML
531
- const html = odtToHtml(bytes);
614
+ const html = odtToHtml(bytes);
532
615
 
533
- // With tracked changes mode
616
+ // Tracked changes
534
617
  const final = odtToHtml(bytes, {}, { trackedChanges: "final" });
535
618
  const original = odtToHtml(bytes, {}, { trackedChanges: "original" });
536
619
  const marked = odtToHtml(bytes, {}, { trackedChanges: "changes" });
537
620
  ```
538
621
 
539
- ### What the reader extracts
622
+ ---
540
623
 
541
- **Tier 1 Structure:** paragraphs, headings, tables, lists, images, notes, bookmarks, fields, hyperlinks, tracked changes (all three ODF-defined modes: final/original/changes).
624
+ ## Read: ODS Spreadsheets
542
625
 
543
- **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.
626
+ `odf-kit/ods-reader` parses `.ods` files into a structured model and renders to HTML.
544
627
 
545
- **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).
628
+ ```typescript
629
+ import { readOds, odsToHtml } from "odf-kit/ods-reader";
630
+ import { readFileSync } from "fs";
631
+
632
+ const bytes = readFileSync("data.ods");
633
+
634
+ // Structured model — typed JavaScript values
635
+ const model = readOds(bytes);
636
+ for (const sheet of model.sheets) {
637
+ console.log(sheet.name);
638
+ for (const row of sheet.rows) {
639
+ for (const cell of row.cells) {
640
+ console.log(cell.colIndex, cell.type, cell.value);
641
+ // e.g. 0 "float" 1234.56
642
+ // e.g. 1 "string" "Hello"
643
+ // e.g. 2 "date" Date { 2026-01-15 }
644
+ // e.g. 3 "formula" 100 (cell.formula = "=SUM(A1:A10)")
645
+ // e.g. 4 "covered" null (part of a merged cell)
646
+ }
647
+ }
648
+ }
546
649
 
547
- ### Document model types
650
+ // HTML table
651
+ const html = odsToHtml(bytes);
652
+
653
+ // Fast mode — values only, no formatting
654
+ const model2 = readOds(bytes, { includeFormatting: false });
655
+ ```
656
+
657
+ ### Cell types
658
+
659
+ | Type | `value` | Notes |
660
+ |------|---------|-------|
661
+ | `"string"` | `string` | |
662
+ | `"float"` | `number` | Includes percentage and currency cells |
663
+ | `"date"` | `Date` (UTC) | |
664
+ | `"boolean"` | `boolean` | |
665
+ | `"formula"` | cached result | `cell.formula` has original string e.g. `"=SUM(A1:A10)"` |
666
+ | `"empty"` | `null` | |
667
+ | `"covered"` | `null` | Covered by a merge — correct `colIndex` always maintained |
668
+
669
+ ### Merged cells
670
+
671
+ Primary cells have `colSpan` and/or `rowSpan`. Covered cells have `type: "covered"`, `value: null`, and the correct physical `colIndex` — no offset confusion.
548
672
 
549
673
  ```typescript
550
- import type {
551
- OdtDocumentModel,
552
- BodyNode, // ParagraphNode | HeadingNode | TableNode | ListNode |
553
- // ImageNode | SectionNode | TrackedChangeNode
554
- ParagraphNode,
555
- HeadingNode,
556
- TableNode,
557
- ListNode,
558
- ImageNode,
559
- SectionNode,
560
- TrackedChangeNode,
561
- InlineNode, // TextNode | SpanNode | ImageNode | NoteNode |
562
- // BookmarkNode | FieldNode | LinkNode
563
- PageLayout,
564
- ReadOdtOptions,
565
- } from "odf-kit/reader";
674
+ // A1:C1 merged — reading row 0:
675
+ // cell 0: { type: "string", value: "Header", colSpan: 3 }
676
+ // cell 1: { type: "covered", value: null, colIndex: 1 }
677
+ // cell 2: { type: "covered", value: null, colIndex: 2 }
678
+ // cell 3: { type: "string", value: "D1", colIndex: 3 } ← always correct
566
679
  ```
567
680
 
568
681
  ---
569
682
 
570
683
  ## Typst: ODT to PDF
571
684
 
572
- `odf-kit/typst` converts `.odt` files to [Typst](https://typst.app/) markup for PDF generation. No LibreOffice, no headless browser — just the Typst CLI.
573
-
574
685
  ```typescript
575
686
  import { odtToTypst, modelToTypst } from "odf-kit/typst";
576
- import { readFileSync, writeFileSync } from "fs";
577
- import { execSync } from "child_process";
578
687
 
579
- // Convenience wrapper — ODT bytes → Typst string
580
688
  const typst = odtToTypst(readFileSync("letter.odt"));
581
689
  writeFileSync("letter.typ", typst);
582
690
  execSync("typst compile letter.typ letter.pdf");
583
-
584
- // From a model (if you already have one from readOdt)
585
- import { readOdt } from "odf-kit/reader";
586
- const model = readOdt(readFileSync("letter.odt"));
587
- const typst2 = modelToTypst(model);
588
691
  ```
589
692
 
590
- 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.
693
+ ---
694
+
695
+ ## API Reference
591
696
 
592
- ### Tracked changes in Typst output
697
+ ### htmlToOdt / markdownToOdt
593
698
 
594
699
  ```typescript
595
- import type { TypstEmitOptions } from "odf-kit/typst";
700
+ function htmlToOdt(html: string, options?: HtmlToOdtOptions): Promise<Uint8Array>
701
+ function markdownToOdt(markdown: string, options?: HtmlToOdtOptions): Promise<Uint8Array>
596
702
 
597
- const options: TypstEmitOptions = { trackedChanges: "final" }; // accepted text only
598
- const options2: TypstEmitOptions = { trackedChanges: "original" }; // before changes
599
- const options3: TypstEmitOptions = { trackedChanges: "changes" }; // annotated markup
703
+ interface HtmlToOdtOptions {
704
+ pageFormat?: "A4" | "letter" | "legal" | "A3" | "A5"; // default: "A4"
705
+ orientation?: "portrait" | "landscape";
706
+ marginTop?: string;
707
+ marginBottom?: string;
708
+ marginLeft?: string;
709
+ marginRight?: string;
710
+ metadata?: { title?: string; creator?: string; description?: string };
711
+ }
600
712
  ```
601
713
 
602
- 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.
714
+ ### tiptapToOdt
603
715
 
604
- ---
716
+ ```typescript
717
+ function tiptapToOdt(json: TiptapNode, options?: TiptapToOdtOptions): Promise<Uint8Array>
605
718
 
606
- ## API Reference
719
+ interface TiptapNode {
720
+ type: string;
721
+ text?: string;
722
+ attrs?: Record<string, unknown>;
723
+ content?: TiptapNode[];
724
+ marks?: TiptapMark[];
725
+ }
726
+
727
+ interface TiptapMark {
728
+ type: string;
729
+ attrs?: Record<string, unknown>;
730
+ }
731
+
732
+ interface TiptapToOdtOptions extends HtmlToOdtOptions {
733
+ images?: Record<string, Uint8Array>;
734
+ unknownNodeHandler?: (node: TiptapNode, doc: OdtDocument) => void;
735
+ }
736
+ ```
607
737
 
608
738
  ### OdtDocument
609
739
 
@@ -621,56 +751,20 @@ See the [complete ODT to PDF with Typst guide](https://githubnewbie0.github.io/o
621
751
  | `addPageBreak()` | Insert page break |
622
752
  | `save()` | Generate `.odt` as `Promise<Uint8Array>` |
623
753
 
624
- ### OdsDocument
754
+ ### OdsDocument / OdsSheet
625
755
 
626
756
  | Method | Description |
627
757
  |--------|-------------|
628
- | `setMetadata(options)` | Set title, creator, description |
629
- | `setDateFormat(format)` | Set default date display format (`"YYYY-MM-DD"` \| `"DD/MM/YYYY"` \| `"MM/DD/YYYY"`) |
630
- | `addSheet(name)` | Add a sheet tab — returns `OdsSheet` |
631
- | `save()` | Generate `.ods` as `Promise<Uint8Array>` |
632
-
633
- ### OdsSheet
634
-
635
- | Method | Description |
636
- |--------|-------------|
637
- | `addRow(values, options?)` | Add a row of cells with optional formatting defaults |
638
- | `setColumnWidth(colIndex, width)` | Set column width (e.g. `"4cm"`) |
639
- | `setRowHeight(rowIndex, height)` | Set row height (e.g. `"1cm"`) |
640
-
641
- ### OdsCellValue
642
-
643
- ```typescript
644
- type OdsCellValue =
645
- | string // → string cell
646
- | number // → float cell
647
- | boolean // → boolean cell
648
- | Date // → date cell
649
- | null // → empty cell
650
- | undefined // → empty cell
651
- | OdsCellObject; // → explicit type (required for formulas)
652
-
653
- interface OdsCellObject extends OdsCellOptions {
654
- value: string | number | boolean | Date | null;
655
- type: "string" | "float" | "date" | "boolean" | "formula";
656
- }
657
- ```
658
-
659
- ### htmlToOdt
660
-
661
- ```typescript
662
- function htmlToOdt(html: string, options?: HtmlToOdtOptions): Promise<Uint8Array>
663
-
664
- interface HtmlToOdtOptions {
665
- pageFormat?: "A4" | "letter" | "legal" | "A3" | "A5"; // default: "A4"
666
- orientation?: "portrait" | "landscape";
667
- marginTop?: string;
668
- marginBottom?: string;
669
- marginLeft?: string;
670
- marginRight?: string;
671
- metadata?: { title?: string; creator?: string; description?: string };
672
- }
673
- ```
758
+ | `doc.setMetadata(options)` | Set title, creator, description |
759
+ | `doc.setDateFormat(format)` | Set default date display format |
760
+ | `doc.addSheet(name)` | Add a sheet tab — returns `OdsSheet` |
761
+ | `doc.save()` | Generate `.ods` as `Promise<Uint8Array>` |
762
+ | `sheet.addRow(values, options?)` | Add a row of cells |
763
+ | `sheet.setColumnWidth(index, width)` | Set column width |
764
+ | `sheet.setRowHeight(index, height)` | Set row height |
765
+ | `sheet.freezeRows(N?)` | Freeze top N rows (default 1) |
766
+ | `sheet.freezeColumns(N?)` | Freeze left N columns (default 1) |
767
+ | `sheet.setTabColor(color)` | Set sheet tab color |
674
768
 
675
769
  ### fillTemplate
676
770
 
@@ -678,31 +772,11 @@ interface HtmlToOdtOptions {
678
772
  function fillTemplate(templateBytes: Uint8Array, data: TemplateData): Uint8Array
679
773
  ```
680
774
 
681
- `TemplateData` is `Record<string, unknown>` — any JSON-serializable value.
682
-
683
775
  | Syntax | Description |
684
776
  |--------|-------------|
685
777
  | `{tag}` | Replace with value |
686
- | `{object.property}` | Dot notation for nested objects |
687
- | `{#tag}...{/tag}` | Loop (array) or conditional (truthy/falsy) |
688
-
689
- ### readOdt / odtToHtml
690
-
691
- ```typescript
692
- function readOdt(bytes: Uint8Array, options?: ReadOdtOptions): OdtDocumentModel
693
- function odtToHtml(
694
- bytes: Uint8Array,
695
- htmlOptions?: HtmlOptions,
696
- readOptions?: ReadOdtOptions
697
- ): string
698
- ```
699
-
700
- ### odtToTypst / modelToTypst
701
-
702
- ```typescript
703
- function odtToTypst(bytes: Uint8Array, options?: TypstEmitOptions): string
704
- function modelToTypst(model: OdtDocumentModel, options?: TypstEmitOptions): string
705
- ```
778
+ | `{object.property}` | Dot notation |
779
+ | `{#tag}...{/tag}` | Loop or conditional |
706
780
 
707
781
  ### TextFormatting
708
782
 
@@ -710,9 +784,9 @@ function modelToTypst(model: OdtDocumentModel, options?: TypstEmitOptions): stri
710
784
  {
711
785
  bold?: boolean,
712
786
  italic?: boolean,
713
- fontSize?: number | string, // 12 or "12pt"
787
+ fontSize?: number | string,
714
788
  fontFamily?: string,
715
- color?: string, // "#FF0000" or "red"
789
+ color?: string,
716
790
  underline?: boolean,
717
791
  strikethrough?: boolean,
718
792
  superscript?: boolean,
@@ -721,37 +795,6 @@ function modelToTypst(model: OdtDocumentModel, options?: TypstEmitOptions): stri
721
795
  }
722
796
  ```
723
797
 
724
- ### TableOptions / CellOptions
725
-
726
- ```typescript
727
- // TableOptions
728
- { columnWidths?: string[], border?: string }
729
-
730
- // CellOptions (extends TextFormatting)
731
- {
732
- backgroundColor?: string,
733
- border?: string,
734
- borderTop?: string, borderBottom?: string,
735
- borderLeft?: string, borderRight?: string,
736
- colSpan?: number,
737
- rowSpan?: number,
738
- }
739
- ```
740
-
741
- ### PageLayout
742
-
743
- ```typescript
744
- {
745
- width?: string, // "21cm" (A4 default)
746
- height?: string, // "29.7cm"
747
- orientation?: "portrait" | "landscape",
748
- marginTop?: string, // "2cm" default
749
- marginBottom?: string,
750
- marginLeft?: string,
751
- marginRight?: string,
752
- }
753
- ```
754
-
755
798
  ---
756
799
 
757
800
  ## Platform support
@@ -763,7 +806,7 @@ function modelToTypst(model: OdtDocumentModel, options?: TypstEmitOptions): stri
763
806
  | Deno, Bun | ✅ Full |
764
807
  | Cloudflare Workers | ✅ Full |
765
808
 
766
- ESM only. Zero Node-specific APIs in the library source — enforced at the TypeScript level, guaranteeing cross-platform compatibility.
809
+ ESM only. Zero Node-specific APIs in the library source — enforced at the TypeScript level.
767
810
 
768
811
  ---
769
812
 
@@ -771,11 +814,11 @@ ESM only. Zero Node-specific APIs in the library source — enforced at the Type
771
814
 
772
815
  **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.
773
816
 
774
- - **Single runtime dependency** — fflate for ZIP. No transitive dependencies.
817
+ - **Two runtime dependencies** — fflate (ZIP) and marked (Markdown parsing). No transitive dependencies.
775
818
  - **Spec-compliant output** — every generated file passes the OASIS ODF validator. Enforced on every commit by CI.
776
819
  - **Multiple ODF formats** — ODT documents and ODS spreadsheets from the same library.
777
- - **Six complete capability modes** — build ODT, build ODS, convert HTML→ODT, fill templates, read, convert to Typst/PDF. Not just generation.
778
- - **HTML→ODT conversion** — the missing direction. Bring web content back into the ISO open standard with no LibreOffice or Pandoc dependency.
820
+ - **Eight complete capability modes** — build ODT, build ODS, convert HTML→ODT, convert Markdown→ODT, convert TipTap JSON→ODT, fill templates, read, convert to Typst/PDF.
821
+ - **TipTap/ProseMirror integration** — direct JSON→ODT conversion for any TipTap-based editor, no intermediate HTML step.
779
822
  - **Zero-dependency Typst emitter** — the only JavaScript library with built-in ODT→Typst conversion for PDF generation.
780
823
  - **TypeScript-first** — full types across all sub-exports.
781
824
  - **Apache 2.0** — use freely in commercial and open source projects.
@@ -787,8 +830,10 @@ ESM only. Zero Node-specific APIs in the library source — enforced at the Type
787
830
  | Feature | odf-kit | simple-odf | docxtemplater |
788
831
  |---------|---------|------------|---------------|
789
832
  | Generate .odt from scratch | ✅ | ⚠️ flat XML only | ❌ |
790
- | Generate .ods from scratch | ✅ | ❌ | ❌ |
833
+ | Generate .ods from scratch | ✅ merged cells, freeze, number formats, hyperlinks | ❌ | ❌ |
791
834
  | Convert HTML → ODT | ✅ | ❌ | ❌ |
835
+ | Convert Markdown → ODT | ✅ | ❌ | ❌ |
836
+ | Convert TipTap JSON → ODT | ✅ | ❌ | ❌ |
792
837
  | Fill .odt templates | ✅ | ❌ | ✅ .docx only |
793
838
  | Read .odt files | ✅ | ❌ | ❌ |
794
839
  | Convert to HTML | ✅ | ❌ | ❌ |
@@ -801,29 +846,35 @@ ESM only. Zero Node-specific APIs in the library source — enforced at the Type
801
846
 
802
847
  ## Specification compliance
803
848
 
804
- 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.
849
+ odf-kit targets ODF 1.2 (ISO/IEC 26300). Generated files include proper ZIP packaging, manifest, metadata, and all required namespace declarations. The OASIS ODF validator runs on every push via GitHub Actions.
805
850
 
806
851
  ---
807
852
 
808
853
  ## Version history
809
854
 
810
- **v0.9.2** — `htmlToOdt()`: HTML→ODT conversion with page format presets (A4/letter/legal/A3/A5), full inline formatting, lists, tables, blockquote, pre, hr, and inline CSS. `addLineBreak()` on `ParagraphBuilder`. `borderBottom` on `ParagraphOptions`. 769 tests passing.
855
+ **v0.9.8** — ODS reader: `readOds()` and `odsToHtml()` via `odf-kit/ods-reader`. Typed values, formula strings, merged cell handling, formatting, metadata. `odf-kit/odt-reader` alias added. 889 tests passing.
811
856
 
812
- **v0.9.0** — ODS spreadsheet generation: `OdsDocument`, multiple sheets, auto-typed cells, formulas, date formatting (ISO/DMY/MDY), row and cell formatting, column widths, row heights, style deduplication. 707 tests passing.
857
+ **v0.9.7** — ODS enhancements: number formats (integer, decimal:N, percentage, currency), merged cells (colSpan/rowSpan), freeze rows/columns, hyperlinks in cells, sheet tab color. 849 tests passing.
813
858
 
814
- **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.
859
+ **v0.9.6** — `tiptapToOdt()`: TipTap/ProseMirror JSON→ODT conversion. `TiptapNode`, `TiptapMark`, `TiptapToOdtOptions` types. `unknownNodeHandler` for custom extensions. Image support via pre-fetched bytes map. 817 tests passing.
815
860
 
816
- **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.
861
+ **v0.9.5** — `markdownToOdt()`: Markdown→ODT via marked + htmlToOdt. 786 tests passing.
817
862
 
818
- **v0.6.0** — Tier 2 reader: span styles, image float/wrap, footnotes/endnotes, bookmarks, fields, cell/row styles, full style inheritance.
863
+ **v0.9.4** — ODS datetime auto-detection (nonzero UTC time datetime format). ODS formula `xmlns:of` namespace fix (Err:510 resolved).
819
864
 
820
- **v0.5.0** — `odf-kit/reader` sub-export: `readOdt()`, `odtToHtml()`. Tier 1: paragraphs, headings, tables, lists, images, notes, tracked changes.
865
+ **v0.9.2** — `htmlToOdt()`: HTML→ODT conversion with page format presets, full inline formatting, lists, tables, blockquote, pre, hr, and inline CSS. 769 tests passing.
821
866
 
822
- **v0.4.0** — Generation repair: 16 spec compliance gaps fixed, OASIS ODF validator added to CI.
867
+ **v0.9.0** — ODS spreadsheet generation: `OdsDocument`, multiple sheets, auto-typed cells, formulas, date formatting, row and cell formatting, column widths, row heights. 707 tests passing.
823
868
 
824
- **v0.3.0** — Template engine: loops, conditionals, dot notation, automatic XML fragment healing.
869
+ **v0.8.0** — `odf-kit/typst`: `odtToTypst()` and `modelToTypst()`. Zero-dependency ODT→Typst emitter for PDF generation.
825
870
 
826
- **v0.2.0** — Migrated to fflate (zero transitive dependencies).
871
+ **v0.7.0** — Tier 3 reader: paragraph styles, page geometry, headers/footers, sections, tracked changes (all three ODF modes).
872
+
873
+ **v0.6.0** — Tier 2 reader: span styles, image float/wrap, footnotes/endnotes, bookmarks, fields, cell/row styles.
874
+
875
+ **v0.5.0** — `odf-kit/reader`: `readOdt()`, `odtToHtml()`. Tier 1 parsing.
876
+
877
+ **v0.3.0** — Template engine: loops, conditionals, dot notation, automatic XML fragment healing.
827
878
 
828
879
  **v0.1.0** — Programmatic ODT creation: text, tables, page layout, lists, images, links, bookmarks.
829
880
 
@@ -831,8 +882,6 @@ odf-kit targets ODF 1.2 (ISO/IEC 26300). Generated files include proper ZIP pack
831
882
 
832
883
  ## Guides
833
884
 
834
- Full walkthroughs and real-world examples on the documentation site:
835
-
836
885
  - [Generate ODT files in Node.js](https://githubnewbie0.github.io/odf-kit/guides/generate-odt-nodejs.html)
837
886
  - [Generate ODT files in the browser](https://githubnewbie0.github.io/odf-kit/guides/generate-odt-browser.html)
838
887
  - [Fill ODT templates in JavaScript](https://githubnewbie0.github.io/odf-kit/guides/fill-odt-template-javascript.html)
@@ -860,21 +909,6 @@ npm run build
860
909
  npm test
861
910
  ```
862
911
 
863
- Full pipeline before submitting a PR:
864
-
865
- ```bash
866
- npm run format:check
867
- npm run lint
868
- npm run build
869
- npm test
870
- ```
871
-
872
- ---
873
-
874
- ## Acknowledgments
875
-
876
- 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.
877
-
878
912
  ---
879
913
 
880
914
  ## License