Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Each converts one document format to and from the shared schema, built on `docum
| Package | Formats |
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`ooxml.js`](packages/ooxml.js/README.md) | OOXML packages (docx, pptx, xlsx) to and from JSON. |
| [`odf.js`](packages/odf.js/README.md) | OpenDocument packages (odt, ods, odp) to and from JSON, plus read support for the pre-OASIS OpenOffice.org 1.x documents ODF was based on (sxw, sxc, sxi, sxd) and a write path for sxw. |
| [`odf.js`](packages/odf.js/README.md) | OpenDocument packages (odt, ods, odp) to and from JSON, plus read support for the pre-OASIS OpenOffice.org 1.x documents ODF was based on (sxw, sxc, sxi, sxd) and write paths for sxw and sxc. |
| [`markdown-codec`](packages/markdown-codec/README.md) | CommonMark+GFM to and from the shared content schema. |
| [`pdf-codec`](packages/pdf-codec/README.md) | Parses arbitrary real-world PDFs and generates new ones, also depending on `byte-codec`. |
| [`epub-codec`](packages/epub-codec/README.md) | Flowable EPUB 2/3 to and from the shared content schema; writes EPUB 3 only. |
Expand Down
23 changes: 16 additions & 7 deletions packages/odf.js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&logoColor=white)](https://github.com/ExaDev/documents.js/tree/main/packages/odf.js) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](https://www.npmjs.com/package/odf.js) [![npm version](https://img.shields.io/npm/v/odf.js)](https://www.npmjs.com/package/odf.js) [![CI](https://img.shields.io/github/actions/workflow/status/ExaDev/documents.js/ci.yml?branch=main)](https://github.com/ExaDev/documents.js/actions)

> A hand-written, dependency-minimal codec for the OpenDocument Format (ODF — OASIS/ISO 26300): `.odt`/`.ods`/`.odp`/`.odg`/`.odf`/`.odb`/`.odm` and their template variants, built on [Zod 4](https://zod.dev) codecs — plus read support for the pre-OASIS OpenOffice.org 1.x / StarOffice 6-7 documents ODF was based on (`.sxw`/`.sxc`/`.sxi`/`.sxd`), with a real `.sxw` writer alongside it.
> A hand-written, dependency-minimal codec for the OpenDocument Format (ODF — OASIS/ISO 26300): `.odt`/`.ods`/`.odp`/`.odg`/`.odf`/`.odb`/`.odm` and their template variants, built on [Zod 4](https://zod.dev) codecs — plus read support for the pre-OASIS OpenOffice.org 1.x / StarOffice 6-7 documents ODF was based on (`.sxw`/`.sxc`/`.sxi`/`.sxd`), with real `.sxw` and `.sxc` writers alongside it.

`odf.js` is the ODF sibling of [`ooxml.js`](../ooxml.js/README.md), mirroring its architecture: a lossless ZIP-of-XML core that round-trips any package byte-for-content-faithful, with typed readers layered on top. Two ODF-specific differences shape the design: ODF has no relationship mechanism (inter-part references are direct paths, with an exhaustive `META-INF/manifest.xml`), and ODF has no inline/direct formatting — every formatting difference must be a named "automatic style," so `odf.js` owns a style-interning subsystem (`src/styles/`) with no OOXML equivalent.

Expand Down Expand Up @@ -65,7 +65,7 @@ Under active development. Built and shipped:
- **`readOdbInventory`** — resolves a `.odb` into connection info, table names, query definitions (`{ name, command, escapeProcessing? }` with real SQL text), and form/report `{ name, href }` pairs. A sub-document directory is named after an opaque _persistent_ name (`forms/Obj11`), not the user-visible name.
- **`readOdbForm`/`readOdbReport`** — extract one sub-document's _static structure_, executing nothing: a form's control tree and data bindings, or a report's band stack, recursive group tree, bound fields, and computed expressions.
- **OpenOffice.org 1.x / StarOffice 6-7 reading** (`readSxw`/`readSxc`/`readSxi`/`readSxd` and their `*Content` siblings, plus `transformOoo1Package` and `isOoo1Package`) — the pre-OASIS ancestor ODF 1.0 was based on, read through the ODF readers above rather than beside them. See [Reading and writing an OpenOffice.org 1.x document](#reading-and-writing-an-openofficeorg-1x-document).
- **OpenOffice.org 1.x writing** (`writeSxw`/`writeSxwContent`, plus `transformToOoo1Package`, the read-side transform's own inverse) — `.sxw` only, built on `writeOdt`/`writeOdtContent`: `.sxc`/`.sxi`/`.sxd` have no writer yet. `.sxc` now has a real `writeOds`/`writeOdsContent` to build one on (the same relationship `.sxw` already has to `writeOdt`/`writeOdtContent`); `.sxi`/`.sxd` still have no `writeOdp`/`writeOdg` underneath them.
- **OpenOffice.org 1.x writing** (`writeSxw`/`writeSxwContent` and `writeSxc`/`writeSxcContent`, plus `transformToOoo1Package`, the read-side transform's own inverse) — `.sxw`, built on `writeOdt`/`writeOdtContent`, and `.sxc`, built on `writeOds`/`writeOdsContent`. `.sxi`/`.sxd` still have no writer, since this package's typed layer has no `writeOdp`/`writeOdg` underneath them yet.
- **The odt writer, at the same two levels** — `writeOdt` takes the `DocumentTree` `readOdt` returns and `writeOdtContent` the flat `ContentDocument` `readOdtContent` returns, and both produce a real `.odt` `Package` (`encodePackage` turns it into bytes). Paragraphs, headings, runs with character formatting and hyperlinks, whitespace, lists, tables, images, explicit page breaks, per-section page geometry, and `meta.xml` all round-trip; the fidelity constructs and embedded objects are refused by name rather than silently dropped. See [Writing a document](#writing-a-document).
- **The ods writer, at the same two levels** — `writeOds`/`writeOdsContent`, the genuine inverse of `readOds`/`readOdsContent`. Every `office:value-type` a cell can carry (float/percentage/currency/boolean/date/time/string, plus a value-less cell), column widths, row heights, hidden rows/columns, merged ranges, cell background/borders/alignment/vertical-alignment, verbatim formulas, cell-anchored images, and print settings (page geometry, gridlines/headers, page order, scale/fit-to-page, print range, repeated header rows/columns, manual page breaks) all round-trip. Embedded objects, data-validation rules, and conditional-formatting rules are refused by name — `readOdsContent` has no write-side counterpart for any of the three yet. See [Writing a document](#writing-a-document).

Expand Down Expand Up @@ -249,20 +249,29 @@ None of these is a second reader. Each is `readOdt`/`readOds`/`readOdp`/`readOdg

