odf.js 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +116 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # odf.js
2
+
3
+ [![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&logoColor=white)](https://github.com/ExaDev/odf.js) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](https://www.npmjs.com/package/odf.js) [![Release](https://img.shields.io/github/v/release/ExaDev/odf.js)](https://github.com/ExaDev/odf.js/releases/latest) [![CI](https://img.shields.io/github/actions/workflow/status/ExaDev/odf.js/ci.yml?branch=main)](https://github.com/ExaDev/odf.js/actions)
4
+
5
+ > 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.
6
+
7
+ `odf.js` is the ODF sibling of [`ooxml.js`](https://github.com/ExaDev/ooxml.js), mirroring its architecture as closely as the two, structurally unrelated formats allow: a lossless ZIP-of-XML core that round-trips any package byte-for-content-faithful, with ergonomic typed readers layered on top for convenient access. Unlike OOXML — a ZIP of parts with a relationship-file (`.rels`) mechanism and an extension-defaults-plus-overrides `[Content_Types].xml` — ODF has no relationships at all (inter-part references are direct paths/IRIs) and an exhaustive `META-INF/manifest.xml` that enumerates every part explicitly. Where OOXML runs carry formatting directly as attributes, ODF has no inline/direct formatting whatsoever: every formatting difference, however small, must be a named "automatic style" — so `odf.js` owns a style-interning subsystem (`src/styles/`) with no equivalent anywhere in `ooxml.js`.
8
+
9
+ **This package does not depend on `ooxml.js`**, even though the two do near-identical jobs for their respective formats: `ooxml.js` is a package signed, SBOM-attested, and branded exclusively around ECMA-376/OOXML, so depending on it here would be a permanently wrong signal for an OASIS-standard codec, and would force a breaking `ooxml.js` release every time an ODF-only fix needed the shared primitive layer. Instead, `odf.js` duplicates the small (~400-line) generic ZIP/XML/`Package` layer as its own code — kept deliberately structurally identical (plain, unmarked shapes, no branding) so TypeScript's structural typing makes the two packages' `Package`/`XmlNode`/`XmlElement` values freely interchangeable wherever a shared consumer (like `documents.js`) needs to treat them uniformly, without either package formally depending on the other.
10
+
11
+ Both packages **do** depend on [`document-content-model`](https://github.com/ExaDev/document-content-model), the genuinely shared canonical schema for `ContentDocument`/`LayoutDocument` — the semantic content model (paragraphs, runs, tables, shapes, slides) both an ODF and an OOXML reader ultimately produce. `odf.js`'s typed readers return the real, imported `ContentSection`/`ContentSlide`/etc. types from that package, not a structurally-similar lookalike, so a downstream consumer (`documents.js`) can run an `.odt` through the exact same layout/pagination engine it already uses for `.docx`, unmodified.
12
+
13
+ ## Status
14
+
15
+ This package is under active development. What's built and shipped:
16
+
17
+ - **Lossless core** — generic ZIP-of-XML primitives (`Package`/`XmlNode`/`XmlElement`, XML parse/build, zip/unzip, base64, the `packageCodec`/`xmlCodec` `z.codec()` pairs) with zero ODF-specific knowledge.
18
+ - **Namespaces, media types, mimetype, manifest** (`src/ns.ts`, `src/media-type.ts`, `src/mimetype.ts`, `src/manifest.ts`) — full read *and* write, including `META-INF/manifest.xml`'s exhaustive per-part enumeration and the mimetype part's mandatory first-entry/stored/uncompressed byte layout, verified against real LibreOffice-produced output.
19
+ - **Style interning** (`src/styles/`) — `StyleRegistry`: adopts a part's existing automatic styles on construction, finds-or-mints on `intern()`, fingerprints on canonical serialized properties plus parent style name (never `JSON.stringify`), and is collision-checked across all four style containers a document can have.
20
+ - **Shared typed primitives** (`src/typed/shared/`) — ODF length-unit parsing (`cm`/`mm`/`in`/`pt`/`pc`/`px`), A1-style spreadsheet cell-reference computation with repeat-count cursor advancement, colour/geometry parsing into `document-content-model`'s own types, ODF's `text:s`/`text:tab`/`text:line-break` whitespace-run decoding, the read-side style cascade (`style:default-style` → parent chain → the referenced style — one layer shorter than OOXML's, since ODF has no separate direct-formatting layer on top), and `meta.xml` reading.
21
+
22
+ Not yet built: the per-format typed readers (`readOdt`, `readOds`, `readOdp`, `readOdg`, `readOdfFormula`, `readOdbInventory`), live-view editors, and the `.odm` master-document/`.odb` database-table-export subsystems. This section will be replaced with real usage examples once those land — see the [Architecture](#architecture) section below for the intended shape, and this repository's own commit history/releases for current progress.
23
+
24
+ ## Getting started
25
+
26
+ Requires Node.js `>=20` and pnpm `11.6.0` (pinned via `packageManager` in `package.json`).
27
+
28
+ ```sh
29
+ pnpm install
30
+ ```
31
+
32
+ Install as a dependency in another project:
33
+
34
+ ```sh
35
+ pnpm add odf.js
36
+ # or
37
+ npm install odf.js
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ The lossless core — the only public surface stable enough to document with real examples right now:
43
+
44
+ ```ts
45
+ import { decodePackage, encodePackage } from 'odf.js';
46
+
47
+ // .odt / .ods / .odp bytes -> faithful JSON Package
48
+ const pkg = decodePackage(new Uint8Array(await file.arrayBuffer()));
49
+
50
+ // ...inspect pkg.parts...
51
+
52
+ // Package -> bytes (content-identical, mimetype-first/stored, manifest untouched)
53
+ const bytes = encodePackage(pkg);
54
+ ```
55
+
56
+ Manifest and mimetype, ODF's own package-identity mechanism (no relationships, unlike OOXML):
57
+
58
+ ```ts
59
+ import { readManifest, syncManifest, setDocumentMediaType, readMimetype } from 'odf.js';
60
+
61
+ const manifest = readManifest(pkg); // { entries: [{ fullPath, mediaType }, ...] }
62
+ setDocumentMediaType(pkg, 'application/vnd.oasis.opendocument.text'); // updates mimetype + manifest root entry atomically
63
+ syncManifest(pkg); // rebuilds manifest.xml to exactly match pkg's current parts
64
+ readMimetype(pkg); // 'application/vnd.oasis.opendocument.text'
65
+ ```
66
+
67
+ ## Architecture
68
+
69
+ Layered from a lossless core outward, mirroring `ooxml.js`'s own structure:
70
+
71
+ - **`src/model/`** — `Package`/`XmlNode`/`XmlElement` and friends: a duplicate-by-design copy of `ooxml.js`'s equivalent, kept structurally identical (see [Why no `ooxml.js` dependency](#why-no-ooxmljs-dependency) above).
72
+ - **`src/xml/`** — `parse.ts`/`build.ts` (XML string ⇄ `XmlNode[]` forest via `fast-xml-parser`), `fragment.ts`/`entities.ts` (production element/text-node construction and entity encoding — `odf.js` writes `manifest.xml` itself, unlike `ooxml.js`'s read-only stance on OPC relationships, so this needs to be real writing code, not test-only scaffolding), `query.ts` (shared tree-query helpers).
73
+ - **`src/zip.ts`** — takes *ordered* `[path, entry]` tuples, not a `Record`, specifically so ODF's mimetype-first/stored/uncompressed requirement doesn't depend on `Record`/`Object.keys` insertion order surviving a Zod round trip.
74
+ - **`src/package-io/`** — `write.ts` hoists a `mimetype` part first (stored) and `META-INF/manifest.xml` second, if present, before everything else in existing order — the one deliberate behavioural difference from `ooxml.js`'s own writer, and never fabricates either part as a side effect.
75
+ - **`src/manifest.ts`** — unlike `ooxml.js` (which only ever *reads* OPC relationships, leaving writing to `documents.js`), `odf.js` owns manifest read **and** write, since the manifest is ODF's one mandatory part and its correctness is exhaustive.
76
+ - **`src/styles/`** — `properties.ts` (the property-bag shape + real ODF attribute parsing), `serialize.ts` (canonical, deterministic property-bag → XML attributes), `registry.ts` (`StyleRegistry`, ODF's mandatory style-interning layer, no OOXML equivalent), `span.ts` (character-range wrapping into a formattable `text:span`, correctly splitting `text:s`/`text:tab` elements that straddle a boundary).
77
+ - **`src/typed/shared/`** — the ODF-specific typed primitives every future format reader builds on: `units.ts`, `a1.ts`, `color.ts`/`geometry.ts` (parsing into `document-content-model`'s own types, never redefining them), `style.ts` (a thin re-export — ODF's style-properties concern is fully covered by `styles/properties.ts` and the cascade below), `text.ts` (whitespace-run decoding), `cascade.ts` (the read-side style-resolution walk), `metadata.ts` (`meta.xml` reading).
78
+ - **`src/typed/odt/`, `src/typed/ods.ts`, `src/typed/odp/`, `src/typed/odg/`, `src/typed/draw/`, `src/typed/formula/`** — per-format typed readers, in progress (see [Status](#status)).
79
+
80
+ ## Why no `ooxml.js` dependency
81
+
82
+ See the top of this README — the short version: `ooxml.js`'s branding and signed SBOM make it the wrong dependency for an OASIS-standard package regardless of how much low-level code the two could share; `document-content-model` is the neutral package both actually depend on for the parts that are genuinely, permanently identical (the semantic content vocabulary), while the ZIP-of-XML primitive layer stays duplicated on purpose.
83
+
84
+ ## Conventions
85
+
86
+ - **Zod-first schema/type/guard**, matching `ooxml.js`/`document-content-model`: every model type is inferred from its Zod schema, never hand-written.
87
+ - **Recursive types use a hand-written structural guard, not `z.lazy`** — the same `z.lazy`-collapses-to-`unknown` issue `ooxml.js`'s `XmlNode` and `document-content-model`'s `ContentBlock` already work around.
88
+ - **No type assertions anywhere** — `assertionStyle: 'never'`, `noInlineConfig: true`, matching both sibling packages exactly.
89
+ - **Ground truth over memory for every ODF spec fact.** Namespace URIs, media types, style-property attribute names, and `meta.xml` element names are all verified against either the live OASIS ODF specification or real files produced by an installed LibreOffice, never assumed from pattern-matching an OOXML analogue or a remembered convention — several confirmed traps exist specifically because the "obvious" guess is wrong (see [Gotchas](#gotchas-and-quirks)).
90
+
91
+ ## Gotchas and quirks
92
+
93
+ - **Several ODF namespace URIs are not what you'd guess from the prefix.** `draw:` is `...xmlns:drawing:1.0`, not `...draw:1.0`; `number:` is `...xmlns:datastyle:1.0`, not `...number:1.0`; `fo:`/`svg:`/`smil:` are OASIS's own `*-compatible:1.0` URIs, not the real W3C namespaces those prefixes suggest. See `src/ns.ts`'s inline comments for the full, verified table.
94
+ - **`.odb`'s real media type is `application/vnd.oasis.opendocument.base`**, not `...database` — a common stale/wrong value found in some third-party documentation.
95
+ - **ODF's `dc:creator` is not "the author."** It records whoever most recently *saved* the document (Dublin Core's own definition); the original author is `meta:initial-creator`. `typed/shared/metadata.ts` maps `LayoutMetadata.author` to `meta:initial-creator`, matching the byline role `ooxml.js`'s own `DocumentMetadata.author` plays for OOXML.
96
+ - **`meta:keyword` appears once per keyword**, unlike OOXML's single comma-separated `cp:keywords` element.
97
+ - **`table:number-columns-repeated`/`table:number-rows-repeated` must be cursor-advanced, never materialized.** A real spreadsheet has trailing cells/rows with repeat counts over a million; `typed/shared/a1.ts`'s cursor advances in O(1) without allocating that many objects — tested against a real repeat count taken from a genuine LibreOffice template.
98
+ - **ODF cells carry no explicit cell-reference attribute at all** (unlike xlsx's `r="B7"`) — `typed/shared/a1.ts` computes A1-style references from a running column/row cursor as a reader walks cells in document order.
99
+
100
+ ## Release and publishing
101
+
102
+ `.github/workflows/ci.yml` runs commitlint, lint, typecheck, the unit suite, and the smoke test on every push and pull request. On a push to `main` where those all pass, `release.config.ts` drives [semantic-release](https://semantic-release.gitbook.io/semantic-release): commit history since the last tag decides the version bump, `CHANGELOG.md` and `package.json` are committed back to `main`, a GitHub Release is cut, and the package publishes to [npmjs.org](https://www.npmjs.com/package/odf.js) — via npm's OIDC trusted publishing, so no `NPM_TOKEN` exists anywhere in the pipeline. A further job republishes the same build under the scoped `@exadev/odf.js` alias to GitHub Packages, and another signs an SPDX SBOM and build-provenance attestation against the exact release tarball.
103
+
104
+ ## Contributing
105
+
106
+ Commits follow Conventional Commits (`feat:`, `fix:`, `test:`, `chore:`, …), enforced by commitlint via a husky `commit-msg` hook and a CI `commitlint` job. A husky `pre-commit` hook runs `lint-staged` (`eslint --fix` on staged `*.ts` files) and `pre-push` runs the test suite. There is a single `main` branch and no open pull request workflow established so far.
107
+
108
+ ## References
109
+
110
+ - [ooxml.js](https://github.com/ExaDev/ooxml.js) — the sibling package doing the equivalent lossless-codec job for OOXML (docx/pptx/xlsx). Architecturally mirrored, deliberately not depended on — see [Why no `ooxml.js` dependency](#why-no-ooxmljs-dependency).
111
+ - [document-content-model](https://github.com/ExaDev/document-content-model) — the canonical `ContentDocument`/`LayoutDocument` schema pivot both this package and `ooxml.js` depend on.
112
+ - [documents.js](https://github.com/ExaDev/documents.js) — the intended downstream consumer, adding ODF ⇄ PDF conversion and a live-view ODF editor once this package's typed readers land.
113
+
114
+ ## License
115
+
116
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "odf.js",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Type-safe, lossless round-trip conversion between OpenDocument Format packages (odt, ods, odp) and JSON, hand-written and dependency-minimal, built on Zod 4 codecs.",
5
5
  "type": "module",
6
6
  "repository": {