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); +}); +``` 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/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.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, []); }); }); 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",