diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index b36e6be4c3..f015dbe7cf 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -16,31 +16,31 @@ import { patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { patches: { - "name":{ + name: { type: PatchType.PARAGRAPH, children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")], }, - "table_heading_1": { + table_heading_1: { type: PatchType.PARAGRAPH, children: [new TextRun("Heading wow!")], }, - "item_1": { + item_1: { type: PatchType.PARAGRAPH, children: [new TextRun("#657")], }, - "paragraph_replace": { + paragraph_replace: { type: PatchType.DOCUMENT, children: [new Paragraph("Lorem ipsum paragraph"), new Paragraph("Another paragraph")], }, - "header_adjective": { + header_adjective: { type: PatchType.PARAGRAPH, children: [new TextRun("Delightful Header")], }, - "footer_text": { + footer_text: { type: PatchType.PARAGRAPH, children: [new TextRun("replaced just as well")], }, - "table": { + table: { type: PatchType.DOCUMENT, children: [ new Table({ @@ -102,7 +102,6 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { ], }), ], - }), ], }, diff --git a/demo/86-generate-template.ts b/demo/86-generate-template.ts index 4af0a2dfd8..e16060f6a6 100644 --- a/demo/86-generate-template.ts +++ b/demo/86-generate-template.ts @@ -8,7 +8,7 @@ const doc = new Document({ { children: [ new Paragraph({ - children: [new TextRun("{{ template }}")], + children: [new TextRun("{{template}}")], }), ], }, diff --git a/demo/assets/generated-template.docx b/demo/assets/generated-template.docx new file mode 100644 index 0000000000..f7244e0c4b Binary files /dev/null and b/demo/assets/generated-template.docx differ diff --git a/src/patcher/from-docx.ts b/src/patcher/from-docx.ts index 0dd38f91c3..535336e70d 100644 --- a/src/patcher/from-docx.ts +++ b/src/patcher/from-docx.ts @@ -43,6 +43,7 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO for (const [patchKey, patchValue] of Object.entries(options.patches)) { const patchText = `{{${patchKey}}}`; const renderedParagraphs = findLocationOfText(json, patchText); + // TODO: mutates json. Make it immutable replacer(json, patchValue, patchText, renderedParagraphs); } } diff --git a/src/patcher/paragraph-split-inject.ts b/src/patcher/paragraph-split-inject.ts index 23a68159c8..d3fe8d5e6c 100644 --- a/src/patcher/paragraph-split-inject.ts +++ b/src/patcher/paragraph-split-inject.ts @@ -1,5 +1,5 @@ import { Element } from "xml-js"; -import { createTextElementContents } from "./util"; +import { createTextElementContents, patchSpaceAttribute } from "./util"; export const findRunElementIndexWithToken = (paragraphElement: Element, token: string): number => { for (let i = 0; i < (paragraphElement.elements ?? []).length; i++) { @@ -29,9 +29,7 @@ export const splitRunElement = (runElement: Element, token: string): { readonly const splitText = text.split(token); const newElements = splitText.map((t) => ({ ...e, - attributes: { - "xml:space": "preserve", - }, + ...patchSpaceAttribute(e), elements: createTextElementContents(t), })); splitIndex = i; diff --git a/src/patcher/paragraph-token-replacer.ts b/src/patcher/paragraph-token-replacer.ts index 45fb2bc302..06593c625f 100644 --- a/src/patcher/paragraph-token-replacer.ts +++ b/src/patcher/paragraph-token-replacer.ts @@ -1,6 +1,6 @@ import { Element } from "xml-js"; -import { createTextElementContents } from "./util"; +import { createTextElementContents, patchSpaceAttribute } from "./util"; import { IRenderedParagraphNode } from "./run-renderer"; enum ReplaceMode { @@ -43,6 +43,11 @@ export const replaceTokenInParagraphElement = ({ if (endIndex <= end) { const lastPart = text.substring(endIndex - start + 1); patchTextElement(paragraphElement.elements![run.index].elements![index], lastPart); + const currentElement = paragraphElement.elements![run.index].elements![index]; + // We need to add xml:space="preserve" to the last element to preserve the whitespace + // Otherwise, the text will be merged with the next element + // eslint-disable-next-line functional/immutable-data + paragraphElement.elements![run.index].elements![index] = patchSpaceAttribute(currentElement); replaceMode = ReplaceMode.END; } else { patchTextElement(paragraphElement.elements![run.index].elements![index], ""); diff --git a/src/patcher/replacer.ts b/src/patcher/replacer.ts index ce6e987d62..488b59760d 100644 --- a/src/patcher/replacer.ts +++ b/src/patcher/replacer.ts @@ -14,7 +14,12 @@ const formatter = new Formatter(); const SPLIT_TOKEN = "ɵ"; -export const replacer = (json: Element, patch: IPatch, patchText: string, renderedParagraphs: readonly IRenderedParagraphNode[]): Element => { +export const replacer = ( + json: Element, + patch: IPatch, + patchText: string, + renderedParagraphs: readonly IRenderedParagraphNode[], +): Element => { for (const renderedParagraph of renderedParagraphs) { const textJson = patch.children.map((c) => toJson(xml(formatter.format(c as XmlComponent)))).map((c) => c.elements![0]); @@ -24,7 +29,6 @@ export const replacer = (json: Element, patch: IPatch, patchText: string, render // eslint-disable-next-line functional/immutable-data, prefer-destructuring parentElement.elements?.splice(elementIndex, 1, ...textJson); } else if (patch.type === PatchType.PARAGRAPH) { - // Hard case where the text is only part of the paragraph const paragraphElement = goToElementFromPath(json, renderedParagraph.path); replaceTokenInParagraphElement({ diff --git a/src/patcher/traverser.ts b/src/patcher/traverser.ts index 95f7829042..5ee2f792ff 100644 --- a/src/patcher/traverser.ts +++ b/src/patcher/traverser.ts @@ -8,17 +8,6 @@ export interface ElementWrapper { readonly parent: ElementWrapper | undefined; } -export interface ILocationOfText { - readonly parent: Element; - readonly startIndex: number; - readonly endIndex: number; - readonly currentText: string; - // This is optional because the text could start in the middle of a tag - readonly startElement?: Element; - // This is optional because the text could end in the middle of a tag - readonly endElement?: Element; -} - const elementsToWrapper = (wrapper: ElementWrapper): readonly ElementWrapper[] => wrapper.element.elements?.map((e, i) => ({ element: e, @@ -56,7 +45,5 @@ export const findLocationOfText = (node: Element, text: string): readonly IRende } } - const filteredParagraphs = renderedParagraphs.filter((p) => p.text.includes(text)); - - return filteredParagraphs; + return renderedParagraphs.filter((p) => p.text.includes(text)); }; diff --git a/src/patcher/util.ts b/src/patcher/util.ts index f385a00025..bdb79eb0d0 100644 --- a/src/patcher/util.ts +++ b/src/patcher/util.ts @@ -17,3 +17,10 @@ export const createTextElementContents = (text: string): Element[] => { return textJson.elements![0].elements ?? []; }; + +export const patchSpaceAttribute = (element: Element): Element => ({ + ...element, + attributes: { + "xml:space": "preserve", + }, +});