| name | odt |
| description | Use this skill whenever the user wants to create, fill, read, or convert OpenDocument Format files (.odt, .ods). Triggers include: any mention of 'ODT', 'ODS', 'ODF', 'OpenDocument', 'LibreOffice document', or requests to produce documents in open-source or ISO standard formats. Also use when converting HTML, Markdown, or TipTap/ProseMirror JSON to ODT, filling ODT templates with data, reading or extracting content from ODT files, generating ODS spreadsheets, reading ODS files, converting XLSX to ODS, converting DOCX to ODT, or creating formal documents like resolutions, reports, letters, contracts, invoices, or memos as .odt files. Use this skill instead of the docx skill when the user explicitly requests .odt output or mentions LibreOffice/OpenOffice as their target application. Do NOT use for .docx files (use the docx skill), PDFs, presentations, or general coding tasks unrelated to document generation. |
| license | Apache-2.0 |
odf-kit — OpenDocument Format for JavaScript and TypeScript
Overview
An .odt file is an OpenDocument Format text document — an ISO standard (ISO/IEC 26300) ZIP archive containing XML files. It is the native format for LibreOffice, Apache OpenOffice, Collabora, OnlyOffice, and is supported by Google Docs and Microsoft Office.
odf-kit is the only actively maintained JavaScript/TypeScript library for ODF generation. It produces spec-validated output (validated against the OASIS ODF Validator on every CI run). Works in Node.js 22+ and browsers. No LibreOffice dependency.
Quick Reference
| Task | Import | Function |
|---|
| Create new ODT document | "odf-kit" | new OdtDocument() |
| Fill existing ODT template | "odf-kit" | fillTemplate(bytes, data) |
| HTML → ODT | "odf-kit" | htmlToOdt(html, options?) |
| Markdown → ODT | "odf-kit" | markdownToOdt(markdown, options?) |
| TipTap/ProseMirror JSON → ODT | "odf-kit" | tiptapToOdt(json, options?) |
| Build ODS spreadsheet | "odf-kit" | new OdsDocument() |
| Read ODT / convert to HTML | "odf-kit/reader" | readOdt(bytes) / odtToHtml(bytes) |
| Read ODS | "odf-kit/ods-reader" | readOds(bytes) / odsToHtml(bytes) |
| XLSX → ODS | "odf-kit/xlsx" | xlsxToOds(bytes) |
| DOCX → ODT | "odf-kit/docx" | docxToOdt(bytes, options?) |
| ODT → Typst / PDF | "odf-kit/typst" | odtToTypst(bytes) |
Installation
npm install odf-kit
Requires Node.js 22 or later. ESM only.
Creating New ODT Documents
Generate .odt files with JavaScript using odf-kit's builder API.
Setup
import { OdtDocument } from "odf-kit";
import { writeFileSync } from "fs";
const doc = new OdtDocument();
doc.addHeading("Document Title", 1);
doc.addParagraph("Body text goes here.");
const bytes = await doc.save();
writeFileSync("output.odt", bytes);
Metadata
doc.setMetadata({
title: "Quarterly Report",
creator: "Jane Doe",
description: "Q4 2026 financial summary",
});
Page Layout
doc.setPageLayout({
orientation: "landscape",
width: "21cm",
height: "29.7cm",
marginTop: "2cm",
marginBottom: "2cm",
marginLeft: "2cm",
marginRight: "2cm",
});
For US Letter: Use width: "8.5in" and height: "11in".
Headers and Footers
doc.setHeader("Confidential — Page ###");
doc.setFooter("© 2026 Acme Corp — Page ###");
doc.setHeader((h) => {
h.addText("Confidential", { bold: true, color: "gray" });
h.addText(" — Page ");
h.addPageNumber();
});
Headings
doc.addHeading("Chapter Title", 1);
doc.addHeading("Section Title", 2);
doc.addHeading("Centered Title", 1, { align: "center" });
doc.addHeading("Title with Space", 1, { spaceBefore: "0.5cm", spaceAfter: "0.5cm" });
Paragraphs
doc.addParagraph("Simple paragraph.");
doc.addParagraph("Right-aligned text.", { align: "right" });
doc.addParagraph((p) => {
p.addText("Normal and ");
p.addText("bold", { bold: true });
p.addText(" text.");
});
doc.addParagraph((p) => {
p.addText("Indented paragraph content.");
}, { indentLeft: "1cm", indentFirst: "0.5cm" });
Paragraph options:
| Option | Type | Example | Description |
|---|
align | string | "left", "center", "right", "justify" | Text alignment |
spaceBefore | string | "0.5cm" | Space above paragraph |
spaceAfter | string | "0.5cm" | Space below paragraph |
lineHeight | number or string | 1.5 or "150%" | Line spacing (number = multiplier) |
indentLeft | string | "1cm" | Left indent |
indentRight | string | "1cm" | Right indent |
indentFirst | string | "0.5cm" | First-line indent (positive) or hanging indent (negative) |
tabStops | array | [{ position: "6cm", type: "right" }] | Tab stop positions |
Text Formatting
doc.addParagraph((p) => {
p.addText("Bold", { bold: true });
p.addText("Italic", { italic: true });
p.addText("Underline", { underline: true });
p.addText("Strikethrough", { strikethrough: true });
p.addText("Super", { superscript: true });
p.addText("Sub", { subscript: true });
p.addText("Arial 14pt red", { fontFamily: "Arial", fontSize: 14, color: "#CC0000" });
p.addText("Highlighted", { highlightColor: "#FFFF00" });
p.addText("UPPERCASE", { textTransform: "uppercase" });
p.addText("Small Caps", { smallCaps: true });
p.addText("Weight 300", { fontWeight: 300 });
p.addText("Weight bold", { fontWeight: "bold" });
});
All text formatting options:
| Option | Type | Example |
|---|
bold | boolean | true |
italic | boolean | true |
underline | boolean | true |
strikethrough | boolean | true |
superscript | boolean | true |
subscript | boolean | true |
fontSize | number or string | 14 or "14pt" |
fontFamily | string | "Arial" |
fontWeight | string or number | "bold", 300, 700 |
fontStyle | string | "normal", "italic" |
color | string | "#FF0000" or "red" |
highlightColor | string | "#FFFF00" or "yellow" |
textTransform | string | "uppercase", "lowercase", "capitalize" |
smallCaps | boolean | true |
Tables
doc.addTable([
["Name", "Role", "Department"],
["Alice", "Engineer", "R&D"],
["Bob", "Designer", "UX"],
]);
doc.addTable([
["Item", "Price"],
["Widget", "$9.99"],
], { columnWidths: ["8cm", "4cm"], border: "0.5pt solid #000000" });
doc.addTable((t) => {
t.addRow((r) => {
r.addCell("Header", {
bold: true,
backgroundColor: "#DDDDDD",
verticalAlign: "middle",
padding: "0.15cm",
});
r.addCell("Value", {
bold: true,
backgroundColor: "#DDDDDD",
colSpan: 2,
});
}, { backgroundColor: "#DDDDDD" });
t.addRow((r) => {
r.addCell("Status");
r.addCell("Complete", { color: "green" });
r.addCell("Verified", { border: "0.5pt solid #000000" });
});
t.addRow((r) => {
r.addCell("Tall", { rowSpan: 2, verticalAlign: "top" });
r.addCell("Normal");
});
t.addRow((r) => {
r.addCell("Adjacent to rowspan");
});
}, { columnWidths: ["4cm", "4cm", "4cm"] });
Cell options (extends all text formatting options, plus):
| Option | Type | Example |
|---|
backgroundColor | string | "#EEEEEE" |
border | string | "0.5pt solid #000000" |
borderTop / borderBottom / borderLeft / borderRight | string | Per-side borders |
colSpan | number | 2 |
rowSpan | number | 3 |
verticalAlign | string | "top", "middle", "bottom" |
padding | string | "0.15cm" |
Lists
doc.addList(["Apples", "Bananas", "Cherries"]);
doc.addList(["First", "Second", "Third"], { type: "numbered" });
doc.addList(["Item a", "Item b"], { type: "numbered", numFormat: "a" });
doc.addList(["Item A", "Item B"], { type: "numbered", numFormat: "A" });
doc.addList(["Item i", "Item ii"], { type: "numbered", numFormat: "i" });
doc.addList(["Step 5", "Step 6"], { type: "numbered", startValue: 5 });
doc.addList(["Item", "Item"], { type: "numbered", numPrefix: "(", numSuffix: ")" });
doc.addList((l) => {
l.addItem((p) => {
p.addText("Important: ", { bold: true });
p.addText("read the documentation");
});
l.addItem("Main topic");
l.addNested((sub) => {
sub.addItem("Subtopic A");
sub.addItem("Subtopic B");
});
});
List options:
| Option | Type | Example | Description |
|---|
type | string | "numbered" | "bullet" (default) or "numbered" |
numFormat | string | "a", "A", "i", "I" | Number format; default "1" |
numPrefix | string | "(" | Text before number |
numSuffix | string | ")" | Text after number; default "." |
startValue | number | 5 | Starting number |
Images
import { readFile } from "fs/promises";
const logo = await readFile("logo.png");
doc.addImage(logo, { width: "10cm", height: "6cm", mimeType: "image/png" });
doc.addParagraph((p) => {
p.addText("Logo: ");
p.addImage(logo, { width: "2cm", height: "1cm", mimeType: "image/png" });
});
Image options:
| Option | Type | Required | Description |
|---|
width | string | Yes | "10cm", "4in" |
height | string | Yes | "6cm", "3in" |
mimeType | string | Yes | "image/png", "image/jpeg" |
anchor | string | No | "as-character" or "paragraph" |
Links and Bookmarks
doc.addParagraph((p) => {
p.addText("Visit ");
p.addLink("our website", "https://example.com", { bold: true });
});
doc.addParagraph((p) => {
p.addBookmark("section-one");
p.addText("Section 1: Introduction");
});
doc.addParagraph((p) => {
p.addLink("Go to Section 1", "#section-one");
});
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" },
],
});
Page Breaks
doc.addPageBreak();
Method Chaining
Every method returns the document for 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"]])
.addList(["No incidents", "No alerts"], { type: "numbered" })
.save();
Template Filling
Fill existing .odt templates created in LibreOffice with dynamic data. Templates use {placeholder} syntax.
Basic Usage
import { fillTemplate } from "odf-kit";
import { readFileSync, writeFileSync } from "fs";
const template = readFileSync("template.odt");
const result = fillTemplate(template, {
name: "Alice",
company: "Acme Corp",
date: "2026-03-01",
});
writeFileSync("output.odt", result);
Template Syntax
| Syntax | Description | Example |
|---|
{tag} | Simple replacement | Dear {name}, |
{object.property} | Dot notation for nested data | {company.address.city} |
{#tag}...{/tag} | Loop over array | {#items}...{/items} |
{#tag}...{/tag} | Conditional (truthy/falsy) | {#showNotes}...{/showNotes} |
Loop items inherit parent data. Item properties override parent properties of the same name. Falsy values (false, null, undefined, 0, "", []) remove conditional sections.
Placeholder Healing
LibreOffice often fragments {placeholder} text across multiple XML elements due to editing history or spell-check. odf-kit automatically reassembles fragmented placeholders before replacement — no manual XML cleanup needed.
HTML → ODT
import { htmlToOdt } from "odf-kit";
const bytes = await htmlToOdt(`
<h1>Meeting Notes</h1>
<p>Attendees: <strong>Alice</strong>, Bob</p>
<ul><li>Project status</li><li>Budget review</li></ul>
<table>
<tr><th>Action</th><th>Owner</th></tr>
<tr><td>Send report</td><td>Alice</td></tr>
</table>
`, {
pageFormat: "A4",
orientation: "portrait",
metadata: { title: "Meeting Notes", creator: "Alice" },
});
Supports: headings (h1–h6), paragraphs, bold, italic, underline, lists (ordered/unordered, nested), tables, blockquotes, code blocks, horizontal rules, inline CSS (color, font-size, font-family, text-align, background-color on cells).
Markdown → ODT
import { markdownToOdt } from "odf-kit";
const bytes = await markdownToOdt(`
# Report Title
Revenue **exceeded** expectations.
## Action Items
- Send report by Friday
- Review budget
| Region | Revenue |
|--------|---------|
| North | $2.1M |
`, { pageFormat: "A4" });
Full CommonMark support. Accepts the same options as htmlToOdt.
TipTap / ProseMirror JSON → ODT
import { tiptapToOdt } from "odf-kit";
const bytes = await tiptapToOdt(editor.getJSON(), {
pageFormat: "A4",
images: { [imageUrl]: imageBytes },
unknownNodeHandler: (node, doc) => {
if (node.type === "callout") doc.addParagraph(`⚠️ ${extractText(node)}`);
},
});
No @tiptap/core dependency — walks the JSON tree directly. Supports all standard TipTap nodes and marks including tables, images, and nested lists.
Build ODS Spreadsheets
import { OdsDocument } from "odf-kit";
import { writeFileSync } from "fs";
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.addRow([{ value: 0.1234, type: "percentage", numberFormat: "percentage:1" }]);
sheet.addRow([{ value: 1234.56, type: "currency", numberFormat: "currency:EUR" }]);
sheet.addRow([{ value: "odf-kit", type: "string", href: "https://github.com/GitHubNewbie0/odf-kit" }]);
sheet.addRow([{ value: "Q1 Report", type: "string", colSpan: 3 }]);
sheet.setColumnWidth(0, "4cm");
sheet.setColumnWidth(1, "4cm");
sheet.freezeRows(1);
sheet.freezeColumns(1);
sheet.setTabColor("#4CAF50");
const bytes = await doc.save();
writeFileSync("sales.ods", bytes);
Cell value auto-types: number → float · Date → date · boolean → boolean · null/undefined → empty · string → string. Use { value, type } object for formula/percentage/currency.
Number formats: "integer" · "decimal:N" · "percentage" / "percentage:N" · "currency:CODE" / "currency:CODE:N"
Read ODT / Convert to HTML
import { readOdt, odtToHtml } from "odf-kit/reader";
import { readFileSync } from "fs";
const bytes = readFileSync("document.odt");
const doc = readOdt(bytes);
const html = odtToHtml(bytes);
const fragment = odtToHtml(bytes, { fragment: true });
for (const node of doc.body) {
if (node.kind === "heading") console.log(`H${node.level}:`, node.spans[0].text);
if (node.kind === "paragraph") console.log(node.spans.map(s => s.text).join(""));
}
console.log(doc.metadata.title);
Read ODS
import { readOds, odsToHtml } from "odf-kit/ods-reader";
import { readFileSync } from "fs";
const bytes = readFileSync("data.ods");
const model = readOds(bytes);
for (const row of model.sheets[0].rows) {
for (const cell of row.cells) {
console.log(cell.type, cell.value);
}
}
const html = odsToHtml(bytes);
XLSX → ODS
import { xlsxToOds } from "odf-kit/xlsx";
import { readFileSync, writeFileSync } from "fs";
const ods = await xlsxToOds(readFileSync("report.xlsx"));
writeFileSync("report.ods", ods);
const ods2 = await xlsxToOds(await file.arrayBuffer());
Preserves: strings, numbers, booleans, dates, formulas (cached result), merged cells, freeze panes, multiple sheets. No SheetJS dependency — own parser built on fflate.
DOCX → ODT
import { docxToOdt } from "odf-kit/docx";
import { readFileSync, writeFileSync } from "fs";
const { bytes, warnings } = await docxToOdt(readFileSync("report.docx"), {
pageFormat: "letter",
preservePageLayout: true,
styleMap: { "Section Title": 1, "Sub Title": 2 },
metadata: { title: "My Report" },
});
writeFileSync("report.odt", bytes);
if (warnings.length) console.warn(warnings);
const { bytes: odt } = await docxToOdt(await file.arrayBuffer());
Preserves: headings, paragraphs, all text formatting (bold, italic, underline, font sizes, colors), lists (bullet/numbered/nested), tables (merged cells, column widths), hyperlinks, bookmarks, images (actual dimensions), page layout, headers/footers, footnotes, tracked changes (final-text mode).
ODT → Typst / PDF
import { odtToTypst, modelToTypst } from "odf-kit/typst";
import { readOdt } from "odf-kit/reader";
import { readFileSync, writeFileSync } from "fs";
import { execSync } from "child_process";
const bytes = new Uint8Array(readFileSync("document.odt"));
const typ = odtToTypst(bytes);
writeFileSync("document.typ", typ);
execSync("typst compile document.typ document.pdf");
const model = readOdt(bytes);
const typst = modelToTypst(model);
odf-kit/typst is zero-dependency and browser-safe. The Typst CLI is only needed when compiling to PDF.
Critical Rules for odf-kit
- Always use
await with save() — it returns a Promise<Uint8Array>
- ESM only — use
import, not require. File must use .mjs extension or "type": "module" in package.json
- Use
writeFileSync (or writeFile) to write the Uint8Array to disk
fontSize as number = points — 14 means 14pt
- Images require all three options —
width, height, and mimeType are all required
fillTemplate is synchronous — returns Uint8Array directly, no await needed
### in header/footer strings is replaced with page numbers automatically
- A4 is the default page size — set explicit dimensions for US Letter (
8.5in × 11in)
fontWeight numeric — accepts CSS-style values 100–900 as well as "bold" / "normal"
lineHeight as number — 1.5 means 150% line height; strings like "150%" also accepted
docxToOdt returns { bytes, warnings } — always destructure, log warnings in development
readOds cell values are typed — cell.value is string | number | boolean | Date | null, never a display-formatted string
Complete Example
import { OdtDocument } from "odf-kit";
import { writeFileSync } from "fs";
async function createReport() {
const doc = new OdtDocument();
doc.setMetadata({ title: "Monthly Report", creator: "Operations Team" });
doc.setPageLayout({
width: "8.5in", height: "11in",
marginTop: "1in", marginBottom: "1in",
marginLeft: "1in", marginRight: "1in",
});
doc.setHeader((h) => {
h.addText("Monthly Operations Report", { bold: true });
h.addText(" — Page ");
h.addPageNumber();
});
doc.setFooter("© 2026 Acme Corp");
doc.addHeading("Executive Summary", 1, { spaceBefore: "0.5cm" });
doc.addParagraph(
"All operations performed within expected parameters.",
{ align: "justify", lineHeight: 1.5 }
);
doc.addHeading("Key Metrics", 2);
doc.addTable((t) => {
t.addRow((r) => {
["Metric", "Target", "Actual", "Status"].forEach((h) =>
r.addCell(h, { bold: true, backgroundColor: "#DDDDDD", verticalAlign: "middle" })
);
}, { backgroundColor: "#DDDDDD" });
t.addRow((r) => {
r.addCell("Uptime"); r.addCell("99.9%"); r.addCell("99.95%");
r.addCell("✓", { color: "green" });
});
}, { columnWidths: ["5cm", "3cm", "3cm", "2cm"], border: "0.5pt solid #000000" });
doc.addHeading("Action Items", 2);
doc.addList([
"Complete infrastructure audit by March 15",
"Deploy monitoring upgrade to production",
"Review disaster recovery procedures",
], { type: "numbered" });
doc.addHeading("Notes", 2);
doc.addParagraph((p) => {
p.addText("Priority: ", { bold: true, color: "red" });
p.addText("Database migration scheduled for next maintenance window. ");
p.addLink("See migration plan", "https://docs.example.com/migration");
p.addText(" for details.");
}, { indentLeft: "0.5cm" });
const bytes = await doc.save();
writeFileSync("report.odt", bytes);
}
createReport();