Skip to content

Commit

Permalink
Feature/multiple patch document exports (#2497)
Browse files Browse the repository at this point in the history
* Turn patch document into options object

Add outputType to options

* Set keep styles to true by default

* Simplify method

* Rename variable

* #2267 Multiple patches of same key

* Remove path which won't be visited
  • Loading branch information
dolanmiu authored Dec 31, 2023
1 parent 24c159d commit 13cf3ee
Show file tree
Hide file tree
Showing 13 changed files with 309 additions and 220 deletions.
18 changes: 15 additions & 3 deletions demo/85-template-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
VerticalAlign,
} from "docx";

patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
patchDocument({
outputType: "nodebuffer",
data: fs.readFileSync("demo/assets/simple-template.docx"),
patches: {
name: {
type: PatchType.PARAGRAPH,
Expand Down Expand Up @@ -56,7 +58,11 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
],
link: "https://www.google.co.uk",
}),
new ImageRun({ data: fs.readFileSync("./demo/images/dog.png"), transformation: { width: 100, height: 100 } }),
new ImageRun({
type: "png",
data: fs.readFileSync("./demo/images/dog.png"),
transformation: { width: 100, height: 100 },
}),
],
}),
],
Expand All @@ -82,7 +88,13 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
},
image_test: {
type: PatchType.PARAGRAPH,
children: [new ImageRun({ data: fs.readFileSync("./demo/images/image1.jpeg"), transformation: { width: 100, height: 100 } })],
children: [
new ImageRun({
type: "jpg",
data: fs.readFileSync("./demo/images/image1.jpeg"),
transformation: { width: 100, height: 100 },
}),
],
},
table: {
type: PatchType.DOCUMENT,
Expand Down
4 changes: 3 additions & 1 deletion demo/87-template-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import * as fs from "fs";
import { patchDocument, PatchType, TextRun } from "docx";

patchDocument(fs.readFileSync("demo/assets/simple-template-2.docx"), {
patchDocument({
outputType: "nodebuffer",
data: fs.readFileSync("demo/assets/simple-template-2.docx"),
patches: {
name: {
type: PatchType.PARAGRAPH,
Expand Down
4 changes: 3 additions & 1 deletion demo/88-template-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ const patches = getPatches({
paragraph_replace: "Lorem ipsum paragraph",
});

patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
patchDocument({
outputType: "nodebuffer",
data: fs.readFileSync("demo/assets/simple-template.docx"),
patches,
}).then((doc) => {
fs.writeFileSync("My Document.docx", doc);
Expand Down
5 changes: 4 additions & 1 deletion demo/89-template-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ const patches = getPatches({
"first-name": "John",
});

patchDocument(fs.readFileSync("demo/assets/simple-template-3.docx"), {
patchDocument({
outputType: "nodebuffer",
data: fs.readFileSync("demo/assets/simple-template-3.docx"),
patches,
keepOriginalStyles: true,
}).then((doc) => {
fs.writeFileSync("My Document.docx", doc);
});
Binary file modified demo/assets/simple-template-3.docx
Binary file not shown.
20 changes: 15 additions & 5 deletions src/patcher/from-docx.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@ describe("from-docx", () => {
});

it("should patch the document", async () => {
const output = await patchDocument(Buffer.from(""), {
const output = await patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
patches: {
name: {
type: PatchType.PARAGRAPH,
Expand Down Expand Up @@ -279,7 +281,9 @@ describe("from-docx", () => {
});

it("should patch the document", async () => {
const output = await patchDocument(Buffer.from(""), {
const output = await patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
patches: {},
});
expect(output).to.not.be.undefined;
Expand All @@ -305,7 +309,9 @@ describe("from-docx", () => {
});

it("should use the relationships file rather than create one", async () => {
const output = await patchDocument(Buffer.from(""), {
const output = await patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
patches: {
// eslint-disable-next-line @typescript-eslint/naming-convention
image_test: {
Expand Down Expand Up @@ -350,7 +356,9 @@ describe("from-docx", () => {

it("should throw an error if the content types is not found", () =>
expect(
patchDocument(Buffer.from(""), {
patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
patches: {
// eslint-disable-next-line @typescript-eslint/naming-convention
image_test: {
Expand Down Expand Up @@ -388,7 +396,9 @@ describe("from-docx", () => {

it("should throw an error if the content types is not found", () =>
expect(
patchDocument(Buffer.from(""), {
patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
patches: {
// eslint-disable-next-line @typescript-eslint/naming-convention
image_test: {
Expand Down
103 changes: 68 additions & 35 deletions src/patcher/from-docx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { TargetModeType } from "@file/relationships/relationship/relationship";
import { uniqueId } from "@util/convenience-functions";

import { replacer } from "./replacer";
import { findLocationOfText } from "./traverser";
import { toJson } from "./util";
import { appendRelationship, getNextRelationshipIndex } from "./relationship-manager";
import { appendContentType } from "./content-types-manager";
Expand Down Expand Up @@ -47,14 +46,37 @@ interface IHyperlinkRelationshipAddition {

export type IPatch = ParagraphPatch | FilePatch;

export interface PatchDocumentOptions {
// From JSZip
type OutputByType = {
readonly base64: string;
// eslint-disable-next-line id-denylist
readonly string: string;
readonly text: string;
readonly binarystring: string;
readonly array: readonly number[];
readonly uint8array: Uint8Array;
readonly arraybuffer: ArrayBuffer;
readonly blob: Blob;
readonly nodebuffer: Buffer;
};

export type PatchDocumentOutputType = keyof OutputByType;

export type PatchDocumentOptions<T extends PatchDocumentOutputType = PatchDocumentOutputType> = {
readonly outputType: T;
readonly data: InputDataType;
readonly patches: { readonly [key: string]: IPatch };
readonly keepOriginalStyles?: boolean;
}
};

const imageReplacer = new ImageReplacer();

export const patchDocument = async (data: InputDataType, options: PatchDocumentOptions): Promise<Uint8Array> => {
export const patchDocument = async <T extends PatchDocumentOutputType = PatchDocumentOutputType>({
outputType,
data,
patches,
keepOriginalStyles,
}: PatchDocumentOptions<T>): Promise<OutputByType[T]> => {
const zipContent = await JSZip.loadAsync(data);
const contexts = new Map<string, IContext>();
const file = {
Expand Down Expand Up @@ -104,38 +126,48 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
};
contexts.set(key, context);

for (const [patchKey, patchValue] of Object.entries(options.patches)) {
for (const [patchKey, patchValue] of Object.entries(patches)) {
const patchText = `{{${patchKey}}}`;
const renderedParagraphs = findLocationOfText(json, patchText);
// TODO: mutates json. Make it immutable
replacer(
json,
{
...patchValue,
children: patchValue.children.map((element) => {
// We need to replace external hyperlinks with concrete hyperlinks
if (element instanceof ExternalHyperlink) {
const concreteHyperlink = new ConcreteHyperlink(element.options.children, uniqueId());
// eslint-disable-next-line functional/immutable-data
hyperlinkRelationshipAdditions.push({
key,
hyperlink: {
id: concreteHyperlink.linkId,
link: element.options.link,
},
});
return concreteHyperlink;
} else {
return element;
}
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
patchText,
renderedParagraphs,
context,
options.keepOriginalStyles,
);
// We need to loop through to catch every occurrence of the patch text
// It is possible that the patch text is in the same run
// This algorithm is limited to one patch per text run
// Once it cannot find any more occurrences, it will throw an error, and then we break out of the loop
// https://github.com/dolanmiu/docx/issues/2267
// eslint-disable-next-line no-constant-condition
while (true) {
try {
replacer({
json,
patch: {
...patchValue,
children: patchValue.children.map((element) => {
// We need to replace external hyperlinks with concrete hyperlinks
if (element instanceof ExternalHyperlink) {
const concreteHyperlink = new ConcreteHyperlink(element.options.children, uniqueId());
// eslint-disable-next-line functional/immutable-data
hyperlinkRelationshipAdditions.push({
key,
hyperlink: {
id: concreteHyperlink.linkId,
link: element.options.link,
},
});
return concreteHyperlink;
} else {
return element;
}
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
patchText,
context,
keepOriginalStyles,
});
} catch {
break;
}
}
}

const mediaDatas = imageReplacer.getMediaData(JSON.stringify(json), context.file.Media);
Expand Down Expand Up @@ -201,6 +233,7 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
appendContentType(contentTypesJson, "image/jpeg", "jpg");
appendContentType(contentTypesJson, "image/bmp", "bmp");
appendContentType(contentTypesJson, "image/gif", "gif");
appendContentType(contentTypesJson, "image/svg+xml", "svg");
}

const zip = new JSZip();
Expand All @@ -220,7 +253,7 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
}

return zip.generateAsync({
type: "uint8array",
type: outputType,
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
compression: "DEFLATE",
});
Expand Down
4 changes: 2 additions & 2 deletions src/patcher/paragraph-token-replacer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("paragraph-token-replacer", () => {
},
renderedParagraph: {
index: 0,
path: [0],
pathToParagraph: [0],
runs: [
{
end: 4,
Expand Down Expand Up @@ -128,7 +128,7 @@ describe("paragraph-token-replacer", () => {
{ text: "World", parts: [{ text: "World", index: 0, start: 15, end: 19 }], index: 3, start: 15, end: 19 },
],
index: 0,
path: [0, 1, 0, 0],
pathToParagraph: [0, 1, 0, 0],
},
originalText: "{{name}}",
replacementText: "John",
Expand Down
Loading

0 comments on commit 13cf3ee

Please sign in to comment.