`transformOoo1Package` is exported for a caller that wants the transformed `Package` rather than a read of it, and returns anything that is not an OpenOffice.org 1.x package unchanged; `isOoo1Package` is the same detection on its own, decided by the namespace URIs the package's parts declare rather than by a file extension or a manifest media type. `OOO1_NAMESPACES`, `OOO1_MEDIA_TYPES`, `ooo1MediaTypeForExtension` and `odfMediaTypeForOoo1MediaType` expose the format's own namespace and media-type tables.

`.sxw` has a real writer, built the same way the reader is — as a transform either side of the ODF writer, not a second writer of its own:
`.sxw` and `.sxc` each have a real writer, built the same way the reader is — as a transform either side of the ODF writer, not a second writer of its own:

```ts
import { writeSxw, writeSxwContent, encodePackage } from "odf.js";
import {
writeSxw,
writeSxwContent,
writeSxc,
writeSxcContent,
encodePackage,
} from "odf.js";

const pkg = writeSxw(document); // a wordprocessing DocumentTree -> a real .sxw Package
const bytes = encodePackage(pkg); // Package -> bytes

const pkgFromContent = writeSxwContent(contentDocument); // the flat ContentDocument level, same shape writeOdtContent returns

const sxcPkg = writeSxc(spreadsheetTree); // a spreadsheet DocumentTree -> a real .sxc Package
const sxcPkgFromContent = writeSxcContent(spreadsheetContentDocument); // the flat ContentDocument level, same shape writeOdsContent returns
```

