Skip to content

GitHubNewbie0/odf-kit

Repository files navigation

odf-kit

Generate, fill, read, and convert OpenDocument Format files (.odt) in TypeScript and JavaScript. Works in Node.js and browsers. No LibreOffice dependency — pure spec-compliant ODF.

Documentation & examples →

npm install odf-kit

Four ways to work with ODT files

// 1. Build a 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. 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);
// 3. 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
// 4. 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");

Installation

npm install odf-kit

Node.js 22+ required. ESM only. Three sub-exports:

import { OdtDocument, fillTemplate } from "odf-kit";          // build + fill
import { readOdt, odtToHtml }        from "odf-kit/reader";   // read + convert to HTML
import { odtToTypst, modelToTypst }  from "odf-kit/typst";    // convert to Typst

Works in Node.js, browsers, Deno, Bun, and Cloudflare Workers. The only runtime dependency is fflate for ZIP packaging — no transitive dependencies.


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: programmatic document creation

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" });
});

In a browser, use fetch() or a file input instead of readFile():

const response = await fetch("logo.png");
const logo = new Uint8Array(await response.arrayBuffer());

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();

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}
fillTemplate(template, {
  items: [
    { product: "Widget", qty: 5, price: "$125" },
    { product: "Gadget", qty: 3, price: "$120" },
  ],
});

Conditionals

{#showDiscount}
You qualify for a {percent}% discount!
{/showDiscount}

Falsy values (false, null, undefined, 0, "", []) remove the block. Truthy values include it. Loops and conditionals nest freely.

How it works

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.

Template syntax follows Mustache conventions, established for document templating by docxtemplater. odf-kit's engine is a clean-room implementation built for ODF — no code from either project was used.


Read: ODT document model

odf-kit/reader parses .odt files into a structured model and renders to HTML.

import { readOdt, odtToHtml } from "odf-kit/reader";
import { readFileSync } from "fs";

const bytes = readFileSync("report.odt");

// Structured model
const model = readOdt(bytes);
console.log(model.body);        // BodyNode[]
console.log(model.pageLayout);  // PageLayout
console.log(model.header);      // HeaderFooterContent

// Styled HTML
const html = odtToHtml(bytes);

// With tracked changes mode
const final    = odtToHtml(bytes, {}, { trackedChanges: "final" });
const original = odtToHtml(bytes, {}, { trackedChanges: "original" });
const marked   = odtToHtml(bytes, {}, { trackedChanges: "changes" });

What the reader extracts

Tier 1 — Structure: paragraphs, headings, tables, lists, images, notes, bookmarks, fields, hyperlinks, tracked changes (all three ODF-defined modes: final/original/changes).

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.

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).

Document model types

import type {
  OdtDocumentModel,
  BodyNode,          // ParagraphNode | HeadingNode | TableNode | ListNode |
                     // ImageNode | SectionNode | TrackedChangeNode
  ParagraphNode,
  HeadingNode,
  TableNode,
  ListNode,
  ImageNode,
  SectionNode,
  TrackedChangeNode,
  InlineNode,        // TextNode | SpanNode | ImageNode | NoteNode |
                     // BookmarkNode | FieldNode | LinkNode
  PageLayout,
  ReadOdtOptions,
} from "odf-kit/reader";

Typst: ODT to PDF

odf-kit/typst converts .odt files to Typst markup for PDF generation. No LibreOffice, no headless browser — just the Typst CLI.

import { odtToTypst, modelToTypst } from "odf-kit/typst";
import { readFileSync, writeFileSync } from "fs";
import { execSync } from "child_process";

// Convenience wrapper — ODT bytes → Typst string
const typst = odtToTypst(readFileSync("letter.odt"));
writeFileSync("letter.typ", typst);
execSync("typst compile letter.typ letter.pdf");

// From a model (if you already have one from readOdt)
import { readOdt } from "odf-kit/reader";
const model  = readOdt(readFileSync("letter.odt"));
const typst2 = modelToTypst(model);

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.

Tracked changes in Typst output

import type { TypstEmitOptions } from "odf-kit/typst";

