odf.js 6.2.1 → 6.4.0
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.
- package/README.md +191 -17
- package/dist/{cascade-BDkyOmoo.d.cts → cascade-BpMwrjkB.d.cts} +1 -1
- package/dist/{cascade-CaVinmvp.d.ts → cascade-PDS3hgrP.d.ts} +1 -1
- package/dist/{codec-B72LApRl.d.cts → codec-CFdlbFLJ.d.cts} +21 -13
- package/dist/{codec-rbZGlr37.d.ts → codec-DTAgph2r.d.ts} +21 -13
- package/dist/codec.d.cts +1 -1
- package/dist/codec.d.ts +1 -1
- package/dist/{embedded-UQQNFd5U.js → embedded-BnavfkP-.js} +4 -5
- package/dist/{embedded-BsO-SvtW.cjs → embedded-Bxyss_ck.cjs} +15 -4
- package/dist/index.cjs +53 -1
- package/dist/index.d.cts +24 -16
- package/dist/index.d.ts +24 -16
- package/dist/index.js +19 -11
- package/dist/list-7FO0SDqZ.d.ts +32 -0
- package/dist/list-BorrTJBY.d.cts +32 -0
- package/dist/metadata-BUzwE_-u.d.ts +10 -0
- package/dist/metadata-CYXgXL_h.d.cts +10 -0
- package/dist/{ns-C14w-Zhf.d.cts → ns-DrleaIZQ.d.cts} +3 -1
- package/dist/{ns-CTyKJZeF.d.ts → ns-E0TfX9tZ.d.ts} +3 -1
- package/dist/ooo1/ns.cjs +9 -0
- package/dist/ooo1/ns.d.cts +2 -2
- package/dist/ooo1/ns.d.ts +2 -2
- package/dist/ooo1/ns.js +8 -1
- package/dist/ooo1/properties.cjs +30 -0
- package/dist/ooo1/properties.d.cts +6 -2
- package/dist/ooo1/properties.d.ts +6 -2
- package/dist/ooo1/properties.js +30 -1
- package/dist/ooo1/read.cjs +1 -1
- package/dist/ooo1/read.d.cts +1 -1
- package/dist/ooo1/read.d.ts +1 -1
- package/dist/ooo1/read.js +1 -1
- package/dist/ooo1/transform.cjs +290 -0
- package/dist/ooo1/transform.d.cts +2 -2
- package/dist/ooo1/transform.d.ts +2 -2
- package/dist/ooo1/transform.js +293 -4
- package/dist/ooo1/write.cjs +40 -0
- package/dist/ooo1/write.d.cts +2 -0
- package/dist/ooo1/write.d.ts +2 -0
- package/dist/ooo1/write.js +32 -0
- package/dist/package-io/scaffold.cjs +65 -0
- package/dist/package-io/scaffold.d.cts +2 -0
- package/dist/package-io/scaffold.d.ts +2 -0
- package/dist/package-io/scaffold.js +62 -0
- package/dist/paragraph-Bf6Iq5wC.d.cts +28 -0
- package/dist/paragraph-DtC86tft.d.ts +28 -0
- package/dist/{path-CK0dIPgx.d.cts → path-8HQS-J7y.d.cts} +3 -1
- package/dist/{path-CK0dIPgx.d.ts → path-8HQS-J7y.d.ts} +3 -1
- package/dist/{read-Bo1GBjSE.d.ts → read-A3CJkqVM.d.ts} +3 -1
- package/dist/{read-RFqq4ckj.d.cts → read-BgnLFuSi.d.cts} +1 -1
- package/dist/{read-3yy5EFV_.d.ts → read-CqaZ01kU.d.ts} +1 -1
- package/dist/{read-tpfiT_n4.d.cts → read-fphkvCpR.d.cts} +3 -1
- package/dist/{registry-Dajfxsbz.d.ts → registry-C_16-N6i.d.ts} +2 -0
- package/dist/{registry-B-ZkdQv1.d.cts → registry-PoQIRgbY.d.cts} +2 -0
- package/dist/scaffold-44EIMGll.d.cts +8 -0
- package/dist/scaffold-ehGsIpZ5.d.ts +8 -0
- package/dist/{shapes-QkOsc-WT.d.ts → shapes-B6Ms7ibr.d.ts} +1 -1
- package/dist/{shapes-BCjeY90t.d.cts → shapes-CbBc9t0y.d.cts} +1 -1
- package/dist/styles/properties.cjs +1 -1
- package/dist/styles/properties.js +2 -2
- package/dist/styles/registry.cjs +9 -6
- package/dist/styles/registry.d.cts +1 -1
- package/dist/styles/registry.d.ts +1 -1
- package/dist/styles/registry.js +9 -6
- package/dist/table-CECMtPxZ.d.cts +20 -0
- package/dist/table-XuniPr2I.d.ts +20 -0
- package/dist/text-B8350HA2.d.ts +17 -0
- package/dist/text-pmlTqzgi.d.cts +17 -0
- package/dist/{transform-SDysw9wx.d.cts → transform-DFCPKX7e.d.cts} +2 -1
- package/dist/{transform-DRr76JEu.d.ts → transform-DPliWQuD.d.ts} +2 -1
- package/dist/typed/draw/embedded.cjs +1 -1
- package/dist/typed/draw/embedded.js +1 -1
- package/dist/typed/draw/shapes.cjs +7 -2
- package/dist/typed/draw/shapes.d.cts +1 -1
- package/dist/typed/draw/shapes.d.ts +1 -1
- package/dist/typed/draw/shapes.js +7 -2
- package/dist/typed/draw/write-shapes.cjs +210 -0
- package/dist/typed/draw/write-shapes.d.cts +2 -0
- package/dist/typed/draw/write-shapes.d.ts +2 -0
- package/dist/typed/draw/write-shapes.js +203 -0
- package/dist/typed/draw/write-vectors.cjs +132 -0
- package/dist/typed/draw/write-vectors.d.cts +2 -0
- package/dist/typed/draw/write-vectors.d.ts +2 -0
- package/dist/typed/draw/write-vectors.js +129 -0
- package/dist/typed/odb/form.cjs +1 -1
- package/dist/typed/odb/form.js +1 -1
- package/dist/typed/odg/write.cjs +80 -0
- package/dist/typed/odg/write.d.cts +2 -0
- package/dist/typed/odg/write.d.ts +2 -0
- package/dist/typed/odg/write.js +77 -0
- package/dist/typed/odp/write.cjs +108 -0
- package/dist/typed/odp/write.d.cts +2 -0
- package/dist/typed/odp/write.d.ts +2 -0
- package/dist/typed/odp/write.js +105 -0
- package/dist/typed/ods/read.cjs +3 -1
- package/dist/typed/ods/read.d.cts +2 -2
- package/dist/typed/ods/read.d.ts +2 -2
- package/dist/typed/ods/read.js +2 -2
- package/dist/typed/ods/write.cjs +578 -0
- package/dist/typed/ods/write.d.cts +2 -0
- package/dist/typed/ods/write.d.ts +2 -0
- package/dist/typed/ods/write.js +575 -0
- package/dist/typed/odt/read.cjs +1 -1
- package/dist/typed/odt/read.js +1 -1
- package/dist/typed/odt/write.cjs +278 -0
- package/dist/typed/odt/write.d.cts +2 -0
- package/dist/typed/odt/write.d.ts +2 -0
- package/dist/typed/odt/write.js +275 -0
- package/dist/typed/shared/canonicalise.cjs +126 -0
- package/dist/typed/shared/canonicalise.d.cts +10 -0
- package/dist/typed/shared/canonicalise.d.ts +10 -0
- package/dist/typed/shared/canonicalise.js +120 -0
- package/dist/typed/shared/cascade.d.cts +1 -1
- package/dist/typed/shared/cascade.d.ts +1 -1
- package/dist/typed/shared/list.cjs +81 -0
- package/dist/typed/shared/list.d.cts +2 -2
- package/dist/typed/shared/list.d.ts +2 -2
- package/dist/typed/shared/list.js +75 -1
- package/dist/typed/shared/metadata.cjs +41 -0
- package/dist/typed/shared/metadata.d.cts +2 -2
- package/dist/typed/shared/metadata.d.ts +2 -2
- package/dist/typed/shared/metadata.js +41 -2
- package/dist/typed/shared/paragraph.cjs +118 -2
- package/dist/typed/shared/paragraph.d.cts +2 -2
- package/dist/typed/shared/paragraph.d.ts +2 -2
- package/dist/typed/shared/paragraph.js +117 -5
- package/dist/typed/shared/path.cjs +19 -0
- package/dist/typed/shared/path.d.cts +2 -2
- package/dist/typed/shared/path.d.ts +2 -2
- package/dist/typed/shared/path.js +18 -1
- package/dist/typed/shared/table.cjs +84 -0
- package/dist/typed/shared/table.d.cts +2 -2
- package/dist/typed/shared/table.d.ts +2 -2
- package/dist/typed/shared/table.js +84 -4
- package/dist/typed/shared/text.cjs +60 -0
- package/dist/typed/shared/text.d.cts +2 -2
- package/dist/typed/shared/text.d.ts +2 -2
- package/dist/typed/shared/text.js +60 -2
- package/dist/typed/shared/units.cjs +16 -1
- package/dist/typed/shared/units.d.cts +2 -2
- package/dist/typed/shared/units.d.ts +2 -2
- package/dist/typed/shared/units.js +16 -2
- package/dist/{units-BQSNkTQZ.d.cts → units-DdyaQolR.d.cts} +2 -1
- package/dist/{units-BQSNkTQZ.d.ts → units-DdyaQolR.d.ts} +2 -1
- package/dist/write-4kMi-asu.d.ts +14 -0
- package/dist/write-BZXpT8F6.d.cts +17 -0
- package/dist/write-BhG3t3fJ.d.cts +14 -0
- package/dist/write-BuLQmwLD.d.cts +14 -0
- package/dist/write-C2c4xBXs.d.cts +14 -0
- package/dist/write-CPATqG4v.d.cts +14 -0
- package/dist/write-CfTE2jY8.d.ts +14 -0
- package/dist/write-DttX1rTC.d.ts +14 -0
- package/dist/write-JERychL3.d.ts +14 -0
- package/dist/write-SKnCTU4C.d.ts +17 -0
- package/dist/write-shapes-Co7BrmMZ.d.ts +36 -0
- package/dist/write-shapes-D4piGKf4.d.cts +36 -0
- package/dist/write-vectors-B-RVHAKI.d.cts +11 -0
- package/dist/write-vectors-CYjZoVBW.d.ts +11 -0
- package/package.json +2 -2
- package/dist/list-DwTyo_B2.d.cts +0 -13
- package/dist/list-X_GJY7P3.d.ts +0 -13
- package/dist/metadata-01SgHkRR.d.cts +0 -7
- package/dist/metadata-D1SOBTh8.d.ts +0 -7
- package/dist/paragraph-ByTzS8Li.d.ts +0 -18
- package/dist/paragraph-CVULactG.d.cts +0 -18
- package/dist/table-CYvFMlP6.d.cts +0 -14
- package/dist/table-DclDgC5h.d.ts +0 -14
- package/dist/text-Bn3C7lnm.d.ts +0 -10
- package/dist/text-CDUCcwje.d.cts +0 -10
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/ExaDev/documents.js/tree/main/packages/odf.js) [](https://www.npmjs.com/package/odf.js) [](https://www.npmjs.com/package/odf.js) [](https://github.com/ExaDev/documents.js/actions)
|
|
4
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 — plus read support for the pre-OASIS OpenOffice.org 1.x / StarOffice 6-7 documents ODF was based on (`.sxw`/`.sxc`/`.sxi`/`.sxd`).
|
|
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 — plus full read **and write** support for the pre-OASIS OpenOffice.org 1.x / StarOffice 6-7 documents ODF was based on (`.sxw`/`.sxc`/`.sxi`/`.sxd`). `.odt`, `.ods`, `.odp`, and `.odg` all read and write.
|
|
6
6
|
|
|
7
7
|
`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.
|
|
8
8
|
|
|
@@ -64,9 +64,14 @@ Under active development. Built and shipped:
|
|
|
64
64
|
- **`readOdm`** — resolves a `.odm` master document into an ordered list of chapter references (`{ name, href, filterName? }`); chapters are genuinely external `.odt` files by ODF design, never cached.
|
|
65
65
|
- **`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.
|
|
66
66
|
- **`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.
|
|
67
|
-
- **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 an OpenOffice.org 1.x document](#reading-an-openofficeorg-1x-document).
|
|
67
|
+
- **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).
|
|
68
|
+
- **OpenOffice.org 1.x writing** (`writeSxw`/`writeSxwContent`, `writeSxc`/`writeSxcContent`, `writeSxi`/`writeSxiContent`, and `writeSxd`/`writeSxdContent`, plus `transformToOoo1Package`, the read-side transform's own inverse) — `.sxw`, built on `writeOdt`/`writeOdtContent`; `.sxc`, built on `writeOds`/`writeOdsContent`; `.sxi`, built on `writeOdp`/`writeOdpContent`; `.sxd`, built on `writeOdg`/`writeOdgContent`, through one wrapper pair each rather than a second writer. Every genuine document format this package reads on the OpenOffice.org 1.x side it now writes as well, including the `.stw`/`.stc`/`.sti`/`.std` template variants of all four (a `template: true` write option) — see [Reading and writing an OpenOffice.org 1.x document](#reading-and-writing-an-openofficeorg-1x-document).
|
|
69
|
+
- **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).
|
|
70
|
+
- **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).
|
|
71
|
+
- **The odp writer, at the same two levels** — `writeOdp`/`writeOdpContent`, the genuine inverse of `readOdp`/`readOdpContent`. A slide's shapes (positioned text boxes with formatted runs and lists, a rotated shape's `draw:transform`, a shape carrying a table or an image as its sole content, per-shape text insets), per-slide page geometry, and speaker notes all round-trip. Shape writing itself (`typed/draw/write-shapes.ts`) is factored out as the shared mirror of the read side's own `typed/draw/shapes.ts`, and the `.odg` writer below reuses it unchanged. The fidelity constructs a shape's own text cannot carry (a heading, a run-level construct extent, a page break, an embedded object, a table or image mixed with other shape content) are refused by name; a slide's own residue (transitions/animations/sound) is dropped, the same deliberate exception `writeOdt` makes. See [Writing a document](#writing-a-document) and this package's own [LibreOffice verification](#libreoffice-verification-writeodp) section for what was checked against a real, independent ODF implementation, including the two gaps that verification found and closed.
|
|
72
|
+
- **The odg writer, at the same two levels** — `writeOdg`/`writeOdgContent`, the genuine inverse of `readOdg`/`readOdgContent`, and the one writer here with content the others have no vocabulary for: a drawing page's **vector primitives**. Rectangles, ellipses, lines, and free-form paths write as real `draw:rect`/`draw:ellipse`/`draw:line`/`draw:path` elements, with fill and stroke interned as graphic-family automatic styles and a path's own subpaths serialised into genuine `svg:d` path data against an `svg:viewBox` sized to its own frame. A page's text-in-a-frame shapes go through the same `writeDrawShapes` the odp writer uses, and per-page geometry through the same `style:master-page`/`style:page-layout` pair, so a drawing costs no second copy of either. A dotted or double stroke, a non-positive stroke width, and a path with no subpaths or a zero-extent frame are refused by name — ODF has no spelling for the first two, and this package's own reader discards the last two outright. See [Writing a document](#writing-a-document) and [LibreOffice verification (`writeOdg`)](#libreoffice-verification-writeodg).
|
|
68
73
|
|
|
69
|
-
Not yet built: live-view editors and the `.odb` database-table-export subsystem. A general-purpose SQL query engine for rendering a Report against its data is **deliberately not attempted** — building even a bounded SQL engine means reimplementing HSQLDB's/Firebird's query semantics, a materially different undertaking from decoding their file formats, with unreviewed licensing questions. Gated on the requesting engineer's explicit sign-off.
|
|
74
|
+
Not yet built: a write path for the fidelity constructs, a `.ods` cell's own `number:*` data-style (`readOdsContent` does not read one back yet, so there is nothing to write against), live-view editors, and the `.odb` database-table-export subsystem. A general-purpose SQL query engine for rendering a Report against its data is **deliberately not attempted** — building even a bounded SQL engine means reimplementing HSQLDB's/Firebird's query semantics, a materially different undertaking from decoding their file formats, with unreviewed licensing questions. Gated on the requesting engineer's explicit sign-off.
|
|
70
75
|
|
|
71
76
|
## Getting started
|
|
72
77
|
|
|
@@ -126,6 +131,119 @@ One reader per format, each returning the `DocumentTree` arm its format produces
|
|
|
126
131
|
|
|
127
132
|
Each is assembled through `document-schema.js`'s own `assembleTree`, so odf.js's packages are built exactly the way every other package construction site in this family builds one. No `pages` array is populated and no node carries `frames`: a reader runs before any layout pass, and rendered page geometry is a layout engine's to report, never a reader's to invent.
|
|
128
133
|
|
|
134
|
+
### Writing a document
|
|
135
|
+
|
|
136
|
+
A typed writer takes a `document-schema.js` `DocumentTree` or flat `ContentDocument` and returns a real `.odt` `Package` — the exact inverse of the reader pair above, mirroring `readOdt`/`readOdtContent`'s own two-level shape:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { writeOdt, writeOdtContent, encodePackage } from "odf.js";
|
|
140
|
+
|
|
141
|
+
const pkg = writeOdt(document); // document-schema.js's DocumentTree -> a real .odt Package
|
|
142
|
+
const bytes = encodePackage(pkg); // Package -> bytes
|
|
143
|
+
|
|
144
|
+
const pkgFromContent = writeOdtContent(contentDocument); // the flat ContentDocument level, same shape readOdtContent returns
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Paragraphs, headings, runs (character formatting, hyperlinks), whitespace, lists, tables, images, explicit page breaks, per-section page geometry, and `meta.xml` all round-trip: `flattenTree(readOdt(writeOdt(document)))` reproduces `document` up to the same normalisation `normaliseOdtContent` states explicitly (its own doc comment — a section-less input, for instance, has no page geometry to invent, so it refuses rather than fabricating one).
|
|
148
|
+
|
|
149
|
+
The fidelity constructs `readOdt` reads (fields, bookmarks, notes, annotations, tracked changes, divisions, index wrappers, forms) and embedded objects are refused **by name** rather than silently dropped — a block or paragraph carrying one throws naming exactly what it carries, since writing a document that silently lost semantic content would be worse than not writing it at all. The one deliberate exception is the quarantined residue channel: residue is opaque by construction, so re-emitting it would be actively wrong rather than merely incomplete, and it is dropped instead, a known, tracked restorable-fidelity gap rather than a silent one.
|
|
150
|
+
|
|
151
|
+
`.odt`, `.ods`, `.odp`, and `.odg` all have a writer today (see [Status](#status)).
|
|
152
|
+
|
|
153
|
+
`writeOds`/`writeOdsContent` are the same shape, over `readOds`/`readOdsContent`:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { writeOds, writeOdsContent, encodePackage } from "odf.js";
|
|
157
|
+
|
|
158
|
+
const pkg = writeOds(document); // document-schema.js's DocumentTree -> a real .ods Package
|
|
159
|
+
const bytes = encodePackage(pkg); // Package -> bytes
|
|
160
|
+
|
|
161
|
+
const pkgFromContent = writeOdsContent(contentDocument); // the flat ContentDocument level, same shape readOdsContent returns
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Every `ContentCellValue` kind `readOdsContent` can actually produce (number, percentage, currency, boolean, date, time, string, a value-less cell) writes back with the correct `office:value-type`; `'dateTime'` and `'error'` are refused by name, since the reader's own `office:value-type` switch can never produce either kind for an `.ods` document, so there is no genuine inverse to verify a write against. A `'time'` cell's ISO wall-clock value is converted to a real ODF `xsd:duration` for `office:time-value` — the format's only valid spelling — even though `readOdsContent` does not yet convert it back on the way in, a narrow, pre-existing, unrelated reader gap this writer's own correctness does not depend on. Column widths, row heights, hidden rows/columns, and merged ranges all round-trip: `flattenTree(readOds(writeOds(document)))` reproduces `document` up to the normalisation `normaliseOdsContent` states explicitly — a sparse `columns`/`rows` array densifies to one entry per position across the sheet's own used range (ODF's `table:table-column`/`-row` model is purely positional), and a value-less, formula-less, text-less cell vanishes entirely (`readOdsContent`'s own trailing-empty-cell skip runs before any of its other attributes are considered). Cell-anchored images, print settings (page geometry, gridlines/headers, page order, scale/fit-to-page, print range, repeated header rows/columns, manual page breaks), and multiple sheets all round-trip too.
|
|
165
|
+
|
|
166
|
+
Embedded objects, data-validation rules, and conditional-formatting rules are refused **by name** for every sheet — `readOdsContent` has no write-side counterpart for any of the three yet (no embedded-sub-document package writer exists anywhere in this package's typed layer, and the reader itself never populates either rule array). A cell's own `numberFormatCode` is not written as a `number:*` data-style/`style:data-style-name` reference for the same reason: `readOdsContent` does not populate that field for any cell today, so there is no genuine inverse to write against. Sheet-level residue is dropped, the same deliberate exception `writeOdt` makes.
|
|
167
|
+
|
|
168
|
+
`writeOdp`/`writeOdpContent` are the same shape, over `readOdp`/`readOdpContent`:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { writeOdp, writeOdpContent, encodePackage } from "odf.js";
|
|
172
|
+
|
|
173
|
+
const pkg = writeOdp(document); // document-schema.js's DocumentTree -> a real .odp Package
|
|
174
|
+
const bytes = encodePackage(pkg); // Package -> bytes
|
|
175
|
+
|
|
176
|
+
const pkgFromContent = writeOdpContent(contentDocument); // the flat ContentDocument level, same shape readOdpContent returns
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A presentation is a sequence of slides, each a positioned bag of shapes rather than flowed blocks — `writeOdp` writes one `style:master-page`/`style:page-layout` pair per slide (a presentation genuinely allows different slides to reference different page geometry, unlike OOXML's single document-level `p:sldSz`) and one `draw:page` per slide, its shapes written by `typed/draw/write-shapes.ts`'s `writeDrawShapes` — the shape writer this package factored out as the shared mirror of the read side's own `typed/draw/shapes.ts`, so the `.odg` writer reuses it rather than reimplementing shape geometry, insets, and text/table/image content from scratch. A shape's own `frame`/`rotationDeg` write as plain `svg:x`/`svg:y`/`svg:width`/`svg:height` when unrotated, or `svg:width`/`svg:height` plus a `draw:transform="rotate(...) translate(...)"` when rotated — the exact algebraic inverse of the reader's own `resolveOdfShapeGeometry`, exact up to ordinary floating-point rounding on a real round trip. A shape's own text (formatted runs, alignment, spacing, indentation, bullet/ordered lists nested per level) writes as a `draw:text-box`; a shape whose sole block is a table or an image writes that content directly as the frame's own `table:table`/`draw:image`, since a real `draw:frame` can hold exactly one of the three, never a mix — a combination ODF has no spelling for is refused **by name**, the same fidelity-construct stance `writeOdt` takes, and so is a heading or a page break inside a shape's own text (a `draw:text-box` has no `text:h` reading path and no page concept at all). A shape's own `paintOrder` writes as `draw:z-index`, the one spelling ODF has for a stacking order independent of document position, and the one the reader already resolves; a `paintOrder` ODF's own `xsd:nonNegativeInteger` cannot spell (a negative or fractional one) writes no attribute rather than a rounded approximation that would reorder it past a sibling. Speaker notes write as `presentation:notes`, one `text:p` per line. `flattenTree(readOdp(writeOdp(document)))` reproduces `document` up to the normalisation `normaliseOdpContent` states explicitly — including the one fact ODF forces rather than this writer choosing it: an image's own `widthPt`/`heightPt` become its enclosing shape's own frame size, since a `draw:image` has no size of its own at all inside a `draw:frame`. A shape's `fontScale`/`lineSpacingReduction` are dropped and say so: they are DrawingML's own `a:normAutofit` percentages — the shrink factor PowerPoint _computed_ and stored — and ODF's own autofit vocabulary is a mode flag with no computed factor anywhere, so a pptx → odp conversion loses autofit shrink state rather than having it approximated into something the format never said. A slide's own residue (transition/animation/sound facts) is dropped, the same deliberate exception `writeOdt` makes. `.sxi` is covered too: `writeSxi`/`writeSxiContent` wrap this writer exactly the way `writeSxw`/`writeSxc` wrap `writeOdt`/`writeOds` — see [Reading and writing an OpenOffice.org 1.x document](#reading-and-writing-an-openofficeorg-1x-document).
|
|
180
|
+
|
|
181
|
+
#### LibreOffice verification (`writeOdp`)
|
|
182
|
+
|
|
183
|
+
Round-tripping through this package's own reader proves internal consistency, not that a real, independent ODF implementation accepts the result — so a sample `.odp` covering multiple slides (one widescreen, one A4-portrait, exercising per-slide page geometry), a shape with mixed bold/italic/plain runs and centred alignment, rotated shapes (`draw:transform`) both away from and at the page origin, shapes whose `paintOrder` disagrees with their document order, a shape whose `draw:name` carries XML special characters, a nested bullet list, a shape carrying a table (including a merged cell) as its sole content, a shape carrying an image as its sole content, and multi-line speaker notes was built with `writeOdp` and checked against LibreOffice 26.2.5.2 directly (`soffice --headless`), matching this package family's own established verification bar (see `doc-codec`'s README and this package's own `.sxw`/`.ods`/`.sxc` writer PRs):
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
soffice --headless --convert-to fodp sample.odp # flat XML, for text-content inspection
|
|
187
|
+
soffice --headless --convert-to pdf sample.odp # rendered pages, for visual inspection
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Both commands exit `0` with no error. What the flat-XML conversion establishes is that every authored string appears **verbatim** in LibreOffice's re-serialised output — all three `draw:page`s, the `table:table`, the `draw:transform`, both `text:list`s, and every string of authored text: titles, bullets, table cells, the rotated shape's own text. It is not a byte-identity check, and could not be: `--convert-to fodp` re-serialises the whole document through LibreOffice's own writer, which renames styles, reorders and reformats attributes, and adds defaults of its own, so its bytes differ from `writeOdp`'s by construction. The rendered PDF (3 pages, matching the 3 slides) visually confirms the bold/italic mixed formatting, the centred title, the nested bullet list, the shape rotated clockwise by the requested angle, the table with its merged cell, and the A4-portrait slide's own different page geometry, all laid out correctly with no visible loss.
|
|
191
|
+
|
|
192
|
+
**One gap found and fixed during this verification**: an earlier version of this writer's `presentation:notes` carried no `style:page-layout-name` attribute at all. Real LibreOffice output always states one (a notes page is sized for printing, independent of whatever on-screen size its slide's own page-layout states), and every real producer's own notes page references it directly — `writeOdp` now mints one page-layout for the whole presentation's own notes pages, lazily, the first time any slide actually has notes to write.
|
|
193
|
+
|
|
194
|
+
**A second gap found and fixed — an undeclared namespace prefix**: speaker notes initially arrived on the slide itself rather than its notes page, which read as LibreOffice's own AutoLayout placeholder-matching declining to bind a minimal `presentation:notes`/`draw:frame` to its internal Notes view. It was not: `presentation:notes` and its frame's `presentation:class` are the only `presentation:`-prefixed names any writer here emits, and `package-io/scaffold.ts`'s shared prefix list never declared that prefix, so the part was not namespace-well-formed XML at all — `xmllint --noout content.xml` reported `Namespace prefix presentation on notes is not defined`. LibreOffice imported the file anyway, treated the unrecognised element as ordinary slide content, and re-homed its text onto the visible shape list. With the prefix declared, `--convert-to fodp` round-trips the notes inside `presentation:notes` where they were written, the slide carries only its own shapes, and `--convert-to pdf` renders no notes text on the slide page.
|
|
195
|
+
|
|
196
|
+
Nothing between a writer and the emitted bytes checks that a qualified name's prefix is actually bound — `src/xml/build.ts` writes whatever name an element carries — so this failure mode is silent by construction, and round-trips perfectly through this package's own (prefix-string-matching, namespace-unaware) reader. `src/package-io/namespace-declarations.test.ts` now audits every prefix each writer emits, across element and attribute names at any depth, against what that part's own root declares, so the next writer to reach for an undeclared prefix fails a test instead of shipping a document no XML parser will accept.
|
|
197
|
+
|
|
198
|
+
**`draw:z-index` is honoured by a real consumer**, confirmed on the same sample: its slide-1 shapes were written in an array order deliberately unlike their own `paintOrder` (`3, 1, 0, 2`), and LibreOffice re-emitted them in `paintOrder` order — dropping the attribute and physically reordering the elements instead, the mirror image of what `typed/draw/shapes.ts`'s own reading of `draw:z-index` already documents finding in LibreOffice-authored files.
|
|
199
|
+
|
|
200
|
+
**One class of defect this verification cannot catch, worth stating so nobody reads more into a green `soffice` run than it proves**: LibreOffice's own length parser accepts values outside the OASIS `length` datatype. A `translate()` component written in JavaScript's exponent notation (`7.105427357601002e-15pt` — the ordinary result of the rotation inverse's terms cancelling for a frame at the page origin) converts through `--convert-to fodp` with no error and lands at the right place, so the file looked correct by every check above while being invalid ODF that this package's own (spec-conforming) reader silently discarded — dropping the whole `translate()` for a rotated frame, and the whole shape for an unrotated one whose `svg:x`/`svg:y` were the unparseable values. `formatOdfLength` now emits fixed-point decimal only, and the regression is pinned by unit tests rather than by a `soffice` run, since `soffice` would have passed either way.
|
|
201
|
+
|
|
202
|
+
`writeOdg`/`writeOdgContent` are the same shape again, over `readOdg`/`readOdgContent`:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { writeOdg, writeOdgContent, encodePackage } from "odf.js";
|
|
206
|
+
|
|
207
|
+
const pkg = writeOdg(document); // document-schema.js's DocumentTree -> a real .odg Package
|
|
208
|
+
const bytes = encodePackage(pkg); // Package -> bytes
|
|
209
|
+
|
|
210
|
+
const pkgFromContent = writeOdgContent(contentDocument); // the flat ContentDocument level, same shape readOdgContent returns
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A drawing page is a presentation slide's structural twin — `draw:page`'s content model is one format-agnostic schema fragment shared by `office:drawing` and `office:presentation` alike — so `writeOdg` writes the same one `style:master-page`/`style:page-layout` pair per page and delegates a page's text-in-a-frame shapes to the same `typed/draw/write-shapes.ts`'s `writeDrawShapes` `writeOdp` uses, refusing exactly what that shared writer refuses. What a drawing adds, and the whole reason it has a writer of its own, is `ContentDrawPage`'s second array: **vector primitives**, which a `ContentShape` has no vocabulary for at all.
|
|
214
|
+
|
|
215
|
+
A `'rect'` writes as `draw:rect`, an `'ellipse'` as `draw:ellipse` (including a circular one — real LibreOffice writes `draw:circle` when width and height happen to be equal, and the reader maps both spellings onto the one variant, so the general spelling loses nothing), a `'line'` as `draw:line` carrying its two endpoints in `svg:x1`/`svg:y1`/`svg:x2`/`svg:y2` rather than a box, and a `'path'` as `draw:path`. Geometry and rotation for the three boxed kinds go through the identical `frameGeometryAttrs` inverse a `draw:frame` uses, because the reader resolves all four element kinds through the same `resolveOdfShapeGeometry`. A path's own subpaths serialise into real `svg:d` path data — one absolute `M` per subpath, an explicit absolute `L`/`C` per segment, a trailing `Z` only when the subpath is closed — against an `svg:viewBox` stated as `0 0 <frame width> <frame height>`, which makes the reader's own viewBox-to-frame scale factor exactly 1 and the coordinates therefore an exact round trip. Every number this writer emits — path coordinates and `svg:viewBox` extents, and `draw:transform`'s own `rotate()` angle alongside its `translate()` lengths — goes through the same fixed-point spelling `formatOdfLength`/`formatOdfNumber` use, since neither `svg:viewBox` nor a bare rotation angle has an exponent form either.
|
|
216
|
+
|
|
217
|
+
Fill and stroke are graphic-family automatic styles, interned like every other formatting difference (ODF has no direct formatting): `draw:fill="solid"` plus `draw:fill-color`, `draw:stroke="solid"`/`"dash"` plus `svg:stroke-color` and `svg:stroke-width`, and `svg:fill-rule` for a path that states one — the exact six attributes `typed/draw/shapes.ts`'s own `readOdfFillAndStroke` reads. A vector with neither fill nor stroke still carries a style, stating `draw:fill="none"` and `draw:stroke="none"` explicitly: an absent declaration means _inherit_ in ODF, and a consumer's own default graphic style supplies a fill, so an unfilled rectangle that says nothing renders filled. A text-in-a-frame shape carries the identical explicit `draw:fill="none"`/`draw:stroke="none"` for the same reason — `ContentShape` has no fill/stroke vocabulary of its own to lose by stating it, and a real consumer's own default graphic style otherwise renders a plain text shape as a filled, bordered box. A `'dotted'` or `'double'` stroke style is refused **by name** — ODF's `draw:stroke` is enumerated to exactly none/solid/dash and its vector-stroke model has no double-line concept — and so are a non-positive stroke width, a path with no subpaths, and a path whose frame has a zero or negative extent, each of which this package's own reader discards outright rather than reading back smaller.
|
|
218
|
+
|
|
219
|
+
`flattenTree(readOdg(writeOdg(document)))` reproduces `document` up to the normalisation `normaliseOdgContent` states explicitly, which adds two page-level facts to the ones the shared shape canonical form already states. First, **a page's shapes and vectors share one document-encounter counter**: the reader walks a `draw:page`'s children once, stamping every shape and vector it meets, so an element carrying no `draw:z-index` takes its position in that single walk. This writer emits a page's shapes first and its vectors after, which is a stated choice rather than an implied one — the two arrays carry no interleaving information beyond `paintOrder` itself, so a page whose items state no paint order has no cross-array order to preserve and one has to be picked; anything that _does_ state an ODF-spellable `paintOrder` carries a real `draw:z-index` and is ordered by that instead. Every item's `draw:z-index` is written **unconditionally**, resolved shape (its own stated `paintOrder` when ODF can spell it, its document-encounter index otherwise) — never omitted for the items that state no spellable `paintOrder`, because an item with no attribute at all reads back on a real consumer as appended after every sibling that does carry one, regardless of its own resolved order relative to them (see [LibreOffice verification](#libreoffice-verification-writeodg) below for the mixed case this closes). Second, **both arrays come back sorted by paint order**, because `readDrawPageContent` sorts them — the one structural difference from `writeOdp`'s own canonical form, where a slide has no sibling vectors array for the value to be comparable across. A page's own residue (the unmapped shape kinds and vendor-extension elements the reader quarantines) is dropped, the same deliberate exception every other writer here makes. `.sxd` is covered too: `writeSxd`/`writeSxdContent` wrap this writer exactly the way `writeSxi`/`writeSxiContent` wrap `writeOdp` — see [Reading and writing an OpenOffice.org 1.x document](#reading-and-writing-an-openofficeorg-1x-document).
|
|
220
|
+
|
|
221
|
+
#### LibreOffice verification (`writeOdg`)
|
|
222
|
+
|
|
223
|
+
Round-tripping through this package's own reader proves internal consistency, not that a real, independent ODF implementation accepts the result — so a sample `.odg` was built with `writeOdgContent` and checked against LibreOffice 26.2.5.2 directly (`soffice --headless`), matching the bar the `writeOdp` section above sets. The sample covers two pages with genuinely different geometry (720×540pt landscape and A4 portrait), a filled and stroked rectangle, a filled ellipse with a dashed stroke, a plain line, a path whose two subpaths are one closed line-plus-cubic and one open cubic (with a fill, an `evenodd` fill rule, and a stroke), a rectangle rotated 30° clockwise, text-in-a-frame shapes alongside the vectors on the same page, and paint orders across **both** arrays deliberately unlike document order (shapes `6, 1`; vectors `5, 0, 3, 2, 4`):
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
xmllint --noout content.xml styles.xml meta.xml META-INF/manifest.xml # the written package's own parts
|
|
227
|
+
soffice --headless --convert-to fodg sample.odg # flat XML, for element and value inspection
|
|
228
|
+
soffice --headless --convert-to pdf sample.odg # rendered pages, for visual inspection
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`xmllint` accepts every part of the written package and the resulting flat XML; both `soffice` commands exit `0` with no error, identifying the input as `a Draw document`. What the flat-XML conversion establishes is that **every authored value appears in LibreOffice's re-serialised output**, either verbatim or unit-converted: all seven authored colours survive verbatim (`#0033ff`, `#cc0000`, `#00aa44`, `#000000`, `#ffcc33`, `#9933cc`, `#e6e6f2`), as does every authored string; each element comes back as a real element of the matching kind (`draw:rect`, `draw:ellipse` — and `draw:circle` for the equal-width-and-height case, LibreOffice's own spelling, which the reader already maps back onto `'ellipse'` — plus `draw:line`, `draw:path`, `draw:frame`), with nothing dropped in either direction; lengths convert exactly (a 200pt width to `7.056cm`, a 280pt `svg:x` to `9.878cm`, stroke widths of 3/2/4/1pt to `0.106cm`/`0.071cm`/`0.141cm`/`0.035cm`); `draw:stroke="dash"`, `draw:stroke="none"` and `draw:fill="none"` all survive as written; and the rotated rectangle's whole `draw:transform` survives, `rotate(-0.5235987755982988) translate(441.07695154586736pt 286.69872981077805pt)` re-emitted as `rotate (-0.523598775598299) translate (15.56cm 10.114cm)`. The path's `svg:d` is re-expressed in LibreOffice's own 1/100mm `svg:viewBox` using relative and shorthand commands, and every coordinate maps back exactly — the authored `C 180,0 180,100 120,100` becomes `c2117 0 2117 3529 0 3529`, the same +60pt/+100pt control offsets and +120pt/+100pt endpoint. It is **not** a byte-identity check and could not be: `--convert-to fodg` re-serialises the whole document through LibreOffice's own writer, which renames styles, reorders and reformats attributes, converts units, and adds defaults of its own, so its bytes differ from `writeOdg`'s by construction. The rendered PDF (2 pages) visually confirms each shape's geometry, its fill and stroke colours, the dashed ellipse border, the 30° clockwise rotation, and the path's curve, and carries the sample's `meta.xml` title, author, subject, and keywords into its own metadata.
|
|
232
|
+
|
|
233
|
+
**`draw:z-index` is honoured across both arrays**, which is the finding that matters most here: LibreOffice re-emitted page 1's seven elements in exactly the authored paint order `0…6` — an ellipse written second came out first, a text frame written first came out last — dropping the attribute and physically reordering the elements instead. That is the same behaviour the `writeOdp` verification found, now confirmed for the case only a drawing has, where the ordering has to hold _between_ the shapes array and the vectors array rather than within one of them.
|
|
234
|
+
|
|
235
|
+
**One gap found and fixed during this verification**: the sample above states an ODF-spellable `paintOrder` on every one of its seven items, which never exercises the case where a page mixes an item that states one with an item that does not. An earlier version of this writer omitted `draw:z-index` entirely for the unspellable case (absent, negative, or fractional `paintOrder`), matching `writeOdp`'s own established convention at the time — but LibreOffice does not treat an omitted `draw:z-index` as "insert at this item's own resolved position": it appends every item with no attribute at all **after** every item that does carry one, regardless of where the omitted item's own resolved order would otherwise place it. A follow-up sample mixing an explicit-`paintOrder` shape with an unspelled-`paintOrder` vector on the same page confirmed it directly — `soffice --headless --convert-to fodg` re-emitted the vector last even though its own resolved document-encounter order placed it between two shapes — so `writeDrawFrame`/`writeDrawVector` now write the fully resolved paint order (`paintOrder` when ODF can spell it, the item's own document-encounter index otherwise) as `draw:z-index` **unconditionally**, never omitting the attribute. `typed/odg/write.test.ts` pins the same mixed case as a unit test, and `writeOdp`'s writer inherited the identical fix since both writers share `writeDrawFrame`.
|
|
236
|
+
|
|
237
|
+
**A second gap found and fixed — the same exponent-notation hazard `writeOdp`'s own verification closed for `translate()`, reached this time through `rotate()`'s own angle.** `draw:transform`'s two components are written by one template string, and an earlier version of `frameGeometryAttrs` ran the `translate()` lengths through `formatOdfLength` but interpolated the `rotate()` angle directly — a bare radians value with no unit suffix, so it never went through any fixed-point formatter at all. A very small non-zero `rotationDeg` (never exactly zero, which collapses to no transform) drives the angle itself into JavaScript's own exponent spelling (`rotate(-1.7453292519943295e-11)`), exactly as invalid to the ODF `length`/number grammar as the `translate()` case, and by the identical mechanism: LibreOffice's own parser accepts it regardless (confirmed directly — a real `.odg` carrying this exact spelling round-trips through `soffice --headless --convert-to fodg` with no error), so a `soffice` check alone cannot catch it. `frameGeometryAttrs` now runs the angle through the bare-number sibling `formatOdfLength` already uses internally (`formatOdfNumber`, `typed/shared/units.ts`), and `typed/odg/write.test.ts`/`typed/odp/write.test.ts` each pin a very-small-`rotationDeg` regression alongside the existing near-origin `translate()` one.
|
|
238
|
+
|
|
239
|
+
**A third gap found and fixed — a text-in-a-frame shape with no insets rendered as a filled, bordered box in LibreOffice**, even though `ContentShape` carries no fill/stroke vocabulary at all for a document to have stated one. `shapeGraphicStyleName` minted a style only when at least one inset was non-zero, so a shape with every inset at zero wrote no `draw:style-name` and inherited LibreOffice's own built-in `standard` graphic style — solid-filled, coloured border — the identical silent-inherit hazard `typed/draw/write-vectors.ts`'s own top-of-file note already documents closing for a vector. It now mints a style unconditionally, stating explicit `draw:fill="none"`/`draw:stroke="none"` (insets are still written only when at least one is non-zero, since `readFrameInsets` already defaults an absent style to zero). Confirmed directly: a zero-inset text shape written with the fix carries a `draw:style-name` whose `style:graphic-properties` states `draw:fill="none" draw:stroke="none"`, and that survives `soffice --headless --convert-to fodg` verbatim.
|
|
240
|
+
|
|
241
|
+
Three things LibreOffice does with a valid file that are worth stating precisely, none of them a defect in what this writer emits:
|
|
242
|
+
|
|
243
|
+
- **`svg:fill-rule` does not survive a LibreOffice round trip.** The attribute is real, spec-defined ODF vocabulary on `style:graphic-properties` (which is why `typed/draw/shapes.ts` reads it and this writer emits it), but LibreOffice's own Draw import/export drops it entirely — it appears zero times in the re-serialised output. It round-trips through this package's own reader; a real consumer simply does not keep it.
|
|
244
|
+
- **A `draw:path` mixing closed and open subpaths renders unfilled in LibreOffice.** A controlled test holding every other property identical — three paths with the same colours, stroke, and frame size, differing only in their subpaths — renders the closed-only path filled, the open-only path unfilled (correct: an open path has no area), and the mixed path unfilled even for its closed subpath. LibreOffice classifies a whole `draw:path` as open or closed for its own shape model, and an open shape is never filled. The fill is preserved in the file: the re-serialised style still reads `draw:fill="solid" draw:fill-color="#ffcc33"`, and this package's own reader reads it back. Only the render omits it.
|
|
245
|
+
- **Per-page geometry is preserved in ODF but normalised on PDF export.** The written package states two `style:page-layout`s and two `style:master-page`s with each `draw:page` referencing its own, LibreOffice's flat XML preserves both (`25.4cm × 19.05cm` landscape and `21cm × 29.7cm` portrait), and `readOdgContent` reads both back — but `--convert-to pdf` renders every page at the first page's size. Separately, LibreOffice tightens a `draw:path`'s own frame to the path geometry's bounding box on re-serialisation: the sample's 240×140pt path frame, whose curve only spans 240×120pt, came back as 240×120pt with its `svg:viewBox` rescaled to match and every coordinate intact.
|
|
246
|
+
|
|
129
247
|
### The flat `ContentDocument` level
|
|
130
248
|
|
|
131
249
|
Beneath each package-native reader sits the flat reader it is built on, unchanged in behaviour and exported under a `*Content` name. Reach for these when you work in `document-schema.js`'s flat codec-exchange form — as `documents.js`'s own conversion pipeline does — rather than in the tree:
|
|
@@ -190,7 +308,7 @@ syncManifest(pkg); // rebuilds manifest.xml to exactly match pkg's current parts
|
|
|
190
308
|
readMimetype(pkg); // 'application/vnd.oasis.opendocument.text'
|
|
191
309
|
```
|
|
192
310
|
|
|
193
|
-
### Reading an OpenOffice.org 1.x document
|
|
311
|
+
### Reading and writing an OpenOffice.org 1.x document
|
|
194
312
|
|
|
195
313
|
`.sxw`/`.sxc`/`.sxi`/`.sxd` (and their `.stw`/`.stc`/`.sti`/`.std` template counterparts) are OpenOffice.org 1.x / StarOffice 6-7 documents — the format OASIS based ODF 1.0 directly on. They read through the same functions as their ODF successors, one reader per format, at the same two levels:
|
|
196
314
|
|
|
@@ -201,18 +319,74 @@ const document = readSxw(decodePackage(sxwBytes)); // a wordprocessing DocumentT
|
|
|
201
319
|
const { sheets } = readSxcContent(decodePackage(sxcBytes)); // the flat ContentSheet[] shape
|
|
202
320
|
```
|
|
203
321
|
|
|
204
|
-
| Format
|
|
205
|
-
| --------------- | --------- | ---------------- | ---------------- |
|
|
206
|
-
| `.sxw` / `.stw` | `readSxw` | `readSxwContent` | `wordprocessing` |
|
|
207
|
-
| `.sxc` / `.stc` | `readSxc` | `readSxcContent` | `spreadsheet` |
|
|
208
|
-
| `.sxi` / `.sti` | `readSxi` | `readSxiContent` | `presentation` |
|
|
209
|
-
| `.sxd` / `.std` | `readSxd` | `readSxdContent` | `drawing` |
|
|
322
|
+
| Format read | Reader | Flat sibling | Format written | Writer | Flat sibling | Package kind |
|
|
323
|
+
| --------------- | --------- | ---------------- | --------------- | ---------- | ----------------- | ---------------- |
|
|
324
|
+
| `.sxw` / `.stw` | `readSxw` | `readSxwContent` | `.sxw` / `.stw` | `writeSxw` | `writeSxwContent` | `wordprocessing` |
|
|
325
|
+
| `.sxc` / `.stc` | `readSxc` | `readSxcContent` | `.sxc` / `.stc` | `writeSxc` | `writeSxcContent` | `spreadsheet` |
|
|
326
|
+
| `.sxi` / `.sti` | `readSxi` | `readSxiContent` | `.sxi` / `.sti` | `writeSxi` | `writeSxiContent` | `presentation` |
|
|
327
|
+
| `.sxd` / `.std` | `readSxd` | `readSxdContent` | `.sxd` / `.std` | `writeSxd` | `writeSxdContent` | `drawing` |
|
|
328
|
+
|
|
329
|
+
The `.st*` template variant of each format both reads and writes: every `Od*WriteOptions` (`OdtWriteOptions`/`OdsWriteOptions`/`OdpWriteOptions`/`OdgWriteOptions`) carries a `template?: boolean` option alongside `version?: string`, which stamps `ODF_MEDIA_TYPES.ott`/`.ots`/`.otp`/`.otg` in place of the base `.odt`/`.ods`/`.odp`/`.odg` media type — both the package's own "mimetype" part and the manifest root entry `syncManifest` derives from it, since both already key off whatever media type `createOdfPackage` was given rather than assuming the base one. `writeSxw`/`writeSxc`/`writeSxi`/`writeSxd` need no template-specific code of their own to inherit this: each already passes its own `options` straight through to the ODF writer it wraps, and `transformToOoo1Package` already derives the package's OpenOffice.org 1.x media type (`.stw`/`.stc`/`.sti`/`.std`) from whatever ODF media type it finds stamped on the package, through the same `ooo1MediaTypeForOdfMediaType` table the read side's `odfMediaTypeForOoo1MediaType` already inverts. Verified against real LibreOffice 26.2.5.2: an `.ott`/`.ots` and a `.stw`/`.stc` built with `template: true` all convert cleanly (`soffice --headless --convert-to fodt`/`--convert-to fods`, exit `0`), each correctly identified by LibreOffice as a genuine Writer/Calc document from its media type alone, with no dependence on the file extension.
|
|
210
330
|
|
|
211
331
|
None of these is a second reader. Each is `readOdt`/`readOds`/`readOdp`/`readOdg` run over a package `transformOoo1Package` has rewritten into the ODF shape — the same approach LibreOffice itself takes, where a `.sxw` goes through a transformer (`xmloff/source/transform/`) into the ordinary ODF importer rather than through an importer of its own. Every construct the ODF readers understand therefore works on an OpenOffice.org 1.x document too, and a fix to any of them fixes both formats at once.
|
|
212
332
|
|
|
213
333
|
`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.
|
|
214
334
|
|
|
215
|
-
|
|
335
|
+
All four formats 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:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
import {
|
|
339
|
+
writeSxw,
|
|
340
|
+
writeSxwContent,
|
|
341
|
+
writeSxc,
|
|
342
|
+
writeSxcContent,
|
|
343
|
+
writeSxi,
|
|
344
|
+
writeSxiContent,
|
|
345
|
+
writeSxd,
|
|
346
|
+
writeSxdContent,
|
|
347
|
+
encodePackage,
|
|
348
|
+
} from "odf.js";
|
|
349
|
+
|
|
350
|
+
const pkg = writeSxw(document); // a wordprocessing DocumentTree -> a real .sxw Package
|
|
351
|
+
const bytes = encodePackage(pkg); // Package -> bytes
|
|
352
|
+
|
|
353
|
+
const pkgFromContent = writeSxwContent(contentDocument); // the flat ContentDocument level, same shape writeOdtContent returns
|
|
354
|
+
|
|
355
|
+
const sxcPkg = writeSxc(spreadsheetTree); // a spreadsheet DocumentTree -> a real .sxc Package
|
|
356
|
+
const sxcPkgFromContent = writeSxcContent(spreadsheetContentDocument); // the flat ContentDocument level, same shape writeOdsContent returns
|
|
357
|
+
|
|
358
|
+
const sxiPkg = writeSxi(presentationTree); // a presentation DocumentTree -> a real .sxi Package
|
|
359
|
+
const sxiPkgFromContent = writeSxiContent(presentationContentDocument); // the flat ContentDocument level, same shape writeOdpContent returns
|
|
360
|
+
|
|
361
|
+
const sxdPkg = writeSxd(drawingTree); // a drawing DocumentTree -> a real .sxd Package
|
|
362
|
+
const sxdPkgFromContent = writeSxdContent(drawingContentDocument); // the flat ContentDocument level, same shape writeOdgContent returns
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`writeSxw`/`writeSxwContent` call `writeOdt`/`writeOdtContent` to build a real ODF `.odt` `Package`; `writeSxc`/`writeSxcContent` call `writeOds`/`writeOdsContent` to build a real ODF `.ods` `Package`; `writeSxi`/`writeSxiContent` call `writeOdp`/`writeOdpContent` to build a real ODF `.odp` `Package`; `writeSxd`/`writeSxdContent` call `writeOdg`/`writeOdgContent` to build a real ODF `.odg` `Package` — all four the identical way. Each then runs its 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, the drawing style family's `"graphic"`/`"graphics"` 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`, `.sxi` and `.sxd` up to it needed one more pair of writer entry points each time and nothing else — wrapping `writeOds`/`writeOdsContent`, `writeOdp`/`writeOdpContent` and `writeOdg`/`writeOdgContent` the way `writeSxw`/`writeSxwContent` already wrap `writeOdt`/`writeOdtContent` — with the single exception of the drawing style family, a value rename `.sxd` is the first format here to actually exercise (see [What `.sxd` needed of the transform](#what-sxd-needed-of-the-transform)). 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`, `readSxc(writeSxc(document))` recovers `document` up to the canonical form `normaliseOdsContent` already states for `writeOds`, `readSxi(writeSxi(document))` recovers `document` up to the canonical form `normaliseOdpContent` already states for `writeOdp`, and `readSxd(writeSxd(document))` recovers `document` up to the canonical form `normaliseOdgContent` already states for `writeOdg`, since each `*Content` writer here is its ODF counterpart's own output run one transform further. What `writeOdt`/`writeOds`/`writeOdp`/`writeOdg` 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; the odp/odg fidelity constructs a shape's own text cannot carry; the odg vector refusals — a dotted or double stroke, a non-positive stroke width, a path with no subpaths, and a path whose frame has a zero or negative extent), `writeSxw`/`writeSxc`/`writeSxi`/`writeSxd` 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.
|
|
366
|
+
|
|
367
|
+
Verified against real LibreOffice 26.2.5.2 the same way, not just the round-trip law: a `.sxi` built with `writeSxiContent` (a title, a rotated shape, a table, an image, and multi-line speaker notes) converts cleanly with `soffice --headless --convert-to fodp`/`--convert-to pdf` (both exit `0`, LibreOffice identifies the input as a genuine Impress document), every authored string survives verbatim in the flat XML, and the rendered PDF confirms the content lays out correctly with the speaker notes absent from the slide itself. `.sxd` is verified to the same bar in its own section below.
|
|
368
|
+
|
|
369
|
+
#### LibreOffice verification (`writeSxd`)
|
|
370
|
+
|
|
371
|
+
The `.sxd` writer was checked against LibreOffice 26.2.5.2 directly, matching the bar the [`writeOdg` section](#libreoffice-verification-writeodg) sets for the ODF writer underneath it. The sample was built with `writeSxdContent` and covers two pages of genuinely different geometry (720×540pt landscape and A4 portrait), a blue rectangle with a red stroke, a green ellipse with a dashed black stroke, an unstroked purple circle, a plain line, a path whose closed subpath mixes line and cubic segments (amber fill, `evenodd` fill rule, red stroke), a rectangle rotated 30° clockwise on the second page, text-in-a-frame shapes with centred bold and mixed bold/italic/plain runs alongside the vectors on the same page, and a page-1 paint order deliberately unlike document order across **both** arrays (shapes `6, 1`; vectors `5, 0, 3, 2, 4`):
|
|
372
|
+
|
|
373
|
+
```sh
|
|
374
|
+
xmllint --noout content.xml styles.xml meta.xml META-INF/manifest.xml # the written package's own parts
|
|
375
|
+
soffice --headless --convert-to fodg sample.sxd # flat XML, for element and value inspection
|
|
376
|
+
soffice --headless --convert-to pdf sample.sxd # rendered pages, for visual inspection
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`xmllint` accepts every part of the written package; both `soffice` commands exit `0` with no error, identifying the input as `a Draw document` — LibreOffice's own OpenOffice.org 1.x import filter accepting it as a genuine StarOffice XML Draw file, not an ODF one. The package itself is authentically pre-OASIS: OpenOffice.org namespace URIs on every part, `office:class="drawing"` with the content directly inside `office:body` (no `office:drawing` genre element), one bare `style:properties` per style rather than ODF's typed `style:graphic-properties`, bare `draw:text-box` shapes rather than `draw:frame`-wrapped ones, `style:page-master`/`style:page-master-name` rather than `style:page-layout`, a `manifest:media-type` of `application/vnd.sun.xml.draw`, and no `mimetype` part at all.
|
|
380
|
+
|
|
381
|
+
Every authored value survives LibreOffice's re-serialisation, either verbatim or unit-converted: all seven colours come back exactly (`#0033ff`, `#cc0000`, `#00aa44`, `#9933cc`, `#ffcc33`, `#000000`), each element comes back as a real element of the matching kind (two `draw:rect`, a `draw:ellipse`, a `draw:circle` — LibreOffice's own spelling for the equal-width-and-height case, which the reader maps back onto `'ellipse'` — a `draw:line`, a `draw:path`, and three `draw:frame`s), lengths convert exactly (a 200pt width to `7.055cm`, a 280pt `svg:x` to `9.878cm`, stroke widths of 3/2/4/1pt to `0.106cm`/`0.071cm`/`0.141cm`/`0.035cm`), `draw:stroke="dash"` and the explicit `draw:fill="none"`/`draw:stroke="none"` on the text frames survive as written, and the rotated rectangle's `rotate(-0.5235987755982988)` comes back as `rotate (-0.523598775598299)`. The path's `svg:d` is re-expressed in LibreOffice's own 1/100mm `svg:viewBox` with relative and shorthand commands, mapping back exactly — the authored `C 180,0 180,100 120,100` becomes `c2116 0 2116 3528 0 3528`, the same +60pt/+100pt control offsets and +120pt/+100pt endpoint. It is **not** a byte-identity check and could not be, for the identical reason every other verification section here states: `--convert-to fodg` re-serialises the whole document through LibreOffice's own writer, which renames styles, reorders and reformats attributes, converts units, and adds defaults of its own (it even introduces its own exponent-notation `skewX (-5.00102713102941E-017)` alongside the rotation), so its bytes differ from `writeSxd`'s by construction. The written package's own parts carry no exponent-notation number anywhere.
|
|
382
|
+
|
|
383
|
+
The rendered PDF (2 pages) visually confirms each shape's geometry, its fill and stroke colours, the dashed ellipse border, the 30° clockwise rotation, the closed path's fill, and the centred bold title and mixed bold/italic/plain runs; the sample's `meta.xml` title, author, subject, and keywords all carry into the PDF's own metadata. **`draw:z-index` is honoured across both arrays here too**: LibreOffice re-emitted page 1's seven elements in exactly the authored paint order `0…6` — an ellipse written third came out first, a text frame written first came out last — the same finding the `writeOdg` verification records, now confirmed through the OpenOffice.org 1.x transform as well. The two LibreOffice behaviours the `writeOdg` section documents hold unchanged here: `svg:fill-rule` does not survive a LibreOffice round trip (it is real, spec-defined vocabulary that this package's own reader reads back, and LibreOffice's Draw export simply drops it), and `--convert-to pdf` renders every page at the first page's size even though both `style:page-master`s are preserved in the file and read back correctly.
|
|
384
|
+
|
|
385
|
+
##### What `.sxd` needed of the transform
|
|
386
|
+
|
|
387
|
+
`.sxw`, `.sxc` and `.sxi` each needed nothing of `src/ooo1/transform.ts` beyond what was already there. `.sxd` needed one genuine addition, and this verification is how it was found: **OpenOffice.org 1.x spells the drawing style family `style:family="graphics"`, where ODF renamed it to the singular `"graphic"`.** LibreOffice's own transformer does this rename (`xmloff/source/transform/StyleOOoTContext.cxx` maps `XML_GRAPHICS` onto `XML_FAMILY_TYPE_GRAPHIC` on the way in and writes `XML_GRAPHIC` back out), and the consequence of skipping it is silent rather than loud: an earlier version of this writer emitted ODF's singular spelling, LibreOffice imported the file with no error at all, and every shape's `draw:style-name` simply failed to resolve — each one falling back to LibreOffice's own default graphic style, so the whole sample rendered in the consumer's default blue-and-grey with not one authored fill or stroke colour anywhere in the output. `normaliseAttributeValue`/`reverseAttributeValue` now rename it in both directions, which also fixes the read side: `PROPERTY_TYPES_BY_FAMILY` is keyed on ODF's spelling, so a real `.sxd` or `.sxi` written by OpenOffice.org 1.x would previously have had its graphic styles left in an unsplit `style:properties` no reader looks at. Every other `style:family` value — including `presentation`, the drawing family's own sibling on a slide — is spelled identically in both vocabularies and passes through untouched.
|
|
388
|
+
|
|
389
|
+
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.
|
|
216
390
|
|
|
217
391
|
### What differs between the two vocabularies
|
|
218
392
|
|
|
@@ -223,7 +397,7 @@ ODF kept OpenOffice.org XML's document model and most of its element and attribu
|
|
|
223
397
|
- **One `style:properties` per style became ODF's family of typed `style:*-properties` elements.** This is the one difference a namespace rename cannot paper over, because the same attribute name is valid in several of them with a different meaning in each (`fo:background-color` is a character highlight, paragraph shading, or a cell fill depending on which element it sits in). The split follows LibreOffice's own algorithm — an ordered candidate list per style family, first match wins — and lives in `src/ooo1/properties.ts`.
|
|
224
398
|
- **Frames.** `draw:image`, `draw:text-box`, `draw:object` and their siblings are bare shapes carrying their own position and anchoring; ODF wraps each in a `draw:frame` that carries those instead.
|
|
225
399
|
- **Renames**: `text:ordered-list`/`text:unordered-list` → `text:list`, the footnote/endnote pair → the `text:note` family with a `text:note-class`, `text:tab-stop` → `text:tab`, `text:h/@text:level` → `@text:outline-level`, `office:font-decls`/`style:font-decl` → `office:font-face-decls`/`style:font-face`, `style:page-master` → `style:page-layout`, `table:sub-table` → `table:table` + `table:is-sub-table`, and the `meta:keywords` wrapper unwrapped to bare `meta:keyword` children.
|
|
226
|
-
- **Values, not just names**: lengths written in the `"inch"` unit become `"in"`; a cell's `table:value-*` attributes become `office:value-*`; the compound `style:text-underline`/`style:text-crossing-out` attributes expand into ODF's style/type/width triples; `fo:keep-with-next`'s boolean becomes `always`/`auto`; a package-internal `xlink:href` loses the `#` OpenOffice.org prefixed it with; and `office:annotation`/`office:change-info` move their author and date from attributes into `dc:creator`/`dc:date` child elements.
|
|
400
|
+
- **Values, not just names**: lengths written in the `"inch"` unit become `"in"`; a cell's `table:value-*` attributes become `office:value-*`; a drawing style's `style:family="graphics"` becomes ODF's singular `"graphic"` (the one style family the two vocabularies spell differently, and the one whose omission a real consumer punishes silently — see [What `.sxd` needed of the transform](#what-sxd-needed-of-the-transform)); the compound `style:text-underline`/`style:text-crossing-out` attributes expand into ODF's style/type/width triples; `fo:keep-with-next`'s boolean becomes `always`/`auto`; a package-internal `xlink:href` loses the `#` OpenOffice.org prefixed it with; and `office:annotation`/`office:change-info` move their author and date from attributes into `dc:creator`/`dc:date` child elements.
|
|
227
401
|
- **The package.** An OpenOffice.org 1.x package has no `mimetype` part at all — the manifest's `/` entry, in its own `http://openoffice.org/2001/manifest` namespace, is the only record of the document's type. The transform rewrites that entry to the OASIS media type and synthesises the `mimetype` part.
|
|
228
402
|
|
|
229
403
|
### Direct module imports
|
|
@@ -249,12 +423,12 @@ Layered from a lossless core outward, mirroring `ooxml.js`:
|
|
|
249
423
|
- **`src/package-io/`** — `write.ts` hoists `mimetype` first (stored) and `META-INF/manifest.xml` second if present; never fabricates either as a side effect.
|
|
250
424
|
- **`src/manifest.ts`** — full manifest read/write; the manifest is ODF's one mandatory part, unlike `ooxml.js`'s read-only OPC-relationship stance.
|
|
251
425
|
- **`src/styles/`** — `properties.ts`/`serialize.ts` (canonical property-bag ↔ XML attributes), `registry.ts` (`StyleRegistry`, the mandatory style-interning layer), `span.ts` (character-range `text:span` wrapping).
|
|
252
|
-
- **`src/typed/shared/`** — ODF-specific typed primitives every reader builds on (units, A1 cursors, colour/geometry, whitespace runs, style cascade, shared paragraph/table readers, transform/path parsing, metadata).
|
|
253
|
-
- **`src/typed/odt/`, `odp/`, `odg/`, `ods/`** — one module per format
|
|
254
|
-
- **`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).
|
|
426
|
+
- **`src/typed/shared/`** — ODF-specific typed primitives every reader/writer builds on (units, A1 cursors, colour/geometry, whitespace runs, style cascade, shared paragraph/table readers, transform/path parsing, metadata, `list.ts`'s write-side numId canonicalisation shared by `writeOdt`/`writeOdp`, `canonicalise.ts`'s write-side paragraph/table/image canonical form shared the same way).
|
|
427
|
+
- **`src/typed/odt/`, `odp/`, `odg/`, `ods/`** — one module per format: `read.ts` carries both levels of the reader (the package-native `readOdt`/`readOdp`/`readOdg`/`readOds` and the flat `readOdtContent`/`readOdpContent`/`readOdgContent`/`readOdsContent` it is built on); `odt/write.ts`, `ods/write.ts`, `odp/write.ts`, and `odg/write.ts` carry the write side the same way.
|
|
428
|
+
- **`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), plus the write-side mirror of `shapes.ts` in two halves: `write-shapes.ts` (`writeDrawFrame`/`writeDrawShapes` and the `canonicalDrawShape` canonical form, shared between `writeOdp` and `writeOdg`) and `write-vectors.ts` (`writeDrawVector`/`writeDrawVectors` and `canonicalDrawVector`, the vector-primitive half only `writeOdg` has content for).
|
|
255
429
|
- **`src/typed/formula/`, `odm/`** — `readOdfFormula`/`readOdfFormulaContent`/`readOdfFormulaMathMl` and `readOdm`.
|
|
256
430
|
- **`src/typed/odb/`** — `readOdbInventory`, `readOdbForm`/`readOdbReport`, `resolveOdbComponent`, `subDocumentPackage`.
|
|
257
|
-
- **`src/ooo1/`** — the OpenOffice.org 1.x variant reader: `ns.ts` (the pre-OASIS namespace and `application/vnd.sun.xml.*` media-type tables plus package detection), `properties.ts` (the `style:properties` split), `transform.ts` (the whole package rewrite), `read.ts` (`readSxw`/`readSxc`/`readSxi`/`readSxd`). Sits _beside_ `typed/`, not inside it: it adds no reader of its own, it feeds the
|
|
431
|
+
- **`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`/`writeSxi`/`writeSxiContent`/`writeSxd`/`writeSxdContent`). 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/`writeOdp`'s/`writeOdg`'s own output into `transformToOoo1Package` and the ODF readers' input through `transformOoo1Package`.
|
|
258
432
|
|
|
259
433
|
## Conventions
|
|
260
434
|
|
|
@@ -295,7 +469,7 @@ Layered from a lossless core outward, mirroring `ooxml.js`:
|
|
|
295
469
|
- **Master pages and page-break styles** — every `style:master-page` reads as part of a whole-page inventory: a paragraph style's `style:paragraph-properties/@style:master-page-name` switch opens a new `ContentSection` at that paragraph carrying the named master page's own geometry (`breakType: 'nextPage'` — ODF defines the switch as forcing a page break), and each master page's `style:header`/`style:footer` variants (default, `-left`, `-first`) read as real block flow on `OdtDocument.headerFooterParts`, with `sectionMasterPages` naming positionally which master page each section came from. Explicit page breaks ride the shared cascade: `fo:break-before`/`fo:break-after="page"` resolve onto the paragraph's `pageBreakBefore`/`pageBreakAfter` flags (`auto` to an explicit false; `column`/`even-page`/`odd-page` quarantine as residue through the unknown-properties path, since the boolean cannot hold their extra meaning).
|
|
296
470
|
- **The quarantined residue rows** — inline no-analogue constructs (`text:ruby`, `text:meta`, a heading's `text:is-list-header` flag) quarantine on their own paragraph beside the style-chain unknowns; document-level tenants nothing else owns (`xforms:model`, DDE connection declarations and `text:dde-source` links, vendor-extension elements) quarantine at the package tier on `OdtDocument.source`; `table:calculation-settings` and vendor-extension elements (Calc writes its `calcext:conditional-formats` inside each `table:table`) do the same on `OdsDocument.source`; an odp slide's presentation extras — the transition attributes off the slide's own drawing-page style, where every ODF schema version and real Impress output put them (the legacy `presentation:transition-*`/`presentation:duration` spelling and the ODF 1.2 `smil:type`/`-subtype`/`-direction`/`-fadeColor` one), plus `presentation:sound` and `anim:` trees — and every format's unmapped shape kinds (`dr3d:scene`, `draw:connector`, `draw:measure`, applet/plugin/floating-frame) quarantine on their own slide/page; an unrecognised `draw:custom-shape` preset's whole `draw:enhanced-geometry` quarantines in the text shape it degrades to; and every non-content XML part (`settings.xml`, `META-INF/manifest.rdf`, `Configurations2/…`) quarantines at the package tier keyed by its part path — never an embedded sub-document's own `Object N/` parts, which the semantic channel already carries whole.
|
|
297
471
|
|
|
298
|
-
Every read-side construct has no write-side counterpart
|
|
472
|
+
Every read-side construct listed above has no write-side counterpart: `writeOdt` refuses each of them by name rather than silently dropping it (see [Writing a document](#writing-a-document)), so the honest asymmetry is a reader that recovers more than any writer in this package will re-emit, not a package with no content writer at all; the lossless `encodePackage` layer remains the byte-fidelity tier regardless.
|
|
299
473
|
|
|
300
474
|
- **An embedded Math object in a spreadsheet cell reads as `objectKind: 'formula'`** — its `content.xml` root _is_ the MathML root, so `readDrawObjectReference` falls back to `findMathRoot` and dispatches to `readOdfFormulaContent`.
|
|
301
475
|
- **A `draw:frame`'s alternative text (`svg:title`, falling back to `svg:desc`) reads into `ContentImageBlock.altText`.**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { l as XmlElement } from "./node-B6JX5CRX.cjs";
|
|
2
2
|
import { r as Package } from "./package-DMk9fuTI.cjs";
|
|
3
3
|
import { n as StyleProperties } from "./properties-CiIoNu2h.cjs";
|
|
4
|
-
import { i as StyleFamily } from "./registry-
|
|
4
|
+
import { i as StyleFamily } from "./registry-PoQIRgbY.cjs";
|
|
5
5
|
//#region src/typed/shared/cascade.d.ts
|
|
6
6
|
interface CascadeDiagnostic {
|
|
7
7
|
severity: "warning";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { l as XmlElement } from "./node-B6JX5CRX.js";
|
|
2
2
|
import { r as Package } from "./package-otZYrxiO.js";
|
|
3
3
|
import { n as StyleProperties } from "./properties-CJyykyc_.js";
|
|
4
|
-
import { i as StyleFamily } from "./registry-
|
|
4
|
+
import { i as StyleFamily } from "./registry-C_16-N6i.js";
|
|
5
5
|
//#region src/typed/shared/cascade.d.ts
|
|
6
6
|
interface CascadeDiagnostic {
|
|
7
7
|
severity: "warning";
|
|
@@ -9,35 +9,43 @@ import "./fragment-9dNg-m46.cjs";
|
|
|
9
9
|
import "./manifest-BGs8Bbd_.cjs";
|
|
10
10
|
import "./properties-CiIoNu2h.cjs";
|
|
11
11
|
import "./serialize-BU7HZ8Qh.cjs";
|
|
12
|
-
import "./registry-
|
|
12
|
+
import "./registry-PoQIRgbY.cjs";
|
|
13
13
|
import "./span-DSM1SnHI.cjs";
|
|
14
14
|
import "./query-C69wTkqg.cjs";
|
|
15
15
|
import "./color-CqXjt_to.cjs";
|
|
16
16
|
import "./geometry-B7FZHSyD.cjs";
|
|
17
|
-
import "./text-
|
|
18
|
-
import "./cascade-
|
|
19
|
-
import "./metadata-
|
|
20
|
-
import "./list-
|
|
21
|
-
import "./paragraph-
|
|
22
|
-
import "./table-
|
|
17
|
+
import "./text-pmlTqzgi.cjs";
|
|
18
|
+
import "./cascade-BpMwrjkB.cjs";
|
|
19
|
+
import "./metadata-CYXgXL_h.cjs";
|
|
20
|
+
import "./list-BorrTJBY.cjs";
|
|
21
|
+
import "./paragraph-Bf6Iq5wC.cjs";
|
|
22
|
+
import "./table-CECMtPxZ.cjs";
|
|
23
|
+
import "./scaffold-44EIMGll.cjs";
|
|
23
24
|
import "./transform-M1gbHp4j.cjs";
|
|
24
25
|
import "./masterpage-KqZshNGZ.cjs";
|
|
25
|
-
import "./path-
|
|
26
|
-
import "./shapes-
|
|
26
|
+
import "./path-8HQS-J7y.cjs";
|
|
27
|
+
import "./shapes-CbBc9t0y.cjs";
|
|
28
|
+
import "./write-shapes-D4piGKf4.cjs";
|
|
29
|
+
import "./write-vectors-B-RVHAKI.cjs";
|
|
27
30
|
import "./embedded-tlaHMady.cjs";
|
|
28
31
|
import "./read-DHOQqjOZ.cjs";
|
|
32
|
+
import "./write-BhG3t3fJ.cjs";
|
|
29
33
|
import "./read-Dy5p1OMb.cjs";
|
|
34
|
+
import "./write-CPATqG4v.cjs";
|
|
30
35
|
import "./read-DSyz0YLj.cjs";
|
|
31
|
-
import "./
|
|
36
|
+
import "./write-C2c4xBXs.cjs";
|
|
37
|
+
import "./read-fphkvCpR.cjs";
|
|
38
|
+
import "./write-BuLQmwLD.cjs";
|
|
32
39
|
import "./read-j9o0IoMq.cjs";
|
|
33
40
|
import "./read-jVX8DHs3.cjs";
|
|
34
41
|
import "./read-D3V_MVAL.cjs";
|
|
35
42
|
import "./subdocument-CQptFIQK.cjs";
|
|
36
43
|
import "./forms-Dc74fCtk.cjs";
|
|
37
44
|
import "./form-coDwVdjG.cjs";
|
|
38
|
-
import "./ns-
|
|
39
|
-
import "./transform-
|
|
40
|
-
import "./read-
|
|
45
|
+
import "./ns-DrleaIZQ.cjs";
|
|
46
|
+
import "./transform-DFCPKX7e.cjs";
|
|
47
|
+
import "./read-BgnLFuSi.cjs";
|
|
48
|
+
import "./write-BZXpT8F6.cjs";
|
|
41
49
|
import "./report-39DyUx5W.cjs";
|
|
42
50
|
import { z } from "zod";
|
|
43
51
|
import { Alignment as Alignment$1, AlignmentSchema, DocumentTree as DocumentTree$1 } from "document-schema.js";
|
|
@@ -9,35 +9,43 @@ import "./fragment-ClgCV_if.js";
|
|
|
9
9
|
import "./manifest-Bju-_gTQ.js";
|
|
10
10
|
import "./properties-CJyykyc_.js";
|
|
11
11
|
import "./serialize-CWz84e0m.js";
|
|
12
|
-
import "./registry-
|
|
12
|
+
import "./registry-C_16-N6i.js";
|
|
13
13
|
import "./span-C3aJuEOM.js";
|
|
14
14
|
import "./query-CbxD-WiQ.js";
|
|
15
15
|
import "./color-CqXjt_to.js";
|
|
16
16
|
import "./geometry-BI33aJpv.js";
|
|
17
|
-
import "./text-
|
|
18
|
-
import "./cascade-
|
|
19
|
-
import "./metadata-
|
|
20
|
-
import "./list-
|
|
21
|
-
import "./paragraph-
|
|
22
|
-
import "./table-
|
|
17
|
+
import "./text-B8350HA2.js";
|
|
18
|
+
import "./cascade-PDS3hgrP.js";
|
|
19
|
+
import "./metadata-BUzwE_-u.js";
|
|
20
|
+
import "./list-7FO0SDqZ.js";
|
|
21
|
+
import "./paragraph-DtC86tft.js";
|
|
22
|
+
import "./table-XuniPr2I.js";
|
|
23
|
+
import "./scaffold-ehGsIpZ5.js";
|
|
23
24
|
import "./transform-BFNc16LQ.js";
|
|
24
25
|
import "./masterpage-BLK2OJ9q.js";
|
|
25
|
-
import "./path-
|
|
26
|
-
import "./shapes-
|
|
26
|
+
import "./path-8HQS-J7y.js";
|
|
27
|
+
import "./shapes-B6Ms7ibr.js";
|
|
28
|
+
import "./write-shapes-Co7BrmMZ.js";
|
|
29
|
+
import "./write-vectors-CYjZoVBW.js";
|
|
27
30
|
import "./embedded-C8tiMCE_.js";
|
|
28
31
|
import "./read-DUOJ-1hX.js";
|
|
32
|
+
import "./write-CfTE2jY8.js";
|
|
29
33
|
import "./read-BcxAWBB5.js";
|
|
34
|
+
import "./write-JERychL3.js";
|
|
30
35
|
import "./read-D3wLfr-7.js";
|
|
31
|
-
import "./
|
|
36
|
+
import "./write-4kMi-asu.js";
|
|
37
|
+
import "./read-A3CJkqVM.js";
|
|
38
|
+
import "./write-DttX1rTC.js";
|
|
32
39
|
import "./read-C_oVZAnY.js";
|
|
33
40
|
import "./read-8s8AdaEd.js";
|
|
34
41
|
import "./read-B2jnd5eS.js";
|
|
35
42
|
import "./subdocument-BhabEY9P.js";
|
|
36
43
|
import "./forms-Eq2p1ss3.js";
|
|
37
44
|
import "./form-D6XHM5SR.js";
|
|
38
|
-
import "./ns-
|
|
39
|
-
import "./transform-
|
|
40
|
-
import "./read-
|
|
45
|
+
import "./ns-E0TfX9tZ.js";
|
|
46
|
+
import "./transform-DPliWQuD.js";
|
|
47
|
+
import "./read-CqaZ01kU.js";
|
|
48
|
+
import "./write-SKnCTU4C.js";
|
|
41
49
|
import "./report-x9RuNwmr.js";
|
|
42
50
|
import { z } from "zod";
|
|
43
51
|
import { Alignment as Alignment$1, AlignmentSchema as AlignmentSchema$1, DocumentTree as DocumentTree$1 } from "document-schema.js";
|
package/dist/codec.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as xmlCodec, n as encodePackage, r as packageCodec, t as decodePackage } from "./codec-
|
|
1
|
+
import { i as xmlCodec, n as encodePackage, r as packageCodec, t as decodePackage } from "./codec-CFdlbFLJ.cjs";
|
|
2
2
|
export { decodePackage, encodePackage, packageCodec, xmlCodec };
|
package/dist/codec.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as xmlCodec, n as encodePackage, r as packageCodec, t as decodePackage } from "./codec-
|
|
1
|
+
import { i as xmlCodec, n as encodePackage, r as packageCodec, t as decodePackage } from "./codec-DTAgph2r.js";
|
|
2
2
|
export { decodePackage, encodePackage, packageCodec, xmlCodec };
|
|
@@ -48,7 +48,7 @@ function readColumnLayout(columnElement, pkg) {
|
|
|
48
48
|
const properties = styleElement === void 0 ? void 0 : childrenWithTag(styleElement, "style:table-column-properties")[0];
|
|
49
49
|
const widthValue = properties === void 0 ? void 0 : attrValue(properties, "style:column-width");
|
|
50
50
|
return {
|
|
51
|
-
widthPt: widthValue === void 0 ?
|
|
51
|
+
widthPt: widthValue === void 0 ? 64 : parseOdfLength(widthValue) ?? 64,
|
|
52
52
|
manualBreak: (properties === void 0 ? void 0 : attrValue(properties, "fo:break-before")) === "page"
|
|
53
53
|
};
|
|
54
54
|
}
|
|
@@ -58,7 +58,7 @@ function readRowLayout(rowElement, pkg) {
|
|
|
58
58
|
const properties = styleElement === void 0 ? void 0 : childrenWithTag(styleElement, "style:table-row-properties")[0];
|
|
59
59
|
const heightValue = properties === void 0 ? void 0 : attrValue(properties, "style:row-height");
|
|
60
60
|
return {
|
|
61
|
-
heightPt: heightValue === void 0 ?
|
|
61
|
+
heightPt: heightValue === void 0 ? 15 : parseOdfLength(heightValue) ?? 15,
|
|
62
62
|
manualBreak: (properties === void 0 ? void 0 : attrValue(properties, "fo:break-before")) === "page"
|
|
63
63
|
};
|
|
64
64
|
}
|
|
@@ -439,8 +439,7 @@ function resolveParagraphMasterPageName(element, pkg, state) {
|
|
|
439
439
|
if (state.masterPageByStyleName.has(styleName)) return state.masterPageByStyleName.get(styleName);
|
|
440
440
|
let resolved;
|
|
441
441
|
for (const style of resolveStyleElementChain(styleName, "paragraph", pkg).elements) {
|
|
442
|
-
const
|
|
443
|
-
const name = properties === void 0 ? void 0 : attrValue(properties, "style:master-page-name");
|
|
442
|
+
const name = attrValue(style, "style:master-page-name");
|
|
444
443
|
if (name !== void 0) resolved = name;
|
|
445
444
|
}
|
|
446
445
|
state.masterPageByStyleName.set(styleName, resolved);
|
|
@@ -888,4 +887,4 @@ function readDrawObjectReference(frame, pkg) {
|
|
|
888
887
|
};
|
|
889
888
|
}
|
|
890
889
|
//#endregion
|
|
891
|
-
export { readOdtContent as a, readOdt as i, readEmbeddedObjectDocument as n,
|
|
890
|
+
export { readOdtContent as a, readOds as c, readOdt as i, readOdsContent as l, readEmbeddedObjectDocument as n, DEFAULT_COLUMN_WIDTH_PT as o, readOdfChartContent as r, DEFAULT_ROW_HEIGHT_PT as s, readDrawObjectReference as t };
|
|
@@ -48,7 +48,7 @@ function readColumnLayout(columnElement, pkg) {
|
|
|
48
48
|
const properties = styleElement === void 0 ? void 0 : require_xml_query.childrenWithTag(styleElement, "style:table-column-properties")[0];
|
|
49
49
|
const widthValue = properties === void 0 ? void 0 : require_xml_query.attrValue(properties, "style:column-width");
|
|
50
50
|
return {
|
|
51
|
-
widthPt: widthValue === void 0 ?
|
|
51
|
+
widthPt: widthValue === void 0 ? 64 : require_typed_shared_units.parseOdfLength(widthValue) ?? 64,
|
|
52
52
|
manualBreak: (properties === void 0 ? void 0 : require_xml_query.attrValue(properties, "fo:break-before")) === "page"
|
|
53
53
|
};
|
|
54
54
|
}
|
|
@@ -58,7 +58,7 @@ function readRowLayout(rowElement, pkg) {
|
|
|
58
58
|
const properties = styleElement === void 0 ? void 0 : require_xml_query.childrenWithTag(styleElement, "style:table-row-properties")[0];
|
|
59
59
|
const heightValue = properties === void 0 ? void 0 : require_xml_query.attrValue(properties, "style:row-height");
|
|
60
60
|
return {
|
|
61
|
-
heightPt: heightValue === void 0 ?
|
|
61
|
+
heightPt: heightValue === void 0 ? 15 : require_typed_shared_units.parseOdfLength(heightValue) ?? 15,
|
|
62
62
|
manualBreak: (properties === void 0 ? void 0 : require_xml_query.attrValue(properties, "fo:break-before")) === "page"
|
|
63
63
|
};
|
|
64
64
|
}
|
|
@@ -439,8 +439,7 @@ function resolveParagraphMasterPageName(element, pkg, state) {
|
|
|
439
439
|
if (state.masterPageByStyleName.has(styleName)) return state.masterPageByStyleName.get(styleName);
|
|
440
440
|
let resolved;
|
|
441
441
|
for (const style of require_typed_shared_cascade.resolveStyleElementChain(styleName, "paragraph", pkg).elements) {
|
|
442
|
-
const
|
|
443
|
-
const name = properties === void 0 ? void 0 : require_xml_query.attrValue(properties, "style:master-page-name");
|
|
442
|
+
const name = require_xml_query.attrValue(style, "style:master-page-name");
|
|
444
443
|
if (name !== void 0) resolved = name;
|
|
445
444
|
}
|
|
446
445
|
state.masterPageByStyleName.set(styleName, resolved);
|
|
@@ -888,6 +887,18 @@ function readDrawObjectReference(frame, pkg) {
|
|
|
888
887
|
};
|
|
889
888
|
}
|
|
890
889
|
//#endregion
|
|
890
|
+
Object.defineProperty(exports, "DEFAULT_COLUMN_WIDTH_PT", {
|
|
891
|
+
enumerable: true,
|
|
892
|
+
get: function() {
|
|
893
|
+
return DEFAULT_COLUMN_WIDTH_PT;
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
Object.defineProperty(exports, "DEFAULT_ROW_HEIGHT_PT", {
|
|
897
|
+
enumerable: true,
|
|
898
|
+
get: function() {
|
|
899
|
+
return DEFAULT_ROW_HEIGHT_PT;
|
|
900
|
+
}
|
|
901
|
+
});
|
|
891
902
|
Object.defineProperty(exports, "readDrawObjectReference", {
|
|
892
903
|
enumerable: true,
|
|
893
904
|
get: function() {
|