Skip to content
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
168 changes: 168 additions & 0 deletions demo/96-template-document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Patch a document with patches

import * as fs from "fs";
import {
ExternalHyperlink,
HeadingLevel,
ImageRun,
Paragraph,
patchDocument,
PatchType,
Table,
TableCell,
TableRow,
TextDirection,
TextRun,
VerticalAlign,
} from "docx";

patchDocument({
outputType: "nodebuffer",
data: fs.readFileSync("demo/assets/simple-template-4.docx"),
patches: {
name: {
type: PatchType.PARAGRAPH,
children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")],
},
table_heading_1: {
type: PatchType.PARAGRAPH,
children: [new TextRun("Heading wow!")],
},
item_1: {
type: PatchType.PARAGRAPH,
children: [
new TextRun("#657"),
new ExternalHyperlink({
children: [
new TextRun({
text: "BBC News Link",
}),
],
link: "https://www.bbc.co.uk/news",
}),
],
},
paragraph_replace: {
type: PatchType.DOCUMENT,
children: [
new Paragraph("Lorem ipsum paragraph"),
new Paragraph("Another paragraph"),
new Paragraph({
children: [
new TextRun("This is a "),
new ExternalHyperlink({
children: [
new TextRun({
text: "Google Link",
}),
],
link: "https://www.google.co.uk",
}),
new ImageRun({
type: "png",
data: fs.readFileSync("./demo/images/dog.png"),
transformation: { width: 100, height: 100 },
}),
],
}),
],
},
header_adjective: {
type: PatchType.PARAGRAPH,
children: [new TextRun("Delightful Header")],
},
footer_text: {
type: PatchType.PARAGRAPH,
children: [
new TextRun("replaced just as"),
new TextRun(" well"),
new ExternalHyperlink({
children: [
new TextRun({
text: "BBC News Link",
}),
],
link: "https://www.bbc.co.uk/news",
}),
],
},
image_test: {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
type: "jpg",
data: fs.readFileSync("./demo/images/image1.jpeg"),
transformation: { width: 100, height: 100 },
}),
],
},
table: {
type: PatchType.DOCUMENT,
children: [
new Table({
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph({}), new Paragraph({})],
verticalAlign: VerticalAlign.CENTER,
}),
new TableCell({
children: [new Paragraph({}), new Paragraph({})],
verticalAlign: VerticalAlign.CENTER,
}),
new TableCell({
children: [new Paragraph({ text: "bottom to top" }), new Paragraph({})],
textDirection: TextDirection.BOTTOM_TO_TOP_LEFT_TO_RIGHT,
}),
new TableCell({
children: [new Paragraph({ text: "top to bottom" }), new Paragraph({})],
textDirection: TextDirection.TOP_TO_BOTTOM_RIGHT_TO_LEFT,
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [
new Paragraph({
text: "Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah",
heading: HeadingLevel.HEADING_1,
}),
],
}),
new TableCell({
children: [
new Paragraph({
text: "This text should be in the middle of the cell",
}),
],
verticalAlign: VerticalAlign.CENTER,
}),
new TableCell({
children: [
new Paragraph({
text: "Text above should be vertical from bottom to top",
}),
],
verticalAlign: VerticalAlign.CENTER,
}),
new TableCell({
children: [
new Paragraph({
text: "Text above should be vertical from top to bottom",
}),
],
verticalAlign: VerticalAlign.CENTER,
}),
],
}),
],
}),
],
},
},
placeholderDelimiters: { start: "<<", end: ">>" },
}).then((doc) => {
fs.writeFileSync("My Document.docx", doc);
});
Binary file added demo/assets/simple-template-4.docx
Binary file not shown.
95 changes: 95 additions & 0 deletions src/patcher/from-docx.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,101 @@ describe("from-docx", () => {
});
expect(output).to.not.be.undefined;
});

it("should patch the document", async () => {
const output = await patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
placeholderDelimiters: { start: "{{", end: "}}" },
patches: {
name: {
type: PatchType.PARAGRAPH,
children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")],
},
item_1: {
type: PatchType.PARAGRAPH,
children: [
new TextRun("#657"),
new ExternalHyperlink({
children: [
new TextRun({
text: "BBC News Link",
}),
],
link: "https://www.bbc.co.uk/news",
}),
],
},
// eslint-disable-next-line @typescript-eslint/naming-convention
paragraph_replace: {
type: PatchType.DOCUMENT,
children: [
new Paragraph({
children: [
new TextRun("This is a "),
new ExternalHyperlink({
children: [
new TextRun({
text: "Google Link",
}),
],
link: "https://www.google.co.uk",
}),
new ImageRun({
type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
],
}),
],
},
// eslint-disable-next-line @typescript-eslint/naming-convention
image_test: {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
],
},
},
});
expect(output).to.not.be.undefined;
});

it("should patch the document", async () => {
const output = await patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
patches: {},
});
expect(output).to.not.be.undefined;
});

it("throws error with empty delimiters", async () => {
await expect(() =>
patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
patches: {},
placeholderDelimiters: { start: "", end: "" },
}),
).rejects.toThrow();
});

it("throws error with whitespace-only delimiters", async () => {
await expect(() =>
patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
patches: {},
placeholderDelimiters: { start: " ", end: " " },
}),
).rejects.toThrowError();
});
});

describe("document.xml and [Content_Types].xml with relationships", () => {
Expand Down
13 changes: 12 additions & 1 deletion src/patcher/from-docx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export type PatchDocumentOptions<T extends PatchDocumentOutputType = PatchDocume
readonly data: InputDataType;
readonly patches: Readonly<Record<string, IPatch>>;
readonly keepOriginalStyles?: boolean;
readonly placeholderDelimiters?: Readonly<{
readonly start: string;
readonly end: string;
}>;
};

const imageReplacer = new ImageReplacer();
Expand All @@ -64,6 +68,7 @@ export const patchDocument = async <T extends PatchDocumentOutputType = PatchDoc
data,
patches,
keepOriginalStyles,
placeholderDelimiters = { start: "{{", end: "}}" } as const,
}: PatchDocumentOptions<T>): Promise<OutputByType[T]> => {
const zipContent = await JSZip.loadAsync(data);
const contexts = new Map<string, IContext>();
Expand Down Expand Up @@ -132,8 +137,14 @@ export const patchDocument = async <T extends PatchDocumentOutputType = PatchDoc
};
contexts.set(key, context);

if (!placeholderDelimiters?.start.trim() || !placeholderDelimiters?.end.trim()) {
throw new Error("Both start and end delimiters must be non-empty strings.");
}

const { start, end } = placeholderDelimiters;

for (const [patchKey, patchValue] of Object.entries(patches)) {
const patchText = `{{${patchKey}}}`;
const patchText = `${start}${patchKey}${end}`;
// TODO: mutates json. Make it immutable
// 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
Expand Down
Loading