From 8e70a465a59adff637c380692d5ced7439be80f3 Mon Sep 17 00:00:00 2001 From: Adam Sherwood Date: Wed, 22 Jan 2025 10:15:12 +0000 Subject: [PATCH 1/3] Adds overrides parameter to Packer methods and compiler --- src/export/packer/next-compiler.ts | 12 +++++++-- src/export/packer/packer.ts | 42 ++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index 20ab00b972..6eb1d5a11c 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -9,7 +9,7 @@ import { ImageReplacer } from "./image-replacer"; import { NumberingReplacer } from "./numbering-replacer"; import { PrettifyType } from "./packer"; -type IXmlifyedFile = { +export type IXmlifyedFile = { readonly data: string; readonly path: string; }; @@ -47,7 +47,11 @@ export class Compiler { this.numberingReplacer = new NumberingReplacer(); } - public compile(file: File, prettifyXml?: (typeof PrettifyType)[keyof typeof PrettifyType]): JSZip { + public compile( + file: File, + prettifyXml?: (typeof PrettifyType)[keyof typeof PrettifyType], + overrides: readonly IXmlifyedFile[] = [], + ): JSZip { const zip = new JSZip(); const xmlifiedFileMapping = this.xmlifyFile(file, prettifyXml); const map = new Map(Object.entries(xmlifiedFileMapping)); @@ -62,6 +66,10 @@ export class Compiler { } } + for (const subFile of overrides) { + zip.file(subFile.path, subFile.data); + } + for (const data of file.Media.Array) { if (data.type !== "svg") { zip.file(`word/media/${data.fileName}`, data.data); diff --git a/src/export/packer/packer.ts b/src/export/packer/packer.ts index 78dff25943..e00ebfe3ff 100644 --- a/src/export/packer/packer.ts +++ b/src/export/packer/packer.ts @@ -2,7 +2,7 @@ import { Stream } from "stream"; import { File } from "@file/file"; -import { Compiler } from "./next-compiler"; +import { Compiler, IXmlifyedFile } from "./next-compiler"; /** * Use blanks to prettify @@ -21,8 +21,12 @@ const convertPrettifyType = ( prettify === true ? PrettifyType.WITH_2_BLANKS : prettify === false ? undefined : prettify; export class Packer { - public static async toString(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Promise { - const zip = this.compiler.compile(file, convertPrettifyType(prettify)); + public static async toString( + file: File, + prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType], + overrides: readonly IXmlifyedFile[] = [], + ): Promise { + const zip = this.compiler.compile(file, convertPrettifyType(prettify), overrides); const zipData = await zip.generateAsync({ type: "string", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", @@ -32,8 +36,12 @@ export class Packer { return zipData; } - public static async toBuffer(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Promise { - const zip = this.compiler.compile(file, convertPrettifyType(prettify)); + public static async toBuffer( + file: File, + prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType], + overrides: readonly IXmlifyedFile[] = [], + ): Promise { + const zip = this.compiler.compile(file, convertPrettifyType(prettify), overrides); const zipData = await zip.generateAsync({ type: "nodebuffer", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", @@ -43,8 +51,12 @@ export class Packer { return zipData; } - public static async toBase64String(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Promise { - const zip = this.compiler.compile(file, convertPrettifyType(prettify)); + public static async toBase64String( + file: File, + prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType], + overrides: readonly IXmlifyedFile[] = [], + ): Promise { + const zip = this.compiler.compile(file, convertPrettifyType(prettify), overrides); const zipData = await zip.generateAsync({ type: "base64", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", @@ -54,8 +66,12 @@ export class Packer { return zipData; } - public static async toBlob(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Promise { - const zip = this.compiler.compile(file, convertPrettifyType(prettify)); + public static async toBlob( + file: File, + prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType], + overrides: readonly IXmlifyedFile[] = [], + ): Promise { + const zip = this.compiler.compile(file, convertPrettifyType(prettify), overrides); const zipData = await zip.generateAsync({ type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", @@ -65,9 +81,13 @@ export class Packer { return zipData; } - public static toStream(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Stream { + public static toStream( + file: File, + prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType], + overrides: readonly IXmlifyedFile[] = [], + ): Stream { const stream = new Stream(); - const zip = this.compiler.compile(file, convertPrettifyType(prettify)); + const zip = this.compiler.compile(file, convertPrettifyType(prettify), overrides); zip.generateAsync({ type: "nodebuffer", From c2560c973f7346c8214046bd9eed28120d2dccb9 Mon Sep 17 00:00:00 2001 From: Adam Sherwood Date: Wed, 22 Jan 2025 10:15:57 +0000 Subject: [PATCH 2/3] Adds tests for overrides parameter --- src/export/packer/next-compiler.spec.ts | 35 +++++++++++++++++++++++++ src/export/packer/packer.spec.ts | 31 +++++++++++++++++++--- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/export/packer/next-compiler.spec.ts b/src/export/packer/next-compiler.spec.ts index e0c7197df7..8db6bc14f6 100644 --- a/src/export/packer/next-compiler.spec.ts +++ b/src/export/packer/next-compiler.spec.ts @@ -112,6 +112,41 @@ describe("Compiler", () => { }, ); + it( + "should pack subfile overrides", + async () => { + const file = new File({ + sections: [], + comments: { + children: [], + }, + }); + const subfileData1 = "comments"; + const subfileData2 = "commentsExtended"; + const overrides = [ + { path: "word/comments.xml", data: subfileData1 }, + { path: "word/commentsExtended.xml", data: subfileData2 }, + ]; + const zipFile = compiler.compile(file, "", overrides); + const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); + + expect(fileNames).is.an.instanceof(Array); + expect(fileNames).has.length(20); + + expect(fileNames).to.include("word/comments.xml"); + expect(fileNames).to.include("word/commentsExtended.xml"); + + const commentsText = await zipFile.file("word/comments.xml")?.async("text"); + const commentsExtendedText = await zipFile.file("word/commentsExtended.xml")?.async("text"); + + expect(commentsText).toBe(subfileData1); + expect(commentsExtendedText).toBe(subfileData2); + }, + { + timeout: 99999999, + }, + ); + it("should call the format method X times equalling X files to be formatted", () => { // This test is required because before, there was a case where Document was formatted twice, which was inefficient // This also caused issues such as running prepForXml multiple times as format() was ran multiple times. diff --git a/src/export/packer/packer.spec.ts b/src/export/packer/packer.spec.ts index 344d3d35f2..7a15a6b1d4 100644 --- a/src/export/packer/packer.spec.ts +++ b/src/export/packer/packer.spec.ts @@ -46,7 +46,7 @@ describe("Packer", () => { await Packer.toString(file, true); - expect(spy).toBeCalledWith(expect.anything(), PrettifyType.WITH_2_BLANKS); + expect(spy).toBeCalledWith(expect.anything(), PrettifyType.WITH_2_BLANKS, expect.anything()); }); it("should use a prettify value", async () => { @@ -55,7 +55,7 @@ describe("Packer", () => { await Packer.toString(file, PrettifyType.WITH_4_BLANKS); - expect(spy).toBeCalledWith(expect.anything(), PrettifyType.WITH_4_BLANKS); + expect(spy).toBeCalledWith(expect.anything(), PrettifyType.WITH_4_BLANKS, expect.anything()); }); it("should use an undefined prettify value", async () => { @@ -64,7 +64,32 @@ describe("Packer", () => { await Packer.toString(file, false); - expect(spy).toBeCalledWith(expect.anything(), undefined); + expect(spy).toBeCalledWith(expect.anything(), undefined, expect.anything()); + }); + }); + + describe("overrides", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should use an overrides value", async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const spy = vi.spyOn((Packer as any).compiler, "compile"); + const overrides = [{ path: "word/comments.xml", data: "comments" }]; + + await Packer.toString(file, true, overrides); + + expect(spy).toBeCalledWith(expect.anything(), expect.anything(), overrides); + }); + + it("should use a default overrides value", async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const spy = vi.spyOn((Packer as any).compiler, "compile"); + + await Packer.toString(file); + + expect(spy).toBeCalledWith(expect.anything(), undefined, []); }); }); From e53209f1a067379d53c47cd8378c1be787bb967a Mon Sep 17 00:00:00 2001 From: Adam Sherwood Date: Wed, 22 Jan 2025 10:40:27 +0000 Subject: [PATCH 3/3] Update Packer usage examples --- docs/usage/packers.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/usage/packers.md b/docs/usage/packers.md index 236494bf96..4b281fb4a5 100644 --- a/docs/usage/packers.md +++ b/docs/usage/packers.md @@ -2,7 +2,7 @@ > Packers are the way in which `docx` turns your code into `.docx` format. It is completely decoupled from the `docx.Document`. -Packers works in both a node and browser environment (Angular etc). Now, the packer returns a `Buffer`, `Blob` or `base64 string`. It is up to you to take that and persist it with node's `fs`, send it down as a downloadable file, or anything else you wish. As of `version 4+`, this library will not have options to export to PDF. +Packers works in both a node and browser environment (Angular etc). Now, the packer returns a `Buffer`, `Blob`, `string`, `base64 string`, or `Stream`. It is up to you to take that and persist it with node's `fs`, send it down as a downloadable file, or anything else you wish. As of `version 4+`, this library will not have options to export to PDF. ### Export as Buffer @@ -14,6 +14,14 @@ Packer.toBuffer(doc).then((buffer) => { }); ``` +### Export as string + +```ts +Packer.toString(doc).then((string) => { + console.log(string); +}); +``` + ### Export as a `base64` string ```ts @@ -32,3 +40,26 @@ Packer.toBlob(doc).then((blob) => { saveAs(blob, "example.docx"); }); ``` + +### Export as a Stream + +```ts +Packer.toStream(doc).then((stream) => { + // read from stream +}); +``` + +### Export using optional arguments + +The `Packer` methods support 2 optional arguments. + +The first is for controlling the indentation of the xml and should be a `boolean` or `keyof typeof PrettifyType`. + +The second is an array of subfile overrides (`{path: string, data: string}[]`). These overrides can be used to write additional subfiles to the result or even override default subfiles in the case that the default handling of these subfiles does not meet your needs. + +```ts +const overrides = [{ path: "word/commentsExtended.xml", data: "string_data" }]; +Packer.toString(doc, true, overrides).then((string) => { + console.log(string); +}); +```