`writeSxw`/`writeSxwContent` call `writeOdt`/`writeOdtContent` to build a real ODF `.odt` `Package`, then run it through `transformToOoo1Package` — `transformOoo1Package`'s own inverse, reversing every rename and restructure the read-side transform documents (namespace URIs, the `office:class` genre wrap/unwrap, the `style:properties` typed-family split/merge, the `draw:frame` wrap/unwrap, the renamed elements and attributes, the `"inch"`/`"in"` unit spelling, and the package-level mimetype/manifest handling) against the same LibreOffice transformer source and OpenOffice.org DTD the forward direction is grounded against. The result genuinely declares OpenOffice.org 1.x namespace URIs, carries no `mimetype` part, and reads back correctly through the ordinary `readSxw`/`readSxwContent` — `readSxw(writeSxw(document))` recovers `document` up to the exact same canonical form `normaliseOdtContent` already states for `writeOdt`, since `writeSxwContent` is `writeOdtContent`'s own output run one transform further. What `writeOdt` refuses (the fidelity constructs — fields, bookmarks, notes, annotations, tracked changes, divisions, index wrappers, forms — and embedded objects), `writeSxw` refuses too, for the same reason: a document that silently lost semantic content would be worse than one this writer declined to produce at all.
`writeSxw`/`writeSxwContent` call `writeOdt`/`writeOdtContent` to build a real ODF `.odt` `Package`; `writeSxc`/`writeSxcContent` call `writeOds`/`writeOdsContent` to build a real ODF `.ods` `Package` the identical way. Both then run their package through `transformToOoo1Package` — `transformOoo1Package`'s own inverse, reversing every rename and restructure the read-side transform documents (namespace URIs, the `office:class` genre wrap/unwrap, the `style:properties` typed-family split/merge, the `draw:frame` wrap/unwrap, the renamed elements and attributes including a cell's `office:value-*` family becoming `table:value-*`, the `"inch"`/`"in"` unit spelling, and the package-level mimetype/manifest handling) against the same LibreOffice transformer source and OpenOffice.org DTD the forward direction is grounded against. Since `transformToOoo1Package` is itself generic across every ODF media type rather than `.odt`-specific, wiring `.sxc` up to it needed no changes to the transform at all — only a second pair of writer entry points wrapping `writeOds`/`writeOdsContent` the way `writeSxw`/`writeSxwContent` already wrap `writeOdt`/`writeOdtContent`. The result genuinely declares OpenOffice.org 1.x namespace URIs, carries no `mimetype` part, and reads back correctly through the ordinary readers — `readSxw(writeSxw(document))` recovers `document` up to the exact same canonical form `normaliseOdtContent` already states for `writeOdt`, and `readSxc(writeSxc(document))` recovers `document` up to the canonical form `normaliseOdsContent` already states for `writeOds`, since each `*Content` writer here is its ODF counterpart's own output run one transform further. What `writeOdt`/`writeOds` refuse (the odt fidelity constructs — fields, bookmarks, notes, annotations, tracked changes, divisions, index wrappers, forms; the ods embedded objects, data-validation rules, and conditional-formatting rules), `writeSxw`/`writeSxc` refuse too, for the same reason: a document that silently lost semantic content would be worse than one this writer declined to produce at all.

`.sxc`/`.sxi`/`.sxd` have no writer yet — this package's typed layer has no `writeOds`/`writeOdp`/`writeOdg` for one to be built on; only `.odt`/`.sxw` do. See [What differs between the two vocabularies](#what-differs-between-the-two-vocabularies) for what the transform covers, and its own module comment (`src/ooo1/transform.ts`) for the full list, including the reverse direction's own note (`transformToOoo1Package`) on the package-wide context (a document's `office:class`, a list's ordered/bullet kind) the reverse needs that the forward direction never did.
`.sxi`/`.sxd` still have no writer — this package's typed layer has no `writeOdp`/`writeOdg` for one to be built on. See [What differs between the two vocabularies](#what-differs-between-the-two-vocabularies) for what the transform covers, and its own module comment (`src/ooo1/transform.ts`) for the full list, including the reverse direction's own note (`transformToOoo1Package`) on the package-wide context (a document's `office:class`, a list's ordered/bullet kind) the reverse needs that the forward direction never did.

### What differs between the two vocabularies

