"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."
Resources
15Install
npx skillscat add githubnewbie0/odf-kit Install via the SkillsCat registry.
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-kitRequires 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", // or "portrait" (default)
width: "21cm", // default A4
height: "29.7cm",
marginTop: "2cm",
marginBottom: "2cm",
marginLeft: "2cm",
marginRight: "2cm",
});For US Letter: Use width: "8.5in" and height: "11in".
Headers and Footers
// Simple string (### = page number)
doc.setHeader("Confidential — Page ###");
doc.setFooter("© 2026 Acme Corp — Page ###");
// Builder for formatted headers
doc.setHeader((h) => {
h.addText("Confidential", { bold: true, color: "gray" });
h.addText(" — Page ");
h.addPageNumber();
});Headings
// Basic heading (levels 1–6)
doc.addHeading("Chapter Title", 1);
doc.addHeading("Section Title", 2);
// With paragraph options
doc.addHeading("Centered Title", 1, { align: "center" });
doc.addHeading("Title with Space", 1, { spaceBefore: "0.5cm", spaceAfter: "0.5cm" });Paragraphs
// Plain text
doc.addParagraph("Simple paragraph.");
// With paragraph options
doc.addParagraph("Right-aligned text.", { align: "right" });
// Builder for mixed formatting
doc.addParagraph((p) => {
p.addText("Normal and ");
p.addText("bold", { bold: true });
p.addText(" text.");
});
// Builder with paragraph options
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 }); // numeric 100–900
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
// Simple — array of arrays
doc.addTable([
["Name", "Role", "Department"],
["Alice", "Engineer", "R&D"],
["Bob", "Designer", "UX"],
]);
// With options
doc.addTable([
["Item", "Price"],
["Widget", "$9.99"],
], { columnWidths: ["8cm", "4cm"], border: "0.5pt solid #000000" });
// Full control — builder callback
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" });
});
// Row spanning
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
// Simple bullet list
doc.addList(["Apples", "Bananas", "Cherries"]);
// Numbered list
doc.addList(["First", "Second", "Third"], { type: "numbered" });
// Alpha / roman / custom formats
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: ")" });
// Nested with formatting — builder callback
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");
// Standalone
doc.addImage(logo, { width: "10cm", height: "6cm", mimeType: "image/png" });
// Inline in text
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
// External URL
doc.addParagraph((p) => {
p.addText("Visit ");
p.addLink("our website", "https://example.com", { bold: true });
});
// Internal bookmark target
doc.addParagraph((p) => {
p.addBookmark("section-one");
p.addText("Section 1: Introduction");
});
// Link to internal bookmark
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", // "A4" | "letter" | "legal" | "A3" | "A5"
orientation: "portrait", // "portrait" | "landscape"
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";
// editor.getJSON() returns TipTap JSONContent
const bytes = await tiptapToOdt(editor.getJSON(), {
pageFormat: "A4",
images: { [imageUrl]: imageBytes }, // pre-fetched image bytes
unknownNodeHandler: (node, doc) => { // handle custom extensions
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); // freeze top row
sheet.freezeColumns(1); // freeze left column
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);
// Render to HTML string
const html = odtToHtml(bytes); // full HTML document
const fragment = odtToHtml(bytes, { fragment: true }); // body content only
// Walk the structured document model
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");
// Parse to structured model — typed JavaScript values
const model = readOds(bytes);
for (const row of model.sheets[0].rows) {
for (const cell of row.cells) {
console.log(cell.type, cell.value);
// type: "string" | "float" | "boolean" | "date" | "formula" | "empty" | "covered"
// value: string | number | boolean | Date | null — never a display-formatted string
}
}
// Or convert to an HTML table
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);
// Also works in browser
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", // override page format (default: read from DOCX)
preservePageLayout: true, // read size/margins from DOCX sectPr (default: true)
styleMap: { "Section Title": 1, "Sub Title": 2 }, // custom style → heading level
metadata: { title: "My Report" }, // override metadata
});
writeFileSync("report.odt", bytes);
if (warnings.length) console.warn(warnings); // log during development
// Also works in browser — pure ESM, zero new dependencies
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"));
// One-call convenience
const typ = odtToTypst(bytes);
writeFileSync("document.typ", typ);
execSync("typst compile document.typ document.pdf"); // Typst CLI installed separately
// Parse once, emit to multiple formats
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
awaitwithsave()— it returns aPromise<Uint8Array> - ESM only — use
import, notrequire. File must use.mjsextension or"type": "module"inpackage.json - Use
writeFileSync(orwriteFile) to write theUint8Arrayto disk fontSizeas number = points —14means 14pt- Images require all three options —
width,height, andmimeTypeare all required fillTemplateis synchronous — returnsUint8Arraydirectly, noawaitneeded###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) fontWeightnumeric — accepts CSS-style values 100–900 as well as"bold"/"normal"lineHeightas number —1.5means 150% line height; strings like"150%"also accepteddocxToOdtreturns{ bytes, warnings }— always destructure, log warnings in developmentreadOdscell values are typed —cell.valueisstring | 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();