Skip to content

Commit d1856d0

Browse files
committed
Merge branch 'numbered-items' of github.com:dolanmiu/docx into numbered-items
2 parents fea3514 + 3ab02c2 commit d1856d0

File tree

4 files changed

+275
-1
lines changed

4 files changed

+275
-1
lines changed

demo/96-template-document.ts

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Patch a document with patches
2+
3+
import * as fs from "fs";
4+
import {
5+
ExternalHyperlink,
6+
HeadingLevel,
7+
ImageRun,
8+
Paragraph,
9+
patchDocument,
10+
PatchType,
11+
Table,
12+
TableCell,
13+
TableRow,
14+
TextDirection,
15+
TextRun,
16+
VerticalAlign,
17+
} from "docx";
18+
19+
patchDocument({
20+
outputType: "nodebuffer",
21+
data: fs.readFileSync("demo/assets/simple-template-4.docx"),
22+
patches: {
23+
name: {
24+
type: PatchType.PARAGRAPH,
25+
children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")],
26+
},
27+
table_heading_1: {
28+
type: PatchType.PARAGRAPH,
29+
children: [new TextRun("Heading wow!")],
30+
},
31+
item_1: {
32+
type: PatchType.PARAGRAPH,
33+
children: [
34+
new TextRun("#657"),
35+
new ExternalHyperlink({
36+
children: [
37+
new TextRun({
38+
text: "BBC News Link",
39+
}),
40+
],
41+
link: "https://www.bbc.co.uk/news",
42+
}),
43+
],
44+
},
45+
paragraph_replace: {
46+
type: PatchType.DOCUMENT,
47+
children: [
48+
new Paragraph("Lorem ipsum paragraph"),
49+
new Paragraph("Another paragraph"),
50+
new Paragraph({
51+
children: [
52+
new TextRun("This is a "),
53+
new ExternalHyperlink({
54+
children: [
55+
new TextRun({
56+
text: "Google Link",
57+
}),
58+
],
59+
link: "https://www.google.co.uk",
60+
}),
61+
new ImageRun({
62+
type: "png",
63+
data: fs.readFileSync("./demo/images/dog.png"),
64+
transformation: { width: 100, height: 100 },
65+
}),
66+
],
67+
}),
68+
],
69+
},
70+
header_adjective: {
71+
type: PatchType.PARAGRAPH,
72+
children: [new TextRun("Delightful Header")],
73+
},
74+
footer_text: {
75+
type: PatchType.PARAGRAPH,
76+
children: [
77+
new TextRun("replaced just as"),
78+
new TextRun(" well"),
79+
new ExternalHyperlink({
80+
children: [
81+
new TextRun({
82+
text: "BBC News Link",
83+
}),
84+
],
85+
link: "https://www.bbc.co.uk/news",
86+
}),
87+
],
88+
},
89+
image_test: {
90+
type: PatchType.PARAGRAPH,
91+
children: [
92+
new ImageRun({
93+
type: "jpg",
94+
data: fs.readFileSync("./demo/images/image1.jpeg"),
95+
transformation: { width: 100, height: 100 },
96+
}),
97+
],
98+
},
99+
table: {
100+
type: PatchType.DOCUMENT,
101+
children: [
102+
new Table({
103+
rows: [
104+
new TableRow({
105+
children: [
106+
new TableCell({
107+
children: [new Paragraph({}), new Paragraph({})],
108+
verticalAlign: VerticalAlign.CENTER,
109+
}),
110+
new TableCell({
111+
children: [new Paragraph({}), new Paragraph({})],
112+
verticalAlign: VerticalAlign.CENTER,
113+
}),
114+
new TableCell({
115+
children: [new Paragraph({ text: "bottom to top" }), new Paragraph({})],
116+
textDirection: TextDirection.BOTTOM_TO_TOP_LEFT_TO_RIGHT,
117+
}),
118+
new TableCell({
119+
children: [new Paragraph({ text: "top to bottom" }), new Paragraph({})],
120+
textDirection: TextDirection.TOP_TO_BOTTOM_RIGHT_TO_LEFT,
121+
}),
122+
],
123+
}),
124+
new TableRow({
125+
children: [
126+
new TableCell({
127+
children: [
128+
new Paragraph({
129+
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",
130+
heading: HeadingLevel.HEADING_1,
131+
}),
132+
],
133+
}),
134+
new TableCell({
135+
children: [
136+
new Paragraph({
137+
text: "This text should be in the middle of the cell",
138+
}),
139+
],
140+
verticalAlign: VerticalAlign.CENTER,
141+
}),
142+
new TableCell({
143+
children: [
144+
new Paragraph({
145+
text: "Text above should be vertical from bottom to top",
146+
}),
147+
],
148+
verticalAlign: VerticalAlign.CENTER,
149+
}),
150+
new TableCell({
151+
children: [
152+
new Paragraph({
153+
text: "Text above should be vertical from top to bottom",
154+
}),
155+
],
156+
verticalAlign: VerticalAlign.CENTER,
157+
}),
158+
],
159+
}),
160+
],
161+
}),
162+
],
163+
},
164+
},
165+
placeholderDelimiters: { start: "<<", end: ">>" },
166+
}).then((doc) => {
167+
fs.writeFileSync("My Document.docx", doc);
168+
});

