odf-kit
v0.13.4
Published
Generate, fill, read, and convert OpenDocument Format (.odt) files in JavaScript and TypeScript. Works in Node.js and browsers.
Maintainers
Readme
odf-kit
Generate, fill, read, and convert OpenDocument Format files (.odt, .ods) in TypeScript and JavaScript. Convert HTML, Markdown, TipTap JSON, Lexical JSON, and DOCX to ODT. Works in Node.js and browsers. No LibreOffice dependency — pure spec-compliant ODF.
npm install odf-kitThirteen ways to work with ODF files
// 1. Build an ODT document from scratch
import { OdtDocument } from "odf-kit";
const doc = new OdtDocument();
doc.addHeading("Quarterly Report", 1);
doc.addParagraph("Revenue exceeded expectations.");
doc.addTable([
["Division", "Q4 Revenue", "Growth"],
["North", "$2.1M", "+12%"],
["South", "$1.8M", "+8%"],
]);
const bytes = await doc.save();// 2. Convert HTML to ODT
import { htmlToOdt } from "odf-kit";
const html = `
<h1>Meeting Notes</h1>
<p>Attendees: <strong>Alice</strong>, Bob, Carol</p>
<ul>
<li>Project status</li>
<li>Budget review</li>
</ul>
`;
const bytes = await htmlToOdt(html, { pageFormat: "A4" });// 3. Convert Markdown to ODT
import { markdownToOdt } from "odf-kit";
const markdown = `
# Meeting Notes
Attendees: **Alice**, Bob, Carol
## Action Items
- Send report by Friday
- Review budget on Monday
`;
const bytes = await markdownToOdt(markdown, { pageFormat: "A4" });// 4. Convert TipTap/ProseMirror JSON to ODT
import { tiptapToOdt } from "odf-kit";
// editor.getJSON() returns TipTap JSONContent
const bytes = await tiptapToOdt(editor.getJSON(), { pageFormat: "A4" });
// With pre-fetched images (e.g. from IPFS or S3)
const images = { [imageUrl]: await fetchImageBytes(imageUrl) };
const bytes2 = await tiptapToOdt(editor.getJSON(), { images });
// With custom node handler for app-specific extensions
const bytes3 = await tiptapToOdt(editor.getJSON(), {
unknownNodeHandler: (node, doc) => {
if (node.type === "callout") doc.addParagraph(`⚠️ ${extractText(node)}`);
},
});// 5. Build an ODS spreadsheet from scratch
import { OdsDocument } from "odf-kit";
const doc = new OdsDocument();
const sheet = doc.addSheet("Sales");
sheet.addRow(["Month", "Revenue", "Growth"], { bold: true, backgroundColor: "#DDDDDD" });
sheet.addRow(["January", 12500, 0.08]);
sheet.addRow(["February", 14200, 0.136]);
sheet.addRow(["Total", { value: "=SUM(B2:B3)", type: "formula" }]);
sheet.setColumnWidth(0, "4cm");
sheet.setColumnWidth(1, "4cm");
const bytes = await doc.save();// 6. Fill an existing .odt template with data
import { fillTemplate } from "odf-kit";
const template = readFileSync("invoice-template.odt");
const result = fillTemplate(template, {
customer: "Acme Corp",
date: "2026-03-19",
items: [
{ product: "Widget", qty: 5, price: "$125" },
{ product: "Gadget", qty: 3, price: "$120" },
],
showNotes: true,
notes: "Net 30",
});
writeFileSync("invoice.odt", result);// 7. Read an existing .odt file
import { readOdt, odtToHtml } from "odf-kit/reader";
const bytes = readFileSync("report.odt");
const model = readOdt(bytes); // structured document model
const html = odtToHtml(bytes); // styled HTML string// 8. Read an existing .ods spreadsheet
import { readOds, odsToHtml } from "odf-kit/ods-reader";
const bytes = readFileSync("data.ods");
const model = readOds(bytes); // structured model — typed values
const html = odsToHtml(bytes); // HTML table string// 9. Convert .xlsx to .ods — no external dependencies
import { xlsxToOds } from "odf-kit/xlsx"
const bytes = await xlsxToOds(readFileSync("report.xlsx"))
writeFileSync("report.ods", bytes)// 10. Convert .odt to Typst for PDF generation
import { odtToTypst } from "odf-kit/typst";
import { execSync } from "child_process";
const typst = odtToTypst(readFileSync("letter.odt"));
writeFileSync("letter.typ", typst);
execSync("typst compile letter.typ letter.pdf");// 11. Convert .docx to .odt — pure ESM, zero new dependencies, browser-safe
import { docxToOdt } from "odf-kit/docx";
const { bytes, warnings } = await docxToOdt(readFileSync("report.docx"));
writeFileSync("report.odt", bytes);
if (warnings.length > 0) console.warn(warnings);
// With options
const { bytes: bytes2 } = await docxToOdt(readFileSync("report.docx"), {
pageFormat: "letter",
styleMap: { "Section Title": 1 }, // map custom Word style → heading level
});// 12. Convert .odt to Markdown
import { odtToMarkdown } from "odf-kit/markdown";
const md = odtToMarkdown(readFileSync("document.odt"));
writeFileSync("document.md", md);
// CommonMark flavor (no pipe tables)
const mdCompat = odtToMarkdown(readFileSync("document.odt"), { flavor: "commonmark" });
// Embed images as base64 data URLs (fully self-contained output)
const mdEmbedded = odtToMarkdown(readFileSync("document.odt"), { embedImages: true });// 13. Convert Lexical editor state to ODT
import { lexicalToOdt } from "odf-kit/lexical";
// editor.getEditorState().toJSON() returns SerializedEditorState
const bytes = await lexicalToOdt(editor.getEditorState().toJSON(), { pageFormat: "A4" });
// With image resolution (e.g. for Proton Docs integration)
const bytes2 = await lexicalToOdt(editor.getEditorState().toJSON(), {
pageFormat: "A4",
fetchImage: async (src) => {
const response = await fetch(src);
return new Uint8Array(await response.arrayBuffer());
},
});Installation
npm install odf-kitNode.js 22+ required. ESM only. Sub-exports:
import { OdtDocument, OdsDocument, htmlToOdt, markdownToOdt, tiptapToOdt, fillTemplate } from "odf-kit";
import { readOdt, odtToHtml } from "odf-kit/odt-reader";
import { readOds, odsToHtml } from "odf-kit/ods-reader";
import { odtToTypst, modelToTypst } from "odf-kit/typst";
import { docxToOdt } from "odf-kit/docx";
import { odtToMarkdown, modelToMarkdown } from "odf-kit/markdown";
import { lexicalToOdt } from "odf-kit/lexical";
import { odfKitNormalizer } from "odf-kit/html-normalizer";Works in Node.js, browsers, Deno, Bun, and Cloudflare Workers. Runtime dependencies: fflate for ZIP, marked for Markdown parsing.
Browser usage
odf-kit generates and reads documents entirely client-side. No server required.
import { OdtDocument } from "odf-kit";
const doc = new OdtDocument();
doc.addHeading("Generated in the Browser", 1);
doc.addParagraph("Created without any server.");
const bytes = await doc.save();
const blob = new Blob([bytes], { type: "application/vnd.oasis.opendocument.text" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "document.odt";
a.click();
URL.revokeObjectURL(url);Template filling and reading work the same way — pass Uint8Array bytes from a <input type="file"> or fetch().
Build: ODT documents
Text and formatting
doc.addHeading("Chapter 1", 1);
doc.addParagraph((p) => {
p.addText("This is ");
p.addText("bold", { bold: true });
p.addText(", ");
p.addText("italic", { italic: true });
p.addText(", and ");
p.addText("red", { color: "red", fontSize: 16 });
p.addText(".");
});
// Scientific notation
doc.addParagraph((p) => {
p.addText("H");
p.addText("2", { subscript: true });
p.addText("O is ");
p.addText("essential", { underline: true, highlightColor: "yellow" });
});Tables
// Simple
doc.addTable([
["Name", "Age", "City"],
["Alice", "30", "Portland"],
["Bob", "25", "Seattle"],
]);
// With column widths and borders
doc.addTable([
["Product", "Price"],
["Widget", "$9.99"],
], { columnWidths: ["8cm", "4cm"], border: "0.5pt solid #000000" });
// Full control — builder callback
doc.addTable((t) => {
t.addRow((r) => {
r.addCell("Name", { bold: true, backgroundColor: "#DDDDDD" });
r.addCell("Status", { bold: true, backgroundColor: "#DDDDDD" });
});
t.addRow((r) => {
r.addCell((c) => { c.addText("Project Alpha", { bold: true }); });
r.addCell("Complete", { color: "green" });
});
}, { columnWidths: ["8cm", "4cm"] });Page layout, headers, footers
doc.setPageLayout({
orientation: "landscape",
marginTop: "1.5cm",
marginBottom: "1.5cm",
});
doc.setHeader((h) => {
h.addText("Confidential", { bold: true, color: "gray" });
h.addText(" — Page ");
h.addPageNumber();
});
doc.setFooter("© 2026 Acme Corp — Page ###"); // ### = page number
doc.addPageBreak();Lists
doc.addList(["Apples", "Bananas", "Cherries"]);
doc.addList(["First", "Second", "Third"], { type: "numbered" });
// Nested with formatting
doc.addList((l) => {
l.addItem((p) => {
p.addText("Important: ", { bold: true });
p.addText("read the docs");
});
l.addItem("Main topic");
l.addNested((sub) => {
sub.addItem("Subtopic A");
sub.addItem("Subtopic B");
});
});Images
import { readFile } from "fs/promises";
const logo = await readFile("logo.png");
doc.addImage(logo, { width: "10cm", height: "6cm", mimeType: "image/png" });
// Inline image inside a paragraph
doc.addParagraph((p) => {
p.addText("Logo: ");
p.addImage(logo, { width: "2cm", height: "1cm", mimeType: "image/png" });
});Links and bookmarks
doc.addParagraph((p) => {
p.addBookmark("introduction");
p.addText("Welcome to the guide.");
});
doc.addParagraph((p) => {
p.addLink("our website", "https://example.com", { bold: true });
p.addText(" or go back to the ");
p.addLink("introduction", "#introduction");
});Tab stops
doc.addParagraph((p) => {
p.addText("Item"); p.addTab();
p.addText("Qty"); p.addTab();
p.addText("$100.00");
}, {
tabStops: [
{ position: "6cm" },
{ position: "12cm", type: "right" },
],
});Method chaining
const bytes = await new OdtDocument()
.setMetadata({ title: "Report" })
.setPageLayout({ orientation: "landscape" })
.setHeader("Confidential")
.setFooter("Page ###")
.addHeading("Summary", 1)
.addParagraph("All systems operational.")
.addTable([["System", "Status"], ["API", "OK"], ["DB", "OK"]])
.save();Build: ODS spreadsheets
OdsDocument generates .ods spreadsheet files with multiple sheets, typed cells, formatting, and formulas.
Cell types
Values are auto-typed from their JavaScript type. Use an explicit OdsCellObject when you need formulas or per-cell overrides.
import { OdsDocument } from "odf-kit";
const doc = new OdsDocument();
const sheet = doc.addSheet("Data");
sheet.addRow([
"Text", // string
42, // float
new Date("2026-01-15"), // date
true, // boolean
null, // empty cell
{ value: "=SUM(B1:B10)", type: "formula" }, // formula — explicit required
]);Row and cell formatting
// Bold header row with background
sheet.addRow(["Month", "Revenue", "Notes"], {
bold: true,
backgroundColor: "#DDDDDD",
align: "center",
});
// Mixed: row default + per-cell override
sheet.addRow([
"January",
{ value: 12500, type: "float", color: "#006600" },
"On track",
], { italic: true });Date formatting
doc.setDateFormat("DD/MM/YYYY"); // "YYYY-MM-DD" | "DD/MM/YYYY" | "MM/DD/YYYY"
sheet.addRow([{ value: new Date("2026-12-25"), type: "date", dateFormat: "MM/DD/YYYY" }]);Column widths and row heights
sheet.setColumnWidth(0, "4cm");
sheet.setColumnWidth(1, "8cm");
sheet.setRowHeight(0, "1.5cm");Multiple sheets
const doc = new OdsDocument();
const q1 = doc.addSheet("Q1").setTabColor("#4CAF50");
const q2 = doc.addSheet("Q2").setTabColor("#2196F3");
q1.addRow(["Month", "Revenue"], { bold: true });
q1.addRow(["January", 12500]);
const q2sheet = doc.addSheet("Summary");
q2sheet.addRow(["Total", 27700]);
const bytes = await doc.save();Number formats
sheet.addRow([{ value: 9999, type: "float", numberFormat: "integer" }]); // 9,999
sheet.addRow([{ value: 1234.567, type: "float", numberFormat: "decimal:2" }]); // 1,234.57
sheet.addRow([{ value: 0.1234, type: "percentage", numberFormat: "percentage" }]); // 12.34%
sheet.addRow([{ value: 0.075, type: "percentage", numberFormat: "percentage:1" }]);// 7.5%
sheet.addRow([{ value: 1234.56, type: "currency", numberFormat: "currency:EUR" }]);// €1,234.56
sheet.addRow([{ value: 99.99, type: "currency", numberFormat: "currency:USD:0" }]);// $100
// Row-level number format — applies to all cells in the row
sheet.addRow([1000, 2000, 3000], { numberFormat: "integer" });Merged cells
// Span across 3 columns
sheet.addRow([{ value: "Q1 Sales Report", type: "string", colSpan: 3, bold: true }]);
sheet.addRow(["Region", "Units", "Revenue"]);
// Span across 2 rows
sheet.addRow([{ value: "North", type: "string", rowSpan: 2 }, "Jan", 12500]);
sheet.addRow(["Feb", 14200]); // "North" continues from above
// Combined colSpan + rowSpan
sheet.addRow([{ value: "Big Cell", type: "string", colSpan: 2, rowSpan: 2 }, "C"]);Freeze rows and columns
// Freeze the header row
sheet.addRow(["Name", "Amount", "Date"], { bold: true });
sheet.freezeRows(1);
// Freeze first column
sheet.freezeColumns(1);
// Both
sheet.freezeRows(1).freezeColumns(1);Hyperlinks in cells
sheet.addRow([{
value: "odf-kit on GitHub",
type: "string",
href: "https://github.com/GitHubNewbie0/odf-kit",
}]);Sheet tab color
doc.addSheet("Q1").setTabColor("#4CAF50"); // green
doc.addSheet("Q2").setTabColor("#2196F3"); // blue
doc.addSheet("Q3").setTabColor("#F44336"); // redConvert: HTML to ODT
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.
import { htmlToOdt } from "odf-kit";
const bytes = await htmlToOdt(html); // A4 default
const bytes = await htmlToOdt(html, { pageFormat: "letter" }); // US letterPage formats
| Format | Dimensions | Default margins | Typical use |
|---|---|---|---|
| "A4" | 21 × 29.7 cm | 2.5 cm | Europe, ISO standard (default) |
| "letter" | 21.59 × 27.94 cm | 2.54 cm | USA, Canada |
| "legal" | 21.59 × 35.56 cm | 2.54 cm | USA legal |
| "A3" | 29.7 × 42 cm | 2.5 cm | Large format |
| "A5" | 14.8 × 21 cm | 2 cm | Small booklets |
Images
Base64 data URLs embedded in src attributes are decoded and embedded automatically. For remote URLs, provide pre-fetched bytes via the images map or an async fetchImage callback. Images without a resolution method are skipped silently.
// Base64 data URL — embedded automatically
const bytes = await htmlToOdt('<img src="data:image/png;base64,..."/>');
// Pre-fetched image map (e.g. from WebDAV in odf-kit-service)
const bytes = await htmlToOdt(html, {
images: {
"https://example.com/logo.png": pngBytes,
},
});
// Async fetch callback (Node.js or browser)
const bytes = await htmlToOdt(html, {
fetchImage: async (src) => {
const res = await fetch(src);
return new Uint8Array(await res.arrayBuffer());
},
});Supported HTML elements
Block: <h1>–<h6>, <p>, <ul>, <ol>, <li> (nested), <table> / <tr> / <td> / <th>, <blockquote>, <pre>, <hr>, <figure> / <figcaption> (transparent), <div> / <section> (transparent).
Inline: <strong>, <em>, <u>, <s>, <sup>, <sub>, <a href>, <code>, <mark>, <span style="">, <br>.
Input contract
htmlToOdt() accepts good HTML5 — the kind produced by Markdown renderers, rich-text editors, templating engines, and modern content management systems. Input is normalized to polyglot markup before parsing. The default normalizer applies seven spec-grounded text transformations:
- Empties
<script>and<style>content - Lowercases the doctype declaration
- Quotes unquoted boolean attributes (
<input checked>→<input checked="">) - Quotes unquoted attribute values (
<a href=foo>→<a href="foo">) - Self-closes 14 HTML5 void elements
- Decodes ~2,120 HTML5 named entities to Unicode
- Escapes lone
&in attribute values (href="?a=1&b=2"→href="?a=1&b=2")
After normalization, the input is valid XHTML and parses correctly.
If your input is already polyglot or XHTML, you can skip normalization:
const bytes = await htmlToOdt(html, { normalizer: false });The underlying parser fails loudly on malformed input — unclosed tags, mismatched tags, and any malformed-attribute patterns the normalizer didn't cover. Code that worked before continues to work; inputs that were silently producing wrong output now raise explicit errors.
Substitution architecture
For users with specific compliance or compatibility requirements, the normalizer and parser are individually substitutable:
import { htmlToOdt } from "odf-kit";
// Use parse5 for full HTML5 spec compliance — write a small adapter
// (see ADAPTERS.md for conventions)
htmlToOdt(html, { parser: fromParse5(parse5.parse) });
// Skip normalization (input is already polyglot)
htmlToOdt(html, { normalizer: false });
// Substitute both
htmlToOdt(html, { normalizer: myNormalizer, parser: myParser });The substitution hooks also propagate to markdownToOdt(). They do not apply to tiptapToOdt() because TipTap input is a JSON tree, not an HTML string.
See ADAPTERS.md for the substitution architecture, naming conventions, and a worked adapter example.
Convert: Markdown to ODT
markdownToOdt() converts any CommonMark Markdown string to ODT. Accepts the same options as htmlToOdt().
import { markdownToOdt } from "odf-kit";
const bytes = await markdownToOdt(markdownString, { pageFormat: "A4" });
const bytes = await markdownToOdt(markdownString, {
pageFormat: "letter",
metadata: { title: "My Document", creator: "Alice" },
});Supports headings, paragraphs, bold, italic, lists (nested), tables, links, blockquotes, code blocks, and horizontal rules.
Convert: TipTap/ProseMirror JSON to ODT
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.).
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.
import { tiptapToOdt } from "odf-kit";
// Basic usage
const bytes = await tiptapToOdt(editor.getJSON(), { pageFormat: "A4" });
// With pre-fetched images
const images = {
"https://example.com/photo.jpg": jpegBytes,
"ipfs://Qm...": ipfsImageBytes,
};
const bytes = await tiptapToOdt(editor.getJSON(), { images });
// With custom node handler for app-specific extensions
const bytes = await tiptapToOdt(editor.getJSON(), {
unknownNodeHandler: (node, doc) => {
if (node.type === "callout") {
doc.addParagraph(`⚠️ ${node.content?.[0]?.content?.[0]?.text ?? ""}`)
}
},
});Supported TipTap nodes
Block: doc, paragraph, heading (1–6), bulletList, orderedList, listItem (nested), blockquote, codeBlock, horizontalRule, hardBreak, image, table, tableRow, tableCell, tableHeader.
Marks: bold, italic, underline, strike, code, link, textStyle (color, fontSize, fontFamily), highlight, superscript, subscript.
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.
Unknown nodes: Silently skipped by default. Provide unknownNodeHandler to handle custom extensions.
Fill: template engine
Create a .odt template in LibreOffice with {placeholders}, then fill it programmatically.
Simple replacement
Dear {name},
Your order #{orderNumber} has shipped to {address}.Dot notation
Company: {company.name}
City: {company.address.city}Loops
{#items}
Product: {product} — Qty: {qty} — Price: {price}
{/items}Conditionals
{#showDiscount}
You qualify for a {percent}% discount!
{/showDiscount}Falsy values (false, null, undefined, 0, "", []) remove the block. Truthy values include it.
Read: ODT document model
odf-kit/reader parses .odt files into a structured model and renders to HTML.
import { readOdt, odtToHtml } from "odf-kit/odt-reader";
const bytes = readFileSync("report.odt");
const model = readOdt(bytes);
const html = odtToHtml(bytes);
// Tracked changes
const final = odtToHtml(bytes, {}, { trackedChanges: "final" });
const original = odtToHtml(bytes, {}, { trackedChanges: "original" });
const marked = odtToHtml(bytes, {}, { trackedChanges: "changes" });Read: ODS Spreadsheets
odf-kit/ods-reader parses .ods files into a structured model and renders to HTML.
import { readOds, odsToHtml } from "odf-kit/ods-reader";
import { readFileSync } from "fs";
const bytes = readFileSync("data.ods");
// Structured model — typed JavaScript values
const model = readOds(bytes);
for (const sheet of model.sheets) {
console.log(sheet.name);
for (const row of sheet.rows) {
for (const cell of row.cells) {
console.log(cell.colIndex, cell.type, cell.value);
// e.g. 0 "float" 1234.56
// e.g. 1 "string" "Hello"
// e.g. 2 "date" Date { 2026-01-15 }
// e.g. 3 "formula" 100 (cell.formula = "=SUM(A1:A10)")
// e.g. 4 "covered" null (part of a merged cell)
}
}
}
// HTML table
const html = odsToHtml(bytes);
// Fast mode — values only, no formatting
const model2 = readOds(bytes, { includeFormatting: false });Cell types
| Type | value | Notes |
|------|---------|-------|
| "string" | string | |
| "float" | number | Includes percentage and currency cells |
| "date" | Date (UTC) | |
| "boolean" | boolean | |
| "formula" | cached result | cell.formula has original string e.g. "=SUM(A1:A10)" |
| "empty" | null | |
| "covered" | null | Covered by a merge — correct colIndex always maintained |
Merged cells
Primary cells have colSpan and/or rowSpan. Covered cells have type: "covered", value: null, and the correct physical colIndex — no offset confusion.
// A1:C1 merged — reading row 0:
// cell 0: { type: "string", value: "Header", colSpan: 3 }
// cell 1: { type: "covered", value: null, colIndex: 1 }
// cell 2: { type: "covered", value: null, colIndex: 2 }
// cell 3: { type: "string", value: "D1", colIndex: 3 } ← always correctConvert: XLSX → ODS
odf-kit/xlsx converts .xlsx spreadsheets to .ods with no external dependencies — parses XLSX XML directly using fflate (already in odf-kit) and our own XML parser. Supports .xlsx and .xlsm. Does not support legacy .xls (binary format).
import { xlsxToOds } from "odf-kit/xlsx"
import { readFileSync, writeFileSync } from "fs"
// Simple conversion
const bytes = await xlsxToOds(readFileSync("report.xlsx"))
writeFileSync("report.ods", bytes)
// With options
const bytes2 = await xlsxToOds(readFileSync("report.xlsx"), {
dateFormat: "DD/MM/YYYY",
metadata: { title: "Q4 Report", creator: "Alice" },
})
// Works with ArrayBuffer too (browser-friendly)
const bytes3 = await xlsxToOds(arrayBuffer)What is preserved:
- All sheets in tab order, with their names
- Cell values: strings, numbers, booleans, dates, formula cached results
- Formula strings
- Merged cells (colSpan/rowSpan)
- Freeze rows/columns
- Multiple sheets
What is not preserved (out of scope for v0.9.9):
- Cell formatting (colors, fonts, borders)
- Column widths and row heights
- Charts, images, pivot tables
Typst: ODT to PDF
import { odtToTypst, modelToTypst } from "odf-kit/typst";
const typst = odtToTypst(readFileSync("letter.odt"));
writeFileSync("letter.typ", typst);
execSync("typst compile letter.typ letter.pdf");API Reference
docxToOdt
import { docxToOdt } from "odf-kit/docx"
const { bytes, warnings } = await docxToOdt(input, options?)
interface DocxToOdtOptions {
pageFormat?: "A4" | "letter" | "legal" | "A3" | "A5";
orientation?: "portrait" | "landscape";
preservePageLayout?: boolean; // default: true — read layout from DOCX
styleMap?: Record<string, number>; // custom style name → heading level
metadata?: { title?: string; creator?: string; description?: string };
}
interface DocxToOdtResult {
bytes: Uint8Array; // the .odt file
warnings: string[]; // content that could not be fully converted
}htmlToOdt / markdownToOdt
function htmlToOdt(html: string, options?: HtmlToOdtOptions): Promise<Uint8Array>
function markdownToOdt(markdown: string, options?: HtmlToOdtOptions): Promise<Uint8Array>
interface HtmlToOdtOptions {
pageFormat?: "A4" | "letter" | "legal" | "A3" | "A5"; // default: "A4"
orientation?: "portrait" | "landscape";
marginTop?: string;
marginBottom?: string;
marginLeft?: string;
marginRight?: string;
metadata?: { title?: string; creator?: string; description?: string };
images?: Record<string, Uint8Array>;
fetchImage?: (src: string) => Promise<Uint8Array | undefined>;
normalizer?: Normalizer | false; // omit for default (Tier 1 normalization);
// false to skip; or supply a custom function
parser?: Parser; // omit for default (odfKitParser);
// or supply a custom function
}tiptapToOdt
function tiptapToOdt(json: TiptapNode, options?: TiptapToOdtOptions): Promise<Uint8Array>
interface TiptapNode {
type: string;
text?: string;
attrs?: Record<string, unknown>;
content?: TiptapNode[];
marks?: TiptapMark[];
}
interface TiptapMark {
type: string;
attrs?: Record<string, unknown>;
}
interface TiptapToOdtOptions extends HtmlToOdtOptions {
images?: Record<string, Uint8Array>;
unknownNodeHandler?: (node: TiptapNode, doc: OdtDocument) => void;
}OdtDocument
| Method | Description |
|--------|-------------|
| setMetadata(options) | Set title, creator, description |
| setPageLayout(options) | Set page size, margins, orientation |
| setHeader(content) | Set page header (string or builder) |
| setFooter(content) | Set page footer (string or builder) |
| addHeading(content, level?) | Add heading (level 1–6) |
| addParagraph(content, options?) | Add paragraph (string or builder) |
| addTable(content, options?) | Add table (string[][] or builder) |
| addList(content, options?) | Add list (string[] or builder) |
| addImage(data, options) | Add standalone image |
| addPageBreak() | Insert page break |
| save() | Generate .odt as Promise<Uint8Array> |
OdsDocument / OdsSheet
| Method | Description |
|--------|-------------|
| doc.setMetadata(options) | Set title, creator, description |
| doc.setDateFormat(format) | Set default date display format |
| doc.addSheet(name) | Add a sheet tab — returns OdsSheet |
| doc.save() | Generate .ods as Promise<Uint8Array> |
| sheet.addRow(values, options?) | Add a row of cells |
| sheet.setColumnWidth(index, width) | Set column width |
| sheet.setRowHeight(index, height) | Set row height |
| sheet.freezeRows(N?) | Freeze top N rows (default 1) |
| sheet.freezeColumns(N?) | Freeze left N columns (default 1) |
| sheet.setTabColor(color) | Set sheet tab color |
fillTemplate
function fillTemplate(templateBytes: Uint8Array, data: TemplateData): Uint8Array| Syntax | Description |
|--------|-------------|
| {tag} | Replace with value |
| {object.property} | Dot notation |
| {#tag}...{/tag} | Loop or conditional |
TextFormatting
{
bold?: boolean,
italic?: boolean,
fontSize?: number | string,
fontFamily?: string,
color?: string,
underline?: boolean,
strikethrough?: boolean,
superscript?: boolean,
subscript?: boolean,
highlightColor?: string,
}Platform support
| Platform | Support | |----------|---------| | Node.js 22+ | ✅ Full | | Chrome, Firefox, Safari, Edge | ✅ Full | | Deno, Bun | ✅ Full | | Cloudflare Workers | ✅ Full |
ESM only. Zero Node-specific APIs in the library source — enforced at the TypeScript level.
Why odf-kit?
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.
- Two runtime dependencies — fflate (ZIP) and marked (Markdown parsing). No transitive dependencies.
- Spec-compliant output — every generated file passes the OASIS ODF validator. Enforced on every commit by CI.
- Multiple ODF formats — ODT documents and ODS spreadsheets from the same library.
- Nine complete capability modes — build ODT, build ODS, convert HTML→ODT, convert Markdown→ODT, convert TipTap JSON→ODT, convert DOCX→ODT, fill templates, read, convert to Typst/PDF.
- TipTap/ProseMirror integration — direct JSON→ODT conversion for any TipTap-based editor, no intermediate HTML step.
- Zero-dependency Typst emitter — the only JavaScript library with built-in ODT→Typst conversion for PDF generation.
- TypeScript-first — full types across all sub-exports.
- Apache 2.0 — use freely in commercial and open source projects.
Comparison
| Feature | odf-kit | simple-odf | docxtemplater | |---------|---------|------------|---------------| | Generate .odt from scratch | ✅ | ⚠️ flat XML only | ❌ | | Generate .ods from scratch | ✅ merged cells, freeze, number formats, hyperlinks | ❌ | ❌ | | Convert HTML → ODT | ✅ | ❌ | ❌ | | Convert Markdown → ODT | ✅ | ❌ | ❌ | | Convert TipTap JSON → ODT | ✅ | ❌ | ❌ | | Convert DOCX → ODT | ✅ native, browser-safe | ❌ | ❌ | | Fill .odt templates | ✅ | ❌ | ✅ .docx only | | Read .odt files | ✅ | ❌ | ❌ | | Convert to HTML | ✅ | ❌ | ❌ | | Convert to Typst / PDF | ✅ | ❌ | ❌ | | Browser support | ✅ | ❌ | ✅ | | Maintained | ✅ | ❌ abandoned 2021 | ✅ | | Open source | ✅ Apache 2.0 | ✅ MIT | ⚠️ paid for advanced features |
Specification compliance
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.
Version history
v0.13.4 — Adds VERSION runtime export from the root entry point and all 11 published sub-paths. Allows runtime consumers (notably the tool pages on githubnewbie0.github.io) to report the actual loaded version in error reports and telemetry. Auto-synced from package.json at build time via new scripts/sync-version.js. Tool pages and landing page now display the correct version automatically. No breaking changes.
v0.13.3 — Dependency security fix: bumped marked from 18.0.0 to 18.0.3 to address an OOM DoS vulnerability in the marked tokenizer (Dependabot alert). Affected applications using markdownToOdt() with untrusted Markdown input. No API changes.
v0.13.2 — HTML5 normalizer for htmlToOdt() and substitution architecture for normalizer and parser. Default Tier 1 normalization applies seven spec-grounded transformations: empties script/style content, lowercases doctype, quotes unquoted boolean attributes, quotes unquoted attribute values, self-closes void elements, decodes named entities, escapes & in attribute values. parseXml now fails loudly on malformed input. New odfKitNormalizer, odfKitParser, and public types ParsedHtmlTree, Parser, Normalizer. Substitution hooks propagate to markdownToOdt(). New sub-export odf-kit/html-normalizer. See ADAPTERS.md. 1307 tests passing.
v0.13.1 — odtToMarkdown() embedImages option for self-contained Markdown output. 1124 tests passing.
v0.13.0 — htmlToOdt() image support: base64 data URLs, images map, async fetchImage callback. 1120 tests passing.
v0.12.3 — ODT settings.xml added. 1113 tests passing.
v0.12.0 — lexicalToOdt() via odf-kit/lexical. Converts Lexical SerializedEditorState to ODT. CellBuilder.addLink(), addLineBreak(), addImage(). ODS freeze panes fixed. 1107 tests passing.
v0.11.0 — odtToMarkdown() and modelToMarkdown() via odf-kit/markdown. GFM and CommonMark flavors. 1078 tests passing.
v0.10.4 — ODS freeze fix: ViewId and ActiveTable added to settings.xml. typesVersions restored (dropped in v0.10.3). 1059 tests passing.
v0.10.3 — module-sync exports condition for bundler compatibility. typesVersions for TypeScript moduleResolution: node compatibility.
v0.10.2 — ODS freeze rows/columns fix — ActiveSplitRange and all split axis items now correctly emitted in settings.xml.
v0.10.0 — docxToOdt() via odf-kit/docx. Native DOCX→ODT converter — pure ESM, zero new dependencies, browser-safe. Preserves text, headings, formatting, tables, lists, images (actual dimensions), hyperlinks, bookmarks, footnotes, page layout, headers/footers, and tracked changes. Spec-validated against ECMA-376 5th edition. 1053 tests passing.
v0.9.9 — xlsxToOds() via odf-kit/xlsx. XLSX→ODS conversion with zero new dependencies. 936 tests passing.
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.
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.
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.
v0.9.5 — markdownToOdt(): Markdown→ODT via marked + htmlToOdt. 786 tests passing.
v0.9.4 — ODS datetime auto-detection (nonzero UTC time → datetime format). ODS formula xmlns:of namespace fix (Err:510 resolved).
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.
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.
v0.8.0 — odf-kit/typst: odtToTypst() and modelToTypst(). Zero-dependency ODT→Typst emitter for PDF generation.
v0.7.0 — Tier 3 reader: paragraph styles, page geometry, headers/footers, sections, tracked changes (all three ODF modes).
v0.6.0 — Tier 2 reader: span styles, image float/wrap, footnotes/endnotes, bookmarks, fields, cell/row styles.
v0.5.0 — odf-kit/reader: readOdt(), odtToHtml(). Tier 1 parsing.
v0.3.0 — Template engine: loops, conditionals, dot notation, automatic XML fragment healing.
v0.1.0 — Programmatic ODT creation: text, tables, page layout, lists, images, links, bookmarks.
Guides
- Generate ODT files in Node.js
- Generate ODT files in the browser
- Fill ODT templates in JavaScript
- Convert ODT to HTML in JavaScript
- Lexical to ODT developer guide
- Convert DOCX to ODT in JavaScript
- LibreOffice headless alternative
- SheetJS alternative for ODF
- ODT to PDF via Typst
- Generate ODT without LibreOffice
- ODF government compliance
- simple-odf alternative
- docxtemplater alternative for ODF
- ODT JavaScript ecosystem
- Free DOCX to ODT converter (online tool)
- Free ODT to Markdown converter (online tool)
- Free ODT to HTML converter (online tool)
- Free ODT to PDF converter (online tool)
- Free XLSX to ODS converter (online tool)
- Free Markdown to ODT converter (online tool)
- Free Lexical JSON to ODT converter (online tool)
- Free HTML to ODT converter (online tool)
- Free ODS to HTML converter (online tool)
Contributing
Issues and pull requests welcome at github.com/GitHubNewbie0/odf-kit.
git clone https://github.com/GitHubNewbie0/odf-kit.git
cd odf-kit
npm install
npm run build
npm testLicense
Apache 2.0 — see LICENSE for details.
