Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/multiple patch document exports #2497

Merged
merged 6 commits into from
Dec 31, 2023
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
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