demo/assets/simple-template-4.docx

15.4 KB
Binary file not shown.

src/patcher/from-docx.spec.ts

+95
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,101 @@ describe("from-docx", () => {
288288
});
289289
expect(output).to.not.be.undefined;
290290
});
291+
292+
it("should patch the document", async () => {
293+
const output = await patchDocument({
294+
outputType: "uint8array",
295+
data: Buffer.from(""),
296+
placeholderDelimiters: { start: "{{", end: "}}" },
297+
patches: {
298+
name: {
299+
type: PatchType.PARAGRAPH,
300+
children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")],
301+
},
302+
item_1: {
303+
type: PatchType.PARAGRAPH,
304+
children: [
305+
new TextRun("#657"),
306+
new ExternalHyperlink({
307+
children: [
308+
new TextRun({
309+
text: "BBC News Link",
310+
}),
311+
],
312+
link: "https://www.bbc.co.uk/news",
313+
}),
314+
],
315+
},
316+
// eslint-disable-next-line @typescript-eslint/naming-convention
317+
paragraph_replace: {
318+
type: PatchType.DOCUMENT,
319+
children: [
320+
new Paragraph({
321+
children: [
322+
new TextRun("This is a "),
323+
new ExternalHyperlink({
324+
children: [
325+
new TextRun({
326+
text: "Google Link",
327+
}),
328+
],
329+
link: "https://www.google.co.uk",
330+
}),
331+
new ImageRun({
332+
type: "png",
333+
data: Buffer.from(""),
334+
transformation: { width: 100, height: 100 },
335+
}),
336+
],
337+
}),
338+
],
339+
},
340+
// eslint-disable-next-line @typescript-eslint/naming-convention
341+
image_test: {
342+
type: PatchType.PARAGRAPH,
343+
children: [
344+
new ImageRun({
345+
type: "png",
346+
data: Buffer.from(""),
347+
transformation: { width: 100, height: 100 },
348+
}),
349+
],
350+
},
351+
},
352+
});
353+
expect(output).to.not.be.undefined;
354+
});
355+
356+
it("should patch the document", async () => {
357+
const output = await patchDocument({
358+
outputType: "uint8array",
359+
data: Buffer.from(""),
360+
patches: {},
361+
});
362+
expect(output).to.not.be.undefined;
363+
});
364+
365+
it("throws error with empty delimiters", async () => {
366+
await expect(() =>
367+
patchDocument({
368+
outputType: "uint8array",
369+
data: Buffer.from(""),
370+
patches: {},
371+
placeholderDelimiters: { start: "", end: "" },
372+
}),
373+
).rejects.toThrow();
374+
});
375+
376+
it("throws error with whitespace-only delimiters", async () => {
377+
await expect(() =>
378+
patchDocument({
379+
outputType: "uint8array",
380+
data: Buffer.from(""),
381+
patches: {},
382+
placeholderDelimiters: { start: " ", end: " " },
383+
}),
384+
).rejects.toThrowError();
385+
});
291386
});
292387

293388
describe("document.xml and [Content_Types].xml with relationships", () => {

src/patcher/from-docx.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export type PatchDocumentOptions<T extends PatchDocumentOutputType = PatchDocume
5555
readonly data: InputDataType;
5656
readonly patches: Readonly<Record<string, IPatch>>;
5757
readonly keepOriginalStyles?: boolean;
58+
readonly placeholderDelimiters?: Readonly<{
59+
readonly start: string;
60+
readonly end: string;
61+
}>;
5862
};
5963

6064
const imageReplacer = new ImageReplacer();
@@ -64,6 +68,7 @@ export const patchDocument = async <T extends PatchDocumentOutputType = PatchDoc
6468
data,
6569
patches,
6670
keepOriginalStyles,
71+
placeholderDelimiters = { start: "{{", end: "}}" } as const,
6772
}: PatchDocumentOptions<T>): Promise<OutputByType[T]> => {
6873
const zipContent = await JSZip.loadAsync(data);
6974
const contexts = new Map<string, IContext>();
@@ -132,8 +137,14 @@ export const patchDocument = async <T extends PatchDocumentOutputType = PatchDoc
132137
};
133138
contexts.set(key, context);
134139

140+
if (!placeholderDelimiters?.start.trim() || !placeholderDelimiters?.end.trim()) {
141+
throw new Error("Both start and end delimiters must be non-empty strings.");
142+
}
143+
144+
const { start, end } = placeholderDelimiters;
145+
135146
for (const [patchKey, patchValue] of Object.entries(patches)) {
136-
const patchText = `{{${patchKey}}}`;
147+
const patchText = `${start}${patchKey}${end}`;
137148
// TODO: mutates json. Make it immutable
138149
// We need to loop through to catch every occurrence of the patch text
139150
// It is possible that the patch text is in the same run

0 commit comments

Comments
 (0)