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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions packages/odf.js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Under active development. Built and shipped:
- **`readOdbInventory`** — resolves a `.odb` into connection info, table names, query definitions (`{ name, command, escapeProcessing? }` with real SQL text), and form/report `{ name, href }` pairs. A sub-document directory is named after an opaque _persistent_ name (`forms/Obj11`), not the user-visible name.
- **`readOdbForm`/`readOdbReport`** — extract one sub-document's _static structure_, executing nothing: a form's control tree and data bindings, or a report's band stack, recursive group tree, bound fields, and computed expressions.
- **OpenOffice.org 1.x / StarOffice 6-7 reading** (`readSxw`/`readSxc`/`readSxi`/`readSxd` and their `*Content` siblings, plus `transformOoo1Package` and `isOoo1Package`) — the pre-OASIS ancestor ODF 1.0 was based on, read through the ODF readers above rather than beside them. See [Reading and writing an OpenOffice.org 1.x document](#reading-and-writing-an-openofficeorg-1x-document).
- **OpenOffice.org 1.x writing** (`writeSxw`/`writeSxwContent`, `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; the `.stw`/`.stc`/`.sti`/`.std` template variants of all four still read but do not write — see [Reading and writing an OpenOffice.org 1.x document](#reading-and-writing-an-openofficeorg-1x-document) for exactly why.
- **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).
- **The odt writer, at the same two levels** — `writeOdt` takes the `DocumentTree` `readOdt` returns and `writeOdtContent` the flat `ContentDocument` `readOdtContent` returns, and both produce a real `.odt` `Package` (`encodePackage` turns it into bytes). Paragraphs, headings, runs with character formatting and hyperlinks, whitespace, lists, tables, images, explicit page breaks, per-section page geometry, and `meta.xml` all round-trip; the fidelity constructs and embedded objects are refused by name rather than silently dropped. See [Writing a document](#writing-a-document).
- **The ods writer, at the same two levels** — `writeOds`/`writeOdsContent`, the genuine inverse of `readOds`/`readOdsContent`. Every `office:value-type` a cell can carry (float/percentage/currency/boolean/date/time/string, plus a value-less cell), column widths, row heights, hidden rows/columns, merged ranges, cell background/borders/alignment/vertical-alignment, verbatim formulas, cell-anchored images, and print settings (page geometry, gridlines/headers, page order, scale/fit-to-page, print range, repeated header rows/columns, manual page breaks) all round-trip. Embedded objects, data-validation rules, and conditional-formatting rules are refused by name — `readOdsContent` has no write-side counterpart for any of the three yet. See [Writing a document](#writing-a-document).
- **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.
Expand Down Expand Up @@ -319,14 +319,14 @@ const document = readSxw(decodePackage(sxwBytes)); // a wordprocessing DocumentT
const { sheets } = readSxcContent(decodePackage(sxcBytes)); // the flat ContentSheet[] shape
```

| Format read | Reader | Flat sibling | Format written | Writer | Flat sibling | Package kind |
| --------------- | --------- | ---------------- | -------------- | ---------- | ----------------- | ---------------- |
| `.sxw` / `.stw` | `readSxw` | `readSxwContent` | `.sxw` only | `writeSxw` | `writeSxwContent` | `wordprocessing` |
| `.sxc` / `.stc` | `readSxc` | `readSxcContent` | `.sxc` only | `writeSxc` | `writeSxcContent` | `spreadsheet` |
| `.sxi` / `.sti` | `readSxi` | `readSxiContent` | `.sxi` only | `writeSxi` | `writeSxiContent` | `presentation` |
| `.sxd` / `.std` | `readSxd` | `readSxdContent` | `.sxd` only | `writeSxd` | `writeSxdContent` | `drawing` |
| Format read | Reader | Flat sibling | Format written | Writer | Flat sibling | Package kind |
| --------------- | --------- | ---------------- | --------------- | ---------- | ----------------- | ---------------- |
| `.sxw` / `.stw` | `readSxw` | `readSxwContent` | `.sxw` / `.stw` | `writeSxw` | `writeSxwContent` | `wordprocessing` |
| `.sxc` / `.stc` | `readSxc` | `readSxcContent` | `.sxc` / `.stc` | `writeSxc` | `writeSxcContent` | `spreadsheet` |
| `.sxi` / `.sti` | `readSxi` | `readSxiContent` | `.sxi` / `.sti` | `writeSxi` | `writeSxiContent` | `presentation` |
| `.sxd` / `.std` | `readSxd` | `readSxdContent` | `.sxd` / `.std` | `writeSxd` | `writeSxdContent` | `drawing` |

The `.st*` template variant of each format reads (its `content.xml`/`styles.xml` are structurally identical to its `.sx*` counterpart, and `transformOoo1Package` does not distinguish them) but does not write: every `Od*WriteOptions` exposes only `version?: string`, with no way to select a template media type for `transformToOoo1Package` to carry through, so `writeSxw`/`writeSxc`/`writeSxi`/`writeSxd` always produce their format's own base (non-template) media type. Template write support is real, separate scope, tracked on [#821](https://github.com/ExaDev/documents.js/issues/821) alongside the identical gap in the ODF writers underneath them (`writeOdt`/`writeOds`/`writeOdp`/`writeOdg` have no template-write option of their own to build this on).
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.

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.

Expand Down
43 changes: 43 additions & 0 deletions packages/odf.js/src/ooo1/write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
findChildElement,
} from "../xml/query";
import { readMimetype } from "../mimetype";
import { readManifest } from "../manifest";
import { normaliseOdtContent } from "../typed/odt/write";
import { normaliseOdsContent } from "../typed/ods/write";
import { normaliseOdpContent } from "../typed/odp/write";
Expand Down Expand Up @@ -299,6 +300,17 @@ describe("writeSxwContent produces genuine OpenOffice.org 1.x XML, not merely so
expect(isOoo1Package(pkg)).toBe(true);
});

