diff --git a/demo/93-template-document.ts b/demo/93-template-document.ts
new file mode 100644
index 00000000000..7f5263004de
--- /dev/null
+++ b/demo/93-template-document.ts
@@ -0,0 +1,72 @@
+// Patch a document with patches
+
+import * as fs from "fs";
+import { patchDocument, PatchType, TextRun } from "docx";
+
+patchDocument({
+ outputType: "nodebuffer",
+ data: fs.readFileSync("demo/assets/field-trip.docx"),
+ patches: {
+ todays_date: {
+ type: PatchType.PARAGRAPH,
+ children: [new TextRun({ text: new Date().toLocaleDateString() })],
+ },
+
+ school_name: {
+ type: PatchType.PARAGRAPH,
+ children: [new TextRun({ text: "test" })],
+ },
+
+ address: {
+ type: PatchType.PARAGRAPH,
+ children: [new TextRun({ text: "blah blah" })],
+ },
+
+ city: {
+ type: PatchType.PARAGRAPH,
+ children: [new TextRun({ text: "test" })],
+ },
+
+ state: {
+ type: PatchType.PARAGRAPH,
+ children: [new TextRun({ text: "test" })],
+ },
+
+ zip: {
+ type: PatchType.PARAGRAPH,
+ children: [new TextRun({ text: "test" })],
+ },
+
+ phone: {
+ type: PatchType.PARAGRAPH,
+ children: [new TextRun({ text: "test" })],
+ },
+
+ first_name: {
+ type: PatchType.PARAGRAPH,
+ children: [new TextRun({ text: "test" })],
+ },
+
+ last_name: {
+ type: PatchType.PARAGRAPH,
+ children: [new TextRun({ text: "test" })],
+ },
+
+ email_address: {
+ type: PatchType.PARAGRAPH,
+ children: [new TextRun({ text: "test" })],
+ },
+
+ ft_dates: {
+ type: PatchType.PARAGRAPH,
+ children: [new TextRun({ text: "test" })],
+ },
+
+ grade: {
+ type: PatchType.PARAGRAPH,
+ children: [new TextRun({ text: "test" })],
+ },
+ },
+}).then((doc) => {
+ fs.writeFileSync("My Document.docx", doc);
+});
diff --git a/demo/assets/field-trip.docx b/demo/assets/field-trip.docx
new file mode 100644
index 00000000000..a7415962bbd
Binary files /dev/null and b/demo/assets/field-trip.docx differ
diff --git a/src/patcher/paragraph-split-inject.spec.ts b/src/patcher/paragraph-split-inject.spec.ts
index 6d58c2f0f12..7b30dd36ade 100644
--- a/src/patcher/paragraph-split-inject.spec.ts
+++ b/src/patcher/paragraph-split-inject.spec.ts
@@ -273,5 +273,65 @@ describe("paragraph-split-inject", () => {
},
});
});
+
+ it("should create an empty end element if it is at the end", () => {
+ const output = splitRunElement(
+ {
+ type: "element",
+ name: "w:r",
+ elements: [
+ {
+ type: "element",
+ name: "w:rPr",
+ elements: [
+ { type: "element", name: "w:rFonts", attributes: { "w:eastAsia": "Times New Roman" } },
+ { type: "element", name: "w:kern", attributes: { "w:val": "0" } },
+ { type: "element", name: "w:sz", attributes: { "w:val": "20" } },
+ {
+ type: "element",
+ name: "w:lang",
+ attributes: { "w:val": "en-US", "w:eastAsia": "en-US", "w:bidi": "ar-SA" },
+ },
+ ],
+ },
+ { type: "element", name: "w:t", elements: [], attributes: { "xml:space": "preserve" } },
+ { type: "element", name: "w:br" },
+ { type: "element", name: "w:t", elements: [{ type: "text", text: "ɵ" }] },
+ ],
+ },
+ "ɵ",
+ );
+
+ expect(output).to.deep.equal({
+ left: {
+ type: "element",
+ name: "w:r",
+ elements: [
+ {
+ type: "element",
+ name: "w:rPr",
+ elements: [
+ { type: "element", name: "w:rFonts", attributes: { "w:eastAsia": "Times New Roman" } },
+ { type: "element", name: "w:kern", attributes: { "w:val": "0" } },
+ { type: "element", name: "w:sz", attributes: { "w:val": "20" } },
+ {
+ type: "element",
+ name: "w:lang",
+ attributes: { "w:val": "en-US", "w:eastAsia": "en-US", "w:bidi": "ar-SA" },
+ },
+ ],
+ },
+ { type: "element", name: "w:t", elements: [], attributes: { "xml:space": "preserve" } },
+ { type: "element", name: "w:br" },
+ { type: "element", name: "w:t", elements: [], attributes: { "xml:space": "preserve" } },
+ ],
+ },
+ right: {
+ type: "element",
+ name: "w:r",
+ elements: [{ type: "element", name: "w:t", elements: [], attributes: { "xml:space": "preserve" } }],
+ },
+ });
+ });
});
});
diff --git a/src/patcher/paragraph-split-inject.ts b/src/patcher/paragraph-split-inject.ts
index d7485e4991e..008d47c7fc2 100644
--- a/src/patcher/paragraph-split-inject.ts
+++ b/src/patcher/paragraph-split-inject.ts
@@ -29,7 +29,7 @@ export const splitRunElement = (runElement: Element, token: string): { readonly
runElement.elements
?.map((e, i) => {
if (e.type === "element" && e.name === "w:t") {
- const text = (e.elements?.[0].text as string) ?? "";
+ const text = (e.elements?.[0]?.text as string) ?? "";
const splitText = text.split(token);
const newElements = splitText.map((t) => ({
...e,
diff --git a/src/patcher/patch-detector.spec.ts b/src/patcher/patch-detector.spec.ts
index 961502c11a0..dee2bb7ecc4 100644
--- a/src/patcher/patch-detector.spec.ts
+++ b/src/patcher/patch-detector.spec.ts
@@ -195,6 +195,147 @@ const MOCK_XML = `
`;
+// cspell:disable
+const MOCK_XML_2 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+
+
+
+
+
+
+
+
+ s
+
+
+
+
+
+
+
+
+ chool_
+
+
+
+
+
+
+
+
+ n
+
+
+
+
+
+
+
+
+ ame}}
+
+ {{
+
+
+
+
+
+
+
+
+ a
+
+
+
+
+
+
+
+
+ ddr
+
+
+
+
+
+
+
+
+ ess
+
+
+
+
+
+
+
+
+ }}
+
+ {{
+
+
+
+
+
+
+`;
+// cspell:enable
+
describe("patch-detector", () => {
describe("patchDetector", () => {
describe("document.xml and [Content_Types].xml", () => {
@@ -222,4 +363,31 @@ describe("patch-detector", () => {
});
});
});
+
+ describe("patchDetector", () => {
+ describe("document.xml and [Content_Types].xml", () => {
+ beforeEach(() => {
+ vi.spyOn(JSZip, "loadAsync").mockReturnValue(
+ new Promise((resolve) => {
+ const zip = new JSZip();
+
+ zip.file("word/document.xml", MOCK_XML_2);
+ zip.file("[Content_Types].xml", ``);
+ resolve(zip);
+ }),
+ );
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it("should patch the document", async () => {
+ const output = await patchDetector({
+ data: Buffer.from(""),
+ });
+ expect(output).toMatchObject(["school_name", "address"]);
+ });
+ });
+ });
});
diff --git a/src/patcher/replacer.spec.ts b/src/patcher/replacer.spec.ts
index 903ef0bd2b2..12fdb27dc7e 100644
--- a/src/patcher/replacer.spec.ts
+++ b/src/patcher/replacer.spec.ts
@@ -200,5 +200,454 @@ describe("replacer", () => {
expect(JSON.stringify(output)).to.contain("Lorem ipsum paragraph");
});
+
+ it("should replace", () => {
+ // cspell:disable
+ const output = replacer({
+ json: {
+ elements: [
+ {
+ type: "element",
+ name: "w:hdr",
+ elements: [
+ {
+ type: "element",
+ name: "w:p",
+ elements: [
+ {
+ type: "element",
+ name: "w:r",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rPr",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rFonts",
+ attributes: { "w:eastAsia": "Times New Roman" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:kern",
+ attributes: { "w:val": "0" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:sz",
+ attributes: { "w:val": "20" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:lang",
+ attributes: {
+ "w:val": "en-US",
+ "w:eastAsia": "en-US",
+ "w:bidi": "ar-SA",
+ },
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:t",
+ elements: [{ type: "text", text: "{{" }],
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:r",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rPr",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rFonts",
+ attributes: { "w:eastAsia": "Times New Roman" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:kern",
+ attributes: { "w:val": "0" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:sz",
+ attributes: { "w:val": "20" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:lang",
+ attributes: {
+ "w:val": "en-US",
+ "w:eastAsia": "en-US",
+ "w:bidi": "ar-SA",
+ },
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:t",
+ elements: [{ type: "text", text: "s" }],
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:r",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rPr",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rFonts",
+ attributes: { "w:eastAsia": "Times New Roman" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:kern",
+ attributes: { "w:val": "0" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:sz",
+ attributes: { "w:val": "20" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:lang",
+ attributes: {
+ "w:val": "en-US",
+ "w:eastAsia": "en-US",
+ "w:bidi": "ar-SA",
+ },
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:t",
+ elements: [{ type: "text", text: "chool_" }],
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:r",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rPr",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rFonts",
+ attributes: { "w:eastAsia": "Times New Roman" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:kern",
+ attributes: { "w:val": "0" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:sz",
+ attributes: { "w:val": "20" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:lang",
+ attributes: {
+ "w:val": "en-US",
+ "w:eastAsia": "en-US",
+ "w:bidi": "ar-SA",
+ },
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:t",
+ elements: [{ type: "text", text: "n" }],
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:r",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:t",
+ elements: [{ type: "text", text: "{{" }],
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:r",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rPr",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rFonts",
+ attributes: { "w:eastAsia": "Times New Roman" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:kern",
+ attributes: { "w:val": "0" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:sz",
+ attributes: { "w:val": "20" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:lang",
+ attributes: {
+ "w:val": "en-US",
+ "w:eastAsia": "en-US",
+ "w:bidi": "ar-SA",
+ },
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:t",
+ elements: [{ type: "text", text: "a" }],
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:r",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rPr",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rFonts",
+ attributes: { "w:eastAsia": "Times New Roman" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:kern",
+ attributes: { "w:val": "0" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:sz",
+ attributes: { "w:val": "20" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:lang",
+ attributes: {
+ "w:val": "en-US",
+ "w:eastAsia": "en-US",
+ "w:bidi": "ar-SA",
+ },
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:t",
+ elements: [{ type: "text", text: "ddr" }],
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:r",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rPr",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rFonts",
+ attributes: { "w:eastAsia": "Times New Roman" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:kern",
+ attributes: { "w:val": "0" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:sz",
+ attributes: { "w:val": "20" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:lang",
+ attributes: {
+ "w:val": "en-US",
+ "w:eastAsia": "en-US",
+ "w:bidi": "ar-SA",
+ },
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:t",
+ elements: [{ type: "text", text: "ess" }],
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:r",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rPr",
+ elements: [
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:rFonts",
+ attributes: { "w:eastAsia": "Times New Roman" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:kern",
+ attributes: { "w:val": "0" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:sz",
+ attributes: { "w:val": "20" },
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:lang",
+ attributes: {
+ "w:val": "en-US",
+ "w:eastAsia": "en-US",
+ "w:bidi": "ar-SA",
+ },
+ },
+ { type: "text", text: "\n " },
+ ],
+ },
+ { type: "text", text: "\n " },
+ {
+ type: "element",
+ name: "w:t",
+ elements: [{ type: "text", text: "}}" }],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ // cspell:enable
+ patch: {
+ type: PatchType.PARAGRAPH,
+ children: [new Paragraph("Lorem ipsum paragraph")],
+ },
+ patchText: "{{address}}",
+ context: {
+ file: {} as unknown as File,
+ viewWrapper: {
+ Relationships: {},
+ } as unknown as IViewWrapper,
+ stack: [],
+ },
+ });
+
+ expect(JSON.stringify(output)).to.contain("Lorem ipsum paragraph");
+ });
});
});
diff --git a/src/patcher/replacer.ts b/src/patcher/replacer.ts
index ab069457586..f9670c1fb02 100644
--- a/src/patcher/replacer.ts
+++ b/src/patcher/replacer.ts
@@ -67,7 +67,7 @@ export const replacer = ({
if (keepOriginalStyles) {
const runElementNonTextualElements = runElementToBeReplaced.elements!.filter(
- (e) => e.type === "element" && e.name !== "w:t",
+ (e) => e.type === "element" && e.name !== "w:t" && e.name !== "w:br",
);
newRunElements = textJson.map((e) => ({