diff --git a/demo/96-template-document.ts b/demo/96-template-document.ts new file mode 100644 index 0000000000..949712e898 --- /dev/null +++ b/demo/96-template-document.ts @@ -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); +}); diff --git a/demo/assets/simple-template-4.docx b/demo/assets/simple-template-4.docx new file mode 100644 index 0000000000..a15d462ed7 Binary files /dev/null and b/demo/assets/simple-template-4.docx differ diff --git a/src/patcher/from-docx.spec.ts b/src/patcher/from-docx.spec.ts index 8eee52260c..908c4a94e5 100644 --- a/src/patcher/from-docx.spec.ts +++ b/src/patcher/from-docx.spec.ts @@ -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", () => { diff --git a/src/patcher/from-docx.ts b/src/patcher/from-docx.ts index 4d42951534..a85bcd06dd 100644 --- a/src/patcher/from-docx.ts +++ b/src/patcher/from-docx.ts @@ -55,6 +55,10 @@ export type PatchDocumentOptions>; readonly keepOriginalStyles?: boolean; + readonly placeholderDelimiters?: Readonly<{ + readonly start: string; + readonly end: string; + }>; }; const imageReplacer = new ImageReplacer(); @@ -64,6 +68,7 @@ export const patchDocument = async ): Promise => { const zipContent = await JSZip.loadAsync(data); const contexts = new Map(); @@ -132,8 +137,14 @@ export const patchDocument = async