it("declares the .stw template media type in the manifest root entry when template is requested -- derived from writeOdtContent's own template option through ooo1MediaTypeForOdfMediaType, with no template-specific code of its own", () => {
const pkg = writeSxwContent(
documentOf([{ kind: "paragraph", runs: [{ text: "x" }] }]),
{ template: true },
);
expect(
readManifest(pkg).entries.find((entry) => entry.fullPath === "/")
?.mediaType,
).toBe("application/vnd.sun.xml.writer.template");
});

it("declares the OpenOffice.org 1.x namespace URIs, not the OASIS ones", () => {
const { root } = contentRootOf(
writeSxwContent(
Expand Down Expand Up @@ -857,6 +869,16 @@ describe("writeSxcContent produces genuine OpenOffice.org 1.x XML, not merely so
expect(isOoo1Package(pkg)).toBe(true);
});

it("declares the .stc template media type in the manifest root entry when template is requested", () => {
const pkg = writeSxcContent(sheetDocumentOf([sheetOf("Sheet1", [])]), {
template: true,
});
expect(
readManifest(pkg).entries.find((entry) => entry.fullPath === "/")
?.mediaType,
).toBe("application/vnd.sun.xml.calc.template");
});

it("declares the OpenOffice.org 1.x namespace URIs and office:class='spreadsheet'", () => {
const { root } = sheetContentRootOf(
writeSxcContent(sheetDocumentOf([sheetOf("Sheet1", [])])),
Expand Down Expand Up @@ -1172,6 +1194,17 @@ describe("writeSxiContent produces genuine OpenOffice.org 1.x XML, not merely so
expect(isOoo1Package(pkg)).toBe(true);
});

it("declares the .sti template media type in the manifest root entry when template is requested", () => {
const pkg = writeSxiContent(
presentationDocumentOf([slideOf([shapeOf()])]),
{ template: true },
);
expect(
readManifest(pkg).entries.find((entry) => entry.fullPath === "/")
?.mediaType,
).toBe("application/vnd.sun.xml.impress.template");
});

it("declares the OpenOffice.org 1.x namespace URIs and office:class='presentation'", () => {
const { root } = presentationContentRootOf(
writeSxiContent(presentationDocumentOf([slideOf([shapeOf()])])),
Expand Down Expand Up @@ -1524,6 +1557,16 @@ describe("writeSxdContent produces genuine OpenOffice.org 1.x XML, not merely so
expect(isOoo1Package(pkg)).toBe(true);
});

it("declares the .std template media type in the manifest root entry when template is requested", () => {
const pkg = writeSxdContent(drawingDocumentOf([drawPageOf([RECT])]), {
template: true,
});
expect(
readManifest(pkg).entries.find((entry) => entry.fullPath === "/")
?.mediaType,
).toBe("application/vnd.sun.xml.draw.template");
});

it("declares the OpenOffice.org 1.x namespace URIs and office:class='drawing'", () => {
const { root } = drawingContentRootOf(
writeSxdContent(drawingDocumentOf([drawPageOf([RECT])])),
Expand Down
13 changes: 13 additions & 0 deletions packages/odf.js/src/typed/odg/write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,19 @@ describe("writeOdgContent: package structure", () => {
writeOdgContent({ kind: "presentation", metadata: {}, slides: [] }),
).toThrow(/expected a 'drawing' document, got 'presentation'/);
});

it("declares the template media type, in both the mimetype part and the manifest root entry, when template is requested", () => {
const template = writeOdgContent(documentOf([page([rect()])]), {
template: true,
});
expect(readMimetype(template)).toBe(
"application/vnd.oasis.opendocument.graphics-template",
);
expect(
readManifest(template).entries.find((entry) => entry.fullPath === "/")
?.mediaType,
).toBe("application/vnd.oasis.opendocument.graphics-template");
});
});

describe("writeOdgContent: page geometry", () => {
Expand Down
8 changes: 7 additions & 1 deletion packages/odf.js/src/typed/odg/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const STYLES_PART = "styles.xml";
export interface OdgWriteOptions {
// The ODF version stamped on each part's office:version and on the manifest. Defaults to the current standard.
readonly version?: string;
// Stamps the package as a document template (ODF_MEDIA_TYPES.otg) rather than a regular document (ODF_MEDIA_TYPES.odg) -- the "mimetype" part and the manifest root entry syncManifest derives from it, both of which createOdfPackage/syncManifest already key off whatever media type is passed in. Nothing else about the writer's own output changes: ODF makes no other structural distinction between a document and its template. Defaults to false.
readonly template?: boolean;
}

// --- the canonical form: what reading this writer's own output back produces ----------------------------------------
Expand Down Expand Up @@ -123,7 +125,11 @@ export function writeOdgContent(
}
const version = options.version ?? DEFAULT_ODF_VERSION;
const drawingElement = el("office:drawing");
const pkg = createOdfPackage(ODF_MEDIA_TYPES.odg, drawingElement, version);
const pkg = createOdfPackage(
options.template ? ODF_MEDIA_TYPES.otg : ODF_MEDIA_TYPES.odg,
drawingElement,
version,
);

const registry = StyleRegistry.forPart(pkg, CONTENT_PART, {
otherPart: { pkg, partPath: STYLES_PART },
Expand Down
13 changes: 13 additions & 0 deletions packages/odf.js/src/typed/odp/write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,19 @@ describe("writeOdpContent: package structure", () => {
it("writes one draw:page for one slide", () => {
expect(pagesOf(pkg)).toHaveLength(1);
});

it("declares the template media type, in both the mimetype part and the manifest root entry, when template is requested", () => {
const template = writeOdpContent(documentOf([slide([shape()])]), {
template: true,
});
expect(readMimetype(template)).toBe(
"application/vnd.oasis.opendocument.presentation-template",
);
expect(
readManifest(template).entries.find((entry) => entry.fullPath === "/")
?.mediaType,
).toBe("application/vnd.oasis.opendocument.presentation-template");
});
});

describe("writeOdpContent: slide page geometry", () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/odf.js/src/typed/odp/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const STYLES_PART = "styles.xml";
export interface OdpWriteOptions {
// The ODF version stamped on each part's office:version and on the manifest. Defaults to the current standard.
readonly version?: string;
// Stamps the package as a document template (ODF_MEDIA_TYPES.otp) rather than a regular document (ODF_MEDIA_TYPES.odp) -- the "mimetype" part and the manifest root entry syncManifest derives from it, both of which createOdfPackage/syncManifest already key off whatever media type is passed in. Nothing else about the writer's own output changes: ODF makes no other structural distinction between a document and its template. Defaults to false.
readonly template?: boolean;
}

// --- the canonical form: what reading this writer's own output back produces ----------------------------------------
Expand Down Expand Up @@ -176,7 +178,7 @@ export function writeOdpContent(
const version = options.version ?? DEFAULT_ODF_VERSION;
const presentationElement = el("office:presentation");
const pkg = createOdfPackage(
ODF_MEDIA_TYPES.odp,
options.template ? ODF_MEDIA_TYPES.otp : ODF_MEDIA_TYPES.odp,
presentationElement,
version,
);
Expand Down
24 changes: 24 additions & 0 deletions packages/odf.js/src/typed/ods/write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
findChildElement,
rootElement,
} from "../../xml/query";
import { readManifest } from "../../manifest";
import { readMimetype } from "../../mimetype";
import { writeOdsContent } from "./write";

// The write side's XML-shape suite: what writeOdsContent actually emits, construct by construct -- the sibling suite (write-round-trip.test.ts) proves the output reads back as the document it came from; this one proves the output is the ODF a real consumer expects, which a round trip through this package's own reader cannot (a writer and reader that agreed on the same wrong spelling would round-trip perfectly and open nowhere). This mirrors typed/odt/write.test.ts's own stated split of responsibility.
Expand Down Expand Up @@ -110,6 +112,28 @@ function masterStyles(pkg: Package): XmlElement {
return container;
}

describe("writeOdsContent: package structure", () => {
it("declares the spreadsheet media type", () => {
const pkg = writeOdsContent(documentOf([sheetOf([])]));
expect(readMimetype(pkg)).toBe(
"application/vnd.oasis.opendocument.spreadsheet",
);
});

it("declares the template media type, in both the mimetype part and the manifest root entry, when template is requested", () => {
const template = writeOdsContent(documentOf([sheetOf([])]), {
template: true,
});
expect(readMimetype(template)).toBe(
"application/vnd.oasis.opendocument.spreadsheet-template",
);
expect(
readManifest(template).entries.find((entry) => entry.fullPath === "/")
?.mediaType,
).toBe("application/vnd.oasis.opendocument.spreadsheet-template");
});
});

describe("writeOdsContent XML shapes", () => {
it("writes a plain number cell as office:value-type='float' with office:value", () => {
const pkg = writeOdsContent(
Expand Down
4 changes: 3 additions & 1 deletion packages/odf.js/src/typed/ods/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ const PICTURES_DIRECTORY = "Pictures";
export interface OdsWriteOptions {
// The ODF version stamped on each part's office:version and on the manifest. Defaults to the current standard.
readonly version?: string;
// Stamps the package as a document template (ODF_MEDIA_TYPES.ots) rather than a regular document (ODF_MEDIA_TYPES.ods) -- the "mimetype" part and the manifest root entry syncManifest derives from it, both of which createOdfPackage/syncManifest already key off whatever media type is passed in. Nothing else about the writer's own output changes: ODF makes no other structural distinction between a document and its template. Defaults to false.
readonly template?: boolean;
}

function unsupported(what: string, where: string): Error {
Expand Down Expand Up @@ -1044,7 +1046,7 @@ export function writeOdsContent(
const version = options.version ?? DEFAULT_ODF_VERSION;
const spreadsheetElement = el("office:spreadsheet");
const pkg = createOdfPackage(
ODF_MEDIA_TYPES.ods,
options.template ? ODF_MEDIA_TYPES.ots : ODF_MEDIA_TYPES.ods,
spreadsheetElement,
version,
);
Expand Down
Loading