From 0be7e8c3f60f5119c2d3ba1d66675059970afbe8 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Fri, 4 Sep 2026 12:45:36 +0100 Subject: [PATCH] feat(odf.js): write .ott/.ots/.otp/.otg templates, and their .stw/.stc/.sti/.std counterparts writeOdtContent/writeOdsContent/writeOdpContent/writeOdgContent could only ever stamp a document's base ODF media type, even though the reader already accepted a template's content.xml/styles.xml as structurally identical to its non-template counterpart. A template write option therefore needed nothing beyond selecting which media type createOdfPackage stamps: syncManifest already derives the manifest root entry from whatever readMimetype(pkg) reports, so stamping ODF_MEDIA_TYPES.ott/.ots/.otp/.otg instead of the base type is the whole change. The OpenOffice.org 1.x writers (writeSxw/writeSxc/writeSxi/writeSxd) need no code of their own to inherit this: each already threads its own options straight through to the ODF writer it wraps, and transformToOoo1Package already derives the package's own .stw/.stc/ .sti/.std media type from whatever ODF media type it finds stamped on the package via the existing ooo1MediaTypeForOdfMediaType table. Verified against real LibreOffice 26.2.5.2: an .ott/.ots built with template: true, and a .stw/.stc built the same way through the OpenOffice.org 1.x writers, all convert cleanly and are correctly identified as genuine Writer/Calc documents from their media type alone, regardless of file extension. --- packages/odf.js/README.md | 16 ++++---- packages/odf.js/src/ooo1/write.test.ts | 43 +++++++++++++++++++++ packages/odf.js/src/typed/odg/write.test.ts | 13 +++++++ packages/odf.js/src/typed/odg/write.ts | 8 +++- packages/odf.js/src/typed/odp/write.test.ts | 13 +++++++ packages/odf.js/src/typed/odp/write.ts | 4 +- packages/odf.js/src/typed/ods/write.test.ts | 24 ++++++++++++ packages/odf.js/src/typed/ods/write.ts | 4 +- packages/odf.js/src/typed/odt/write.test.ts | 15 +++++++ packages/odf.js/src/typed/odt/write.ts | 8 +++- 10 files changed, 136 insertions(+), 12 deletions(-) diff --git a/packages/odf.js/README.md b/packages/odf.js/README.md index a0224d458..e5511e75b 100644 --- a/packages/odf.js/README.md +++ b/packages/odf.js/README.md @@ -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. @@ -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. diff --git a/packages/odf.js/src/ooo1/write.test.ts b/packages/odf.js/src/ooo1/write.test.ts index 7dcfeb609..13c69446b 100644 --- a/packages/odf.js/src/ooo1/write.test.ts +++ b/packages/odf.js/src/ooo1/write.test.ts @@ -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"; @@ -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( @@ -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", [])])), @@ -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()])])), @@ -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])])), diff --git a/packages/odf.js/src/typed/odg/write.test.ts b/packages/odf.js/src/typed/odg/write.test.ts index ef0055104..973f0b0ea 100644 --- a/packages/odf.js/src/typed/odg/write.test.ts +++ b/packages/odf.js/src/typed/odg/write.test.ts @@ -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", () => { diff --git a/packages/odf.js/src/typed/odg/write.ts b/packages/odf.js/src/typed/odg/write.ts index af5acaac6..47cb1827c 100644 --- a/packages/odf.js/src/typed/odg/write.ts +++ b/packages/odf.js/src/typed/odg/write.ts @@ -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 ---------------------------------------- @@ -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 }, diff --git a/packages/odf.js/src/typed/odp/write.test.ts b/packages/odf.js/src/typed/odp/write.test.ts index ef7d4ff2b..98b9d36b8 100644 --- a/packages/odf.js/src/typed/odp/write.test.ts +++ b/packages/odf.js/src/typed/odp/write.test.ts @@ -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", () => { diff --git a/packages/odf.js/src/typed/odp/write.ts b/packages/odf.js/src/typed/odp/write.ts index 6e7b3d18a..6006c67c2 100644 --- a/packages/odf.js/src/typed/odp/write.ts +++ b/packages/odf.js/src/typed/odp/write.ts @@ -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 ---------------------------------------- @@ -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, ); diff --git a/packages/odf.js/src/typed/ods/write.test.ts b/packages/odf.js/src/typed/ods/write.test.ts index 380f7e26e..b6eb56a09 100644 --- a/packages/odf.js/src/typed/ods/write.test.ts +++ b/packages/odf.js/src/typed/ods/write.test.ts @@ -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. @@ -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( diff --git a/packages/odf.js/src/typed/ods/write.ts b/packages/odf.js/src/typed/ods/write.ts index c2fb44bbf..daf9584cb 100644 --- a/packages/odf.js/src/typed/ods/write.ts +++ b/packages/odf.js/src/typed/ods/write.ts @@ -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 { @@ -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, ); diff --git a/packages/odf.js/src/typed/odt/write.test.ts b/packages/odf.js/src/typed/odt/write.test.ts index a815dcb80..3bfa055b8 100644 --- a/packages/odf.js/src/typed/odt/write.test.ts +++ b/packages/odf.js/src/typed/odt/write.test.ts @@ -184,6 +184,21 @@ describe("writeOdtContent: package structure", () => { "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", ); }); + + it("stamps the template media type, in both the mimetype part and the manifest root entry, when template is requested", () => { + const template = writeOdtContent( + documentOf([{ kind: "paragraph", runs: [{ text: "Body" }] }]), + { template: true }, + ); + assertMimetypeEntryLayout( + encodePackage(template), + "application/vnd.oasis.opendocument.text-template", + ); + expect( + readManifest(template).entries.find((entry) => entry.fullPath === "/") + ?.mediaType, + ).toBe("application/vnd.oasis.opendocument.text-template"); + }); }); describe("writeOdtContent: paragraphs and runs", () => { diff --git a/packages/odf.js/src/typed/odt/write.ts b/packages/odf.js/src/typed/odt/write.ts index 9de791a96..a18502602 100644 --- a/packages/odf.js/src/typed/odt/write.ts +++ b/packages/odf.js/src/typed/odt/write.ts @@ -59,6 +59,8 @@ const PICTURES_DIRECTORY = "Pictures"; export interface OdtWriteOptions { // 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.ott) rather than a regular document (ODF_MEDIA_TYPES.odt) -- 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 block plan: one description of an .odt's block flow, shared by the writer and the normaliser ------------- @@ -467,7 +469,11 @@ export function writeOdtContent( } const version = options.version ?? DEFAULT_ODF_VERSION; const textElement = el("office:text"); - const pkg = createOdfPackage(ODF_MEDIA_TYPES.odt, textElement, version); + const pkg = createOdfPackage( + options.template ? ODF_MEDIA_TYPES.ott : ODF_MEDIA_TYPES.odt, + textElement, + version, + ); const state: OdtWriteState = { pkg,