odf-kit 0.9.6 → 0.9.7

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,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
+ ## Eight 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,7 @@ 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. Convert .odt to Typst for PDF generation
87
123
  import { odtToTypst } from "odf-kit/typst";
88
124
  import { execSync } from "child_process";
89
125
 
@@ -103,12 +139,12 @@ npm install odf-kit
103
139
  Node.js 22+ required. ESM only. Sub-exports:
104
140
 
105
141
  ```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
142
+ import { OdtDocument, OdsDocument, htmlToOdt, markdownToOdt, tiptapToOdt, fillTemplate } from "odf-kit";
143
+ import { readOdt, odtToHtml } from "odf-kit/reader";
144
+ import { odtToTypst, modelToTypst } from "odf-kit/typst";
109
145
  ```
110
146
 
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.
147
+ 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
148
 
113
149
  ---
114
150
 
@@ -248,13 +284,6 @@ doc.addParagraph((p) => {
248
284
  });
249
285
  ```
250
286
 
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
287
  ### Links and bookmarks
259
288
 
260
289
  ```typescript
@@ -327,8 +356,6 @@ sheet.addRow([
327
356
 
328
357
  ### Row and cell formatting
329
358
 
330
- Options on `addRow()` apply to all cells in the row. Per-cell options inside an `OdsCellObject` override the row defaults.
331
-
332
359
  ```typescript
333
360
  // Bold header row with background
334
361
  sheet.addRow(["Month", "Revenue", "Notes"], {
@@ -340,32 +367,23 @@ sheet.addRow(["Month", "Revenue", "Notes"], {
340
367
  // Mixed: row default + per-cell override
341
368
  sheet.addRow([
342
369
  "January",
343
- { value: 12500, type: "float", color: "#006600" }, // green text on this cell only
370
+ { value: 12500, type: "float", color: "#006600" },
344
371
  "On track",
345
372
  ], { italic: true });
346
373
  ```
347
374
 
348
- Available formatting options: `bold`, `italic`, `fontSize`, `fontFamily`, `color`, `underline`, `backgroundColor`, `border`, `borderTop/Bottom/Left/Right`, `align`, `verticalAlign`, `padding`, `wrap`.
349
-
350
375
  ### Date formatting
351
376
 
352
377
  ```typescript
353
- // Document-level default
354
378
  doc.setDateFormat("DD/MM/YYYY"); // "YYYY-MM-DD" | "DD/MM/YYYY" | "MM/DD/YYYY"
355
-
356
- // Per-row or per-cell override
357
379
  sheet.addRow([{ value: new Date("2026-12-25"), type: "date", dateFormat: "MM/DD/YYYY" }]);
358
380
  ```
359
381
 
360
- The `office:date-value` attribute always stores the ISO date — display format is separate.
361
-
362
382
  ### Column widths and row heights
363
383
 
364
384
  ```typescript
365
385
  sheet.setColumnWidth(0, "4cm");
366
386
  sheet.setColumnWidth(1, "8cm");
367
-
368
- sheet.addRow(["Header"]);
369
387
  sheet.setRowHeight(0, "1.5cm");
370
388
  ```
371
389
 
@@ -373,51 +391,90 @@ sheet.setRowHeight(0, "1.5cm");
373
391
 
374
392
  ```typescript
375
393
  const doc = new OdsDocument();
376
- doc.setMetadata({ title: "Annual Report", creator: "Acme Corp" });
377
-
378
- const q1 = doc.addSheet("Q1");
394
+ const q1 = doc.addSheet("Q1").setTabColor("#4CAF50");
395
+ const q2 = doc.addSheet("Q2").setTabColor("#2196F3");
379
396
  q1.addRow(["Month", "Revenue"], { bold: true });
380
397
  q1.addRow(["January", 12500]);
381
- q1.addRow(["March", 14800]);
382
398
 
383
- const q2 = doc.addSheet("Q2");
384
- q2.addRow(["Month", "Revenue"], { bold: true });
385
- q2.addRow(["April", 15300]);
399
+ const q2sheet = doc.addSheet("Summary");
400
+ q2sheet.addRow(["Total", 27700]);
386
401
 
387
402
  const bytes = await doc.save();
388
403
  ```
389
404
 
390
- ---
405
+ ### Number formats
391
406
 
392
- ## Convert: HTML to ODT
407
+ ```typescript
408
+ sheet.addRow([{ value: 9999, type: "float", numberFormat: "integer" }]); // 9,999
409
+ sheet.addRow([{ value: 1234.567, type: "float", numberFormat: "decimal:2" }]); // 1,234.57
410
+ sheet.addRow([{ value: 0.1234, type: "percentage", numberFormat: "percentage" }]); // 12.34%
411
+ sheet.addRow([{ value: 0.075, type: "percentage", numberFormat: "percentage:1" }]);// 7.5%
412
+ sheet.addRow([{ value: 1234.56, type: "currency", numberFormat: "currency:EUR" }]);// €1,234.56
413
+ sheet.addRow([{ value: 99.99, type: "currency", numberFormat: "currency:USD:0" }]);// $100
414
+
415
+ // Row-level number format — applies to all cells in the row
416
+ sheet.addRow([1000, 2000, 3000], { numberFormat: "integer" });
417
+ ```
393
418
 
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.
419
+ ### Merged cells
395
420
 
396
- The primary use case is Nextcloud Text ODT export: Nextcloud Text produces clean, well-formed HTML that maps directly to ODT elements.
421
+ ```typescript
422
+ // Span across 3 columns
423
+ sheet.addRow([{ value: "Q1 Sales Report", type: "string", colSpan: 3, bold: true }]);
424
+ sheet.addRow(["Region", "Units", "Revenue"]);
425
+
426
+ // Span across 2 rows
427
+ sheet.addRow([{ value: "North", type: "string", rowSpan: 2 }, "Jan", 12500]);
428
+ sheet.addRow(["Feb", 14200]); // "North" continues from above
429
+
430
+ // Combined colSpan + rowSpan
431
+ sheet.addRow([{ value: "Big Cell", type: "string", colSpan: 2, rowSpan: 2 }, "C"]);
432
+ ```
397
433
 
398
- ### Basic usage
434
+ ### Freeze rows and columns
399
435
 
400
436
  ```typescript
401
- import { htmlToOdt } from "odf-kit";
437
+ // Freeze the header row
438
+ sheet.addRow(["Name", "Amount", "Date"], { bold: true });
439
+ sheet.freezeRows(1);
402
440
 
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
- `;
441
+ // Freeze first column
442
+ sheet.freezeColumns(1);
443
+
444
+ // Both
445
+ sheet.freezeRows(1).freezeColumns(1);
446
+ ```
447
+
448
+ ### Hyperlinks in cells
412
449
 
413
- // A4 is the default — correct for Europe and most of the world
414
- const bytes = await htmlToOdt(html);
450
+ ```typescript
451
+ sheet.addRow([{
452
+ value: "odf-kit on GitHub",
453
+ type: "string",
454
+ href: "https://github.com/GitHubNewbie0/odf-kit",
455
+ }]);
456
+ ```
457
+
458
+ ### Sheet tab color
415
459
 
416
- // US letter
417
- const bytes = await htmlToOdt(html, { pageFormat: "letter" });
460
+ ```typescript
461
+ doc.addSheet("Q1").setTabColor("#4CAF50"); // green
462
+ doc.addSheet("Q2").setTabColor("#2196F3"); // blue
463
+ doc.addSheet("Q3").setTabColor("#F44336"); // red
418
464
  ```
419
465
 
420
- Both fragment HTML (`<h1>...</h1><p>...</p>`) and full documents (`<html><body>...</body></html>`) are accepted.
466
+ ---
467
+
468
+ ## Convert: HTML to ODT
469
+
470
+ `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.
471
+
472
+ ```typescript
473
+ import { htmlToOdt } from "odf-kit";
474
+
475
+ const bytes = await htmlToOdt(html); // A4 default
476
+ const bytes = await htmlToOdt(html, { pageFormat: "letter" }); // US letter
477
+ ```
421
478
 
422
479
  ### Page formats
423
480
 
@@ -429,31 +486,70 @@ Both fragment HTML (`<h1>...</h1><p>...</p>`) and full documents (`<html><body>.
429
486
  | `"A3"` | 29.7 × 42 cm | 2.5 cm | Large format |
430
487
  | `"A5"` | 14.8 × 21 cm | 2 cm | Small booklets |
431
488
 
489
+ ### Supported HTML elements
490
+
491
+ **Block:** `<h1>`–`<h6>`, `<p>`, `<ul>`, `<ol>`, `<li>` (nested), `<table>` / `<tr>` / `<td>` / `<th>`, `<blockquote>`, `<pre>`, `<hr>`, `<figure>` / `<figcaption>`, `<div>` / `<section>` (transparent).
492
+
493
+ **Inline:** `<strong>`, `<em>`, `<u>`, `<s>`, `<sup>`, `<sub>`, `<a href>`, `<code>`, `<mark>`, `<span style="">`, `<br>`.
494
+
495
+ ---
496
+
497
+ ## Convert: Markdown to ODT
498
+
499
+ `markdownToOdt()` converts any CommonMark Markdown string to ODT. Accepts the same options as `htmlToOdt()`.
500
+
432
501
  ```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
- });
502
+ import { markdownToOdt } from "odf-kit";
440
503
 
441
- // With metadata
442
- const bytes = await htmlToOdt(html, {
504
+ const bytes = await markdownToOdt(markdownString, { pageFormat: "A4" });
505
+ const bytes = await markdownToOdt(markdownString, {
443
506
  pageFormat: "letter",
444
- metadata: { title: "Meeting Notes", creator: "Alice" },
507
+ metadata: { title: "My Document", creator: "Alice" },
445
508
  });
446
509
  ```
447
510
 
448
- ### Supported HTML elements
511
+ Supports headings, paragraphs, bold, italic, lists (nested), tables, links, blockquotes, code blocks, and horizontal rules.
512
+
513
+ ---
514
+
515
+ ## Convert: TipTap/ProseMirror JSON to ODT
449
516
 
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).
517
+ `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.).
451
518
 
452
- **Inline:** `<strong>` / `<b>`, `<em>` / `<i>`, `<u>`, `<s>` / `<del>`, `<sup>`, `<sub>`, `<a href>`, `<code>` (monospace), `<mark>` (highlight), `<span style="">` (inline CSS), `<br>` (line break).
519
+ **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.
453
520
 
454
- **Inline CSS on `<span>`:** `color`, `font-size` (px/pt/em), `font-family`, `font-weight`, `font-style`, `text-decoration`.
521
+ ```typescript
522
+ import { tiptapToOdt } from "odf-kit";
455
523
 
456
- **Images:** skipped in v1 — v2 will add an `images` option for pre-fetched bytes.
524
+ // Basic usage
525
+ const bytes = await tiptapToOdt(editor.getJSON(), { pageFormat: "A4" });
526
+
527
+ // With pre-fetched images
528
+ const images = {
529
+ "https://example.com/photo.jpg": jpegBytes,
530
+ "ipfs://Qm...": ipfsImageBytes,
531
+ };
532
+ const bytes = await tiptapToOdt(editor.getJSON(), { images });
533
+
534
+ // With custom node handler for app-specific extensions
535
+ const bytes = await tiptapToOdt(editor.getJSON(), {
536
+ unknownNodeHandler: (node, doc) => {
537
+ if (node.type === "callout") {
538
+ doc.addParagraph(`⚠️ ${node.content?.[0]?.content?.[0]?.text ?? ""}`)
539
+ }
540
+ },
541
+ });
542
+ ```
543
+
544
+ ### Supported TipTap nodes
545
+
546
+ **Block:** `doc`, `paragraph`, `heading` (1–6), `bulletList`, `orderedList`, `listItem` (nested), `blockquote`, `codeBlock`, `horizontalRule`, `hardBreak`, `image`, `table`, `tableRow`, `tableCell`, `tableHeader`.
547
+
548
+ **Marks:** `bold`, `italic`, `underline`, `strike`, `code`, `link`, `textStyle` (color, fontSize, fontFamily), `highlight`, `superscript`, `subscript`.
549
+
550
+ **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.
551
+
552
+ **Unknown nodes:** Silently skipped by default. Provide `unknownNodeHandler` to handle custom extensions.
457
553
 
458
554
  ---
459
555
 
@@ -484,15 +580,6 @@ Product: {product} — Qty: {qty} — Price: {price}
484
580
  {/items}
485
581
  ```
486
582
 
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
583
  ### Conditionals
497
584
 
498
585
  ```
@@ -501,13 +588,7 @@ You qualify for a {percent}% discount!
501
588
  {/showDiscount}
502
589
  ```
503
590
 
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.
591
+ Falsy values (`false`, `null`, `undefined`, `0`, `""`, `[]`) remove the block. Truthy values include it.
511
592
 
512
593
  ---
513
594
 
@@ -517,93 +598,73 @@ Template syntax follows [Mustache](https://mustache.github.io/) conventions, est
517
598
 
518
599
  ```typescript
519
600
  import { readOdt, odtToHtml } from "odf-kit/reader";
520
- import { readFileSync } from "fs";
521
601
 
522
602
  const bytes = readFileSync("report.odt");
523
-
524
- // Structured model
525
603
  const model = readOdt(bytes);
526
- console.log(model.body); // BodyNode[]
527
- console.log(model.pageLayout); // PageLayout
528
- console.log(model.header); // HeaderFooterContent
604
+ const html = odtToHtml(bytes);
529
605
 
530
- // Styled HTML
531
- const html = odtToHtml(bytes);
532
-
533
- // With tracked changes mode
606
+ // Tracked changes
534
607
  const final = odtToHtml(bytes, {}, { trackedChanges: "final" });
535
608
  const original = odtToHtml(bytes, {}, { trackedChanges: "original" });
536
609
  const marked = odtToHtml(bytes, {}, { trackedChanges: "changes" });
537
610
  ```
538
611
 
539
- ### What the reader extracts
540
-
541
- **Tier 1 — Structure:** paragraphs, headings, tables, lists, images, notes, bookmarks, fields, hyperlinks, tracked changes (all three ODF-defined modes: final/original/changes).
542
-
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.
544
-
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).
546
-
547
- ### Document model types
548
-
549
- ```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";
566
- ```
567
-
568
612
  ---
569
613
 
570
614
  ## Typst: ODT to PDF
571
615
 
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
616
  ```typescript
575
617
  import { odtToTypst, modelToTypst } from "odf-kit/typst";
576
- import { readFileSync, writeFileSync } from "fs";
577
- import { execSync } from "child_process";
578
618
 
579
- // Convenience wrapper — ODT bytes → Typst string
580
619
  const typst = odtToTypst(readFileSync("letter.odt"));
581
620
  writeFileSync("letter.typ", typst);
582
621
  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
622
  ```
589
623
 
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.
624
+ ---
625
+
626
+ ## API Reference
591
627
 
592
- ### Tracked changes in Typst output
628
+ ### htmlToOdt / markdownToOdt
593
629
 
594
630
  ```typescript
595
- import type { TypstEmitOptions } from "odf-kit/typst";
631
+ function htmlToOdt(html: string, options?: HtmlToOdtOptions): Promise<Uint8Array>
632
+ function markdownToOdt(markdown: string, options?: HtmlToOdtOptions): Promise<Uint8Array>
596
633
 
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
634
+ interface HtmlToOdtOptions {
635
+ pageFormat?: "A4" | "letter" | "legal" | "A3" | "A5"; // default: "A4"
636
+ orientation?: "portrait" | "landscape";
637
+ marginTop?: string;
638
+ marginBottom?: string;
639
+ marginLeft?: string;
640
+ marginRight?: string;
641
+ metadata?: { title?: string; creator?: string; description?: string };
642
+ }
600
643
  ```
601
644
 
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.
645
+ ### tiptapToOdt
603
646
 
604
- ---
647
+ ```typescript
648
+ function tiptapToOdt(json: TiptapNode, options?: TiptapToOdtOptions): Promise<Uint8Array>
605
649
 
606
- ## API Reference
650
+ interface TiptapNode {
651
+ type: string;
652
+ text?: string;
653
+ attrs?: Record<string, unknown>;
654
+ content?: TiptapNode[];
655
+ marks?: TiptapMark[];
656
+ }
657
+
658
+ interface TiptapMark {
659
+ type: string;
660
+ attrs?: Record<string, unknown>;
661
+ }
662
+
663
+ interface TiptapToOdtOptions extends HtmlToOdtOptions {
664
+ images?: Record<string, Uint8Array>;
665
+ unknownNodeHandler?: (node: TiptapNode, doc: OdtDocument) => void;
666
+ }
667
+ ```
607
668
 
608
669
  ### OdtDocument
609
670
 
@@ -621,56 +682,20 @@ See the [complete ODT to PDF with Typst guide](https://githubnewbie0.github.io/o
621
682
  | `addPageBreak()` | Insert page break |
622
683
  | `save()` | Generate `.odt` as `Promise<Uint8Array>` |
623
684
 
624
- ### OdsDocument
625
-
626
- | Method | Description |
627
- |--------|-------------|
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
685
+ ### OdsDocument / OdsSheet
634
686
 
635
687
  | Method | Description |
636
688
  |--------|-------------|
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
- ```
689
+ | `doc.setMetadata(options)` | Set title, creator, description |
690
+ | `doc.setDateFormat(format)` | Set default date display format |
691
+ | `doc.addSheet(name)` | Add a sheet tab — returns `OdsSheet` |
692
+ | `doc.save()` | Generate `.ods` as `Promise<Uint8Array>` |
693
+ | `sheet.addRow(values, options?)` | Add a row of cells |
694
+ | `sheet.setColumnWidth(index, width)` | Set column width |
695
+ | `sheet.setRowHeight(index, height)` | Set row height |
696
+ | `sheet.freezeRows(N?)` | Freeze top N rows (default 1) |
697
+ | `sheet.freezeColumns(N?)` | Freeze left N columns (default 1) |
698
+ | `sheet.setTabColor(color)` | Set sheet tab color |
674
699
 
675
700
  ### fillTemplate
676
701
 
@@ -678,31 +703,11 @@ interface HtmlToOdtOptions {
678
703
  function fillTemplate(templateBytes: Uint8Array, data: TemplateData): Uint8Array
679
704
  ```
680
705
 
681
- `TemplateData` is `Record<string, unknown>` — any JSON-serializable value.
682
-
683
706
  | Syntax | Description |
684
707
  |--------|-------------|
685
708
  | `{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
- ```
709
+ | `{object.property}` | Dot notation |
710
+ | `{#tag}...{/tag}` | Loop or conditional |
706
711
 
707
712
  ### TextFormatting
708
713
 
@@ -710,9 +715,9 @@ function modelToTypst(model: OdtDocumentModel, options?: TypstEmitOptions): stri
710
715
  {
711
716
  bold?: boolean,
712
717
  italic?: boolean,
713
- fontSize?: number | string, // 12 or "12pt"
718
+ fontSize?: number | string,
714
719
  fontFamily?: string,
715
- color?: string, // "#FF0000" or "red"
720
+ color?: string,
716
721
  underline?: boolean,
717
722
  strikethrough?: boolean,
718
723
  superscript?: boolean,
@@ -721,37 +726,6 @@ function modelToTypst(model: OdtDocumentModel, options?: TypstEmitOptions): stri
721
726
  }
722
727
  ```
723
728
 
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
729
  ---
756
730
 
757
731
  ## Platform support
@@ -763,7 +737,7 @@ function modelToTypst(model: OdtDocumentModel, options?: TypstEmitOptions): stri
763
737
  | Deno, Bun | ✅ Full |
764
738
  | Cloudflare Workers | ✅ Full |
765
739
 
766
- ESM only. Zero Node-specific APIs in the library source — enforced at the TypeScript level, guaranteeing cross-platform compatibility.
740
+ ESM only. Zero Node-specific APIs in the library source — enforced at the TypeScript level.
767
741
 
768
742
  ---
769
743
 
@@ -771,11 +745,11 @@ ESM only. Zero Node-specific APIs in the library source — enforced at the Type
771
745
 
772
746
  **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
747
 
774
- - **Single runtime dependency** — fflate for ZIP. No transitive dependencies.
748
+ - **Two runtime dependencies** — fflate (ZIP) and marked (Markdown parsing). No transitive dependencies.
775
749
  - **Spec-compliant output** — every generated file passes the OASIS ODF validator. Enforced on every commit by CI.
776
750
  - **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.
751
+ - **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.
752
+ - **TipTap/ProseMirror integration** — direct JSON→ODT conversion for any TipTap-based editor, no intermediate HTML step.
779
753
  - **Zero-dependency Typst emitter** — the only JavaScript library with built-in ODT→Typst conversion for PDF generation.
780
754
  - **TypeScript-first** — full types across all sub-exports.
781
755
  - **Apache 2.0** — use freely in commercial and open source projects.
@@ -787,8 +761,10 @@ ESM only. Zero Node-specific APIs in the library source — enforced at the Type
787
761
  | Feature | odf-kit | simple-odf | docxtemplater |
788
762
  |---------|---------|------------|---------------|
789
763
  | Generate .odt from scratch | ✅ | ⚠️ flat XML only | ❌ |
790
- | Generate .ods from scratch | ✅ | ❌ | ❌ |
764
+ | Generate .ods from scratch | ✅ merged cells, freeze, number formats, hyperlinks | ❌ | ❌ |
791
765
  | Convert HTML → ODT | ✅ | ❌ | ❌ |
766
+ | Convert Markdown → ODT | ✅ | ❌ | ❌ |
767
+ | Convert TipTap JSON → ODT | ✅ | ❌ | ❌ |
792
768
  | Fill .odt templates | ✅ | ❌ | ✅ .docx only |
793
769
  | Read .odt files | ✅ | ❌ | ❌ |
794
770
  | Convert to HTML | ✅ | ❌ | ❌ |
@@ -801,29 +777,33 @@ ESM only. Zero Node-specific APIs in the library source — enforced at the Type
801
777
 
802
778
  ## Specification compliance
803
779
 
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.
780
+ 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
781
 
806
782
  ---
807
783
 
808
784
  ## Version history
809
785
 
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.
786
+ **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.
811
787
 
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.
788
+ **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.
813
789
 
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.
790
+ **v0.9.5** — `markdownToOdt()`: Markdown→ODT via marked + htmlToOdt. 786 tests passing.
815
791
 
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.
792
+ **v0.9.4** — ODS datetime auto-detection (nonzero UTC time datetime format). ODS formula `xmlns:of` namespace fix (Err:510 resolved).
817
793
 
818
- **v0.6.0** — Tier 2 reader: span styles, image float/wrap, footnotes/endnotes, bookmarks, fields, cell/row styles, full style inheritance.
794
+ **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.
819
795
 
820
- **v0.5.0** — `odf-kit/reader` sub-export: `readOdt()`, `odtToHtml()`. Tier 1: paragraphs, headings, tables, lists, images, notes, tracked changes.
796
+ **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.
821
797
 
822
- **v0.4.0** — Generation repair: 16 spec compliance gaps fixed, OASIS ODF validator added to CI.
798
+ **v0.8.0** — `odf-kit/typst`: `odtToTypst()` and `modelToTypst()`. Zero-dependency ODT→Typst emitter for PDF generation.
823
799
 
824
- **v0.3.0** — Template engine: loops, conditionals, dot notation, automatic XML fragment healing.
800
+ **v0.7.0** — Tier 3 reader: paragraph styles, page geometry, headers/footers, sections, tracked changes (all three ODF modes).
825
801
 
826
- **v0.2.0** — Migrated to fflate (zero transitive dependencies).
802
+ **v0.6.0** — Tier 2 reader: span styles, image float/wrap, footnotes/endnotes, bookmarks, fields, cell/row styles.
803
+
804
+ **v0.5.0** — `odf-kit/reader`: `readOdt()`, `odtToHtml()`. Tier 1 parsing.
805
+
806
+ **v0.3.0** — Template engine: loops, conditionals, dot notation, automatic XML fragment healing.
827
807
 
828
808
  **v0.1.0** — Programmatic ODT creation: text, tables, page layout, lists, images, links, bookmarks.
829
809
 
@@ -831,8 +811,6 @@ odf-kit targets ODF 1.2 (ISO/IEC 26300). Generated files include proper ZIP pack
831
811
 
832
812
  ## Guides
833
813
 
834
- Full walkthroughs and real-world examples on the documentation site:
835
-
836
814
  - [Generate ODT files in Node.js](https://githubnewbie0.github.io/odf-kit/guides/generate-odt-nodejs.html)
837
815
  - [Generate ODT files in the browser](https://githubnewbie0.github.io/odf-kit/guides/generate-odt-browser.html)
838
816
  - [Fill ODT templates in JavaScript](https://githubnewbie0.github.io/odf-kit/guides/fill-odt-template-javascript.html)
@@ -860,21 +838,6 @@ npm run build
860
838
  npm test
861
839
  ```
862
840
 
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
841
  ---
879
842
 
880
843
  ## License