Expand Down Expand Up @@ -304,7 +313,7 @@ Layered from a lossless core outward, mirroring `ooxml.js`:
- **`src/typed/draw/`** — the shared `draw:frame`/`draw:g`/vector shape vocabulary and `readDrawImageBlock` (`shapes.ts`), plus `embedded.ts` (`readDrawObjectReference`, `readEmbeddedObjectDocument`, `readOdfChartContent` — the shared embedded-object reference resolver and the central kind→reader dispatch table).
- **`src/typed/formula/`, `odm/`** — `readOdfFormula`/`readOdfFormulaContent`/`readOdfFormulaMathMl` and `readOdm`.
- **`src/typed/odb/`** — `readOdbInventory`, `readOdbForm`/`readOdbReport`, `resolveOdbComponent`, `subDocumentPackage`.
- **`src/ooo1/`** — the OpenOffice.org 1.x variant reader and writer: `ns.ts` (the pre-OASIS namespace and `application/vnd.sun.xml.*` media-type tables plus package detection, in both directions), `properties.ts` (the `style:properties` split, and `mergeStyleProperties`, its own inverse), `transform.ts` (the whole package rewrite, `transformOoo1Package` and its inverse `transformToOoo1Package`), `read.ts` (`readSxw`/`readSxc`/`readSxi`/`readSxd`), `write.ts` (`writeSxw`/`writeSxwContent`). Sits _beside_ `typed/`, not inside it: it adds no reader or writer of its own for the ODF content model, it feeds `writeOdt`'s output into `transformToOoo1Package` and the ODF readers' input through `transformOoo1Package`.
- **`src/ooo1/`** — the OpenOffice.org 1.x variant reader and writer: `ns.ts` (the pre-OASIS namespace and `application/vnd.sun.xml.*` media-type tables plus package detection, in both directions), `properties.ts` (the `style:properties` split, and `mergeStyleProperties`, its own inverse), `transform.ts` (the whole package rewrite, `transformOoo1Package` and its inverse `transformToOoo1Package`), `read.ts` (`readSxw`/`readSxc`/`readSxi`/`readSxd`), `write.ts` (`writeSxw`/`writeSxwContent`/`writeSxc`/`writeSxcContent`). Sits _beside_ `typed/`, not inside it: it adds no reader or writer of its own for the ODF content model, it feeds `writeOdt`'s/`writeOds`'s own output into `transformToOoo1Package` and the ODF readers' input through `transformOoo1Package`.

## Conventions

Expand Down
11 changes: 8 additions & 3 deletions packages/odf.js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ export {
} from "./typed/shared/forms";
export type { OdbFormDefinition, OdbFormControl } from "./typed/shared/forms";

// --- OpenOffice.org 1.x / StarOffice 6-7, the pre-OASIS ancestor ODF 1.0 was based on. Read-only, and read through the ODF readers above rather than beside them: transformOoo1Package rewrites a .sxw/.sxc/.sxi/.sxd package into the ODF shape those readers already understand, so every construct they know how to read works on an OpenOffice.org 1.x document too. See src/ooo1/transform.ts for what actually differs between the two vocabularies. ---
// --- OpenOffice.org 1.x / StarOffice 6-7, the pre-OASIS ancestor ODF 1.0 was based on. Read through the ODF readers above rather than beside them: transformOoo1Package rewrites a .sxw/.sxc/.sxi/.sxd package into the ODF shape those readers already understand, so every construct they know how to read works on an OpenOffice.org 1.x document too. .sxw and .sxc also write, the same way: transformToOoo1Package rewrites a real ODF Package (writeOdt's/writeOds's own output) into genuine OpenOffice.org 1.x XML; .sxi/.sxd remain read-only, since this package's typed layer has no writeOdp/writeOdg yet. See src/ooo1/transform.ts for what actually differs between the two vocabularies. ---
export {
OOO1_NAMESPACES,
OOO1_MEDIA_TYPES,
Expand All @@ -338,8 +338,13 @@ export {
readSxdContent,
} from "./ooo1/read";

// The .sxw writer -- the OpenOffice.org 1.x / StarOffice 6-7 counterpart to writeOdt/writeOdtContent above, built on them: writeOdt/writeOdtContent produce a real ODF Package, and transformToOoo1Package (this format's own inverse of transformOoo1Package, the same module the readers above run) rewrites it into genuine OpenOffice.org 1.x XML. See src/ooo1/write.ts for the full scope statement -- .sxc/.sxi/.sxd have no writer yet, since this package's typed layer has no writeOds/writeOdp/writeOdg for one to be built on.
export { writeSxw, writeSxwContent } from "./ooo1/write";
// The .sxw and .sxc writers -- the OpenOffice.org 1.x / StarOffice 6-7 counterparts to writeOdt/writeOdtContent and writeOds/writeOdsContent above, built on them: those produce a real ODF Package, and transformToOoo1Package (this format's own inverse of transformOoo1Package, the same module the readers above run) rewrites it into genuine OpenOffice.org 1.x XML. See src/ooo1/write.ts for the full scope statement -- .sxi/.sxd have no writer yet, since this package's typed layer has no writeOdp/writeOdg for one to be built on.
export {
writeSxw,
writeSxwContent,
writeSxc,
writeSxcContent,
} from "./ooo1/write";

export { readOdbReport } from "./typed/odb/report";
export type {
Expand Down
Loading