const options: TypstEmitOptions = { trackedChanges: "final" };     // accepted text only
const options2: TypstEmitOptions = { trackedChanges: "original" }; // before changes
const options3: TypstEmitOptions = { trackedChanges: "changes" };  // annotated markup

See the complete ODT to PDF with Typst guide for installation, font setup, and real-world examples.


API Reference

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>

fillTemplate

function fillTemplate(templateBytes: Uint8Array, data: TemplateData): Uint8Array

TemplateData is Record<string, unknown> — any JSON-serializable value.

Syntax Description
{tag} Replace with value
{object.property} Dot notation for nested objects
{#tag}...{/tag} Loop (array) or conditional (truthy/falsy)

readOdt / odtToHtml

function readOdt(bytes: Uint8Array, options?: ReadOdtOptions): OdtDocumentModel
function odtToHtml(
  bytes: Uint8Array,
  htmlOptions?: HtmlOptions,
  readOptions?: ReadOdtOptions
): string

odtToTypst / modelToTypst

function odtToTypst(bytes: Uint8Array, options?: TypstEmitOptions): string
function modelToTypst(model: OdtDocumentModel, options?: TypstEmitOptions): string

TextFormatting

{
  bold?: boolean,
  italic?: boolean,
  fontSize?: number | string,    // 12 or "12pt"
  fontFamily?: string,
  color?: string,                // "#FF0000" or "red"
  underline?: boolean,
  strikethrough?: boolean,
  superscript?: boolean,
  subscript?: boolean,
  highlightColor?: string,
}

TableOptions / CellOptions

// TableOptions
{ columnWidths?: string[], border?: string }

// CellOptions (extends TextFormatting)
{
  backgroundColor?: string,
  border?: string,
  borderTop?: string, borderBottom?: string,
  borderLeft?: string, borderRight?: string,
  colSpan?: number,
  rowSpan?: number,
}

PageLayout

{
  width?: string,           // "21cm" (A4 default)
  height?: string,          // "29.7cm"
  orientation?: "portrait" | "landscape",
  marginTop?: string,       // "2cm" default
  marginBottom?: string,
  marginLeft?: string,
  marginRight?: 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, guaranteeing cross-platform compatibility.


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.

  • Single runtime dependency — fflate for ZIP. No transitive dependencies.
  • Spec-compliant output — every generated file passes the OASIS ODF validator. Enforced on every commit by CI.
  • Four complete capability modes — build, fill, read, convert. Not just generation.
  • Zero-dependency Typst emitter — the only JavaScript library with built-in ODT→Typst conversion for PDF generation.
  • TypeScript-first — full types across all three 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
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 (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.


Version history

v0.8.0odf-kit/typst sub-export: odtToTypst() and modelToTypst(). Zero-dependency ODT→Typst emitter for PDF generation via Typst CLI. 650+ tests passing.

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.

v0.6.0 — Tier 2 reader: span styles, image float/wrap, footnotes/endnotes, bookmarks, fields, cell/row styles, full style inheritance.

v0.5.0odf-kit/reader sub-export: readOdt(), odtToHtml(). Tier 1: paragraphs, headings, tables, lists, images, notes, tracked changes.

v0.4.0 — Generation repair: 16 spec compliance gaps fixed, OASIS ODF validator added to CI.

v0.3.0 — Template engine: loops, conditionals, dot notation, automatic XML fragment healing.

v0.2.0 — Migrated to fflate (zero transitive dependencies).

v0.1.0 — Programmatic ODT creation: text, tables, page layout, lists, images, links, bookmarks.


Guides

Full walkthroughs and real-world examples on the documentation site:


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 test

Full pipeline before submitting a PR:

npm run format:check
npm run lint
npm run build
npm test

Acknowledgments

Template syntax follows Mustache conventions, established for document templating by docxtemplater. odf-kit's engine is a clean-room implementation purpose-built for ODF — no code from either project was used.


License

Apache 2.0 — see LICENSE for details.

About

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

Topics

Resources

License

Stars

Watchers

Forks

Packages