Loading repository data…
Loading repository data…
GitHubNewbie0 / repository
Generate, fill, and read OpenDocument Format (.odt) files in JavaScript and TypeScript. Works in Node.js and browsers. Build documents from scratch, fill existing templates with data, or convert .odt files to HTML. Create reports, invoices, contracts, and letters. The modern simple-odf alternative. No dependencies. Apache 2.0 open source
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-kit
// 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());
},
});
npm install odf-kit
Node.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.
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().
odf-kit ships an Agent Skill (SKILL.md) that teaches Claude how to generate, fill, and read .odt/.ods files correctly — current API, correct options, no guessing. Once it's loaded, you can just ask Claude for an OpenDocument file in plain English and it writes working odf-kit code itself.
Paste the contents of SKILL.md into the conversation (or link to the raw file) and ask Claude to use it for the task. This works in any Claude surface but only lasts for that conversation.
SKILL.md (and package it as a .zip if you want to bundle extra reference files).Once installed, Claude loads it automatically whenever you ask for an ODT/ODS file, a LibreOffice-compatible document, or mention OpenDocument — no need to re-explain the API each session.
Skills in Claude Code are just files on disk, no upload step:
# Personal skill (available in all your projects)
mkdir -p ~/.claude/skills/odt
cp SKILL.md ~/.claude/skills/odt/SKILL.md
# OR project skill (this repo/project only)
mkdir -p .claude/skills/odt
cp SKILL.md .claude/skills/odt/SKILL.md
Claude Code picks it up automatically the next time you ask it to create or fill an ODT/ODS file.
Upload it once via the Skills API and reference it by skill_id in the container parameter alongside the code_execution tool. See Anthropic's Skills API guide for the exact request shape.
Try it: "Create an ODT invoice with a line-item table and a bold total row" — with the skill installed, Claude reaches for odf-kit and writes the code directly instead of improvising XML by hand.
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" });
});
// 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"] });
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();
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");
});
});
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" });
});
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("introducti