diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index 5baa2397f4..4e7608009f 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -9,6 +9,18 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { children: [new TextRun("John Doe")], text: "{{ name }}", }, + { + children: [new TextRun("Heading wow!")], + text: "{{ table_heading_1 }}", + }, + { + children: [new TextRun("#657")], + text: "{{ item_1 }}", + }, + { + children: [new TextRun("Lorem ipsum paragraph")], + text: "{{ paragraph_replace }}", + }, ], }).then((doc) => { fs.writeFileSync("My Document.docx", doc); diff --git a/src/templater/paragraph-split-inject.ts b/src/templater/paragraph-split-inject.ts new file mode 100644 index 0000000000..23a68159c8 --- /dev/null +++ b/src/templater/paragraph-split-inject.ts @@ -0,0 +1,56 @@ +import { Element } from "xml-js"; +import { createTextElementContents } from "./util"; + +export const findRunElementIndexWithToken = (paragraphElement: Element, token: string): number => { + for (let i = 0; i < (paragraphElement.elements ?? []).length; i++) { + const element = paragraphElement.elements![i]; + if (element.type === "element" && element.name === "w:r") { + const textElement = (element.elements ?? []).filter((e) => e.type === "element" && e.name === "w:t"); + + for (const text of textElement) { + if ((text.elements?.[0].text as string)?.includes(token)) { + return i; + } + } + } + } + // return -1; + throw new Error("Token not found"); +}; + +export const splitRunElement = (runElement: Element, token: string): { readonly left: Element; readonly right: Element } => { + let splitIndex = 0; + + const splitElements = + runElement.elements + ?.map((e, i) => { + if (e.type === "element" && e.name === "w:t") { + const text = e.elements?.[0].text as string; + const splitText = text.split(token); + const newElements = splitText.map((t) => ({ + ...e, + attributes: { + "xml:space": "preserve", + }, + elements: createTextElementContents(t), + })); + splitIndex = i; + return newElements; + } else { + return e; + } + }) + .flat() ?? []; + + const leftRunElement: Element = { + ...JSON.parse(JSON.stringify(runElement)), + elements: splitElements.slice(0, splitIndex + 1), + }; + + const rightRunElement: Element = { + ...JSON.parse(JSON.stringify(runElement)), + elements: splitElements.slice(splitIndex + 1), + }; + + return { left: leftRunElement, right: rightRunElement }; +}; diff --git a/src/templater/paragraph-token-replacer.ts b/src/templater/paragraph-token-replacer.ts index 52c06892f5..45fb2bc302 100644 --- a/src/templater/paragraph-token-replacer.ts +++ b/src/templater/paragraph-token-replacer.ts @@ -1,10 +1,6 @@ import { Element } from "xml-js"; -import * as xml from "xml"; -import { Formatter } from "@export/formatter"; -import { Text } from "@file/paragraph/run/run-components/text"; - -import { toJson } from "./util"; +import { createTextElementContents } from "./util"; import { IRenderedParagraphNode } from "./run-renderer"; enum ReplaceMode { @@ -13,8 +9,6 @@ enum ReplaceMode { END, } -const formatter = new Formatter(); - export const replaceTokenInParagraphElement = ({ paragraphElement, renderedParagraph, @@ -63,10 +57,8 @@ export const replaceTokenInParagraphElement = ({ }; const patchTextElement = (element: Element, text: string): Element => { - const textJson = toJson(xml(formatter.format(new Text({ text })))); - // eslint-disable-next-line functional/immutable-data - element.elements = textJson.elements; + element.elements = createTextElementContents(text); return element; }; diff --git a/src/templater/replacer.ts b/src/templater/replacer.ts index 5dfacc3a2a..e40d66ca92 100644 --- a/src/templater/replacer.ts +++ b/src/templater/replacer.ts @@ -8,6 +8,7 @@ import { IPatch } from "./from-docx"; import { toJson } from "./util"; import { IRenderedParagraphNode } from "./run-renderer"; import { replaceTokenInParagraphElement } from "./paragraph-token-replacer"; +import { findRunElementIndexWithToken, splitRunElement } from "./paragraph-split-inject"; const formatter = new Formatter(); @@ -18,7 +19,6 @@ export const replacer = (json: Element, options: IPatch, renderedParagraphs: rea if (child instanceof Paragraph) { console.log("is para"); } else if (child instanceof TextRun) { - console.log("paragrapghs", JSON.stringify(renderedParagraphs, null, 2)); for (const renderedParagraph of renderedParagraphs) { const textJson = toJson(xml(formatter.format(child))); const paragraphElement = goToElementFromPath(json, renderedParagraph.path); @@ -33,9 +33,6 @@ export const replacer = (json: Element, options: IPatch, renderedParagraphs: rea } else { // Hard case where the text is only part of the paragraph - console.log("hard case"); - console.log("paragraphElement", JSON.stringify(paragraphElement, null, 2)); - replaceTokenInParagraphElement({ paragraphElement, renderedParagraph, @@ -43,6 +40,12 @@ export const replacer = (json: Element, options: IPatch, renderedParagraphs: rea replacementText: SPLIT_TOKEN, }); + const index = findRunElementIndexWithToken(paragraphElement, SPLIT_TOKEN); + + const { left, right } = splitRunElement(paragraphElement.elements![index], SPLIT_TOKEN); + // eslint-disable-next-line functional/immutable-data + paragraphElement.elements!.splice(index, 1, left, ...textJson.elements!, right); + console.log(index, JSON.stringify(paragraphElement.elements![index], null, 2)); console.log("paragraphElement after", JSON.stringify(paragraphElement, null, 2)); } } diff --git a/src/templater/run-renderer.ts b/src/templater/run-renderer.ts index b0e6132783..344898c10a 100644 --- a/src/templater/run-renderer.ts +++ b/src/templater/run-renderer.ts @@ -82,7 +82,7 @@ const renderRunNode = (node: Element, index: number, currentRunStringIndex: numb const parts = node.elements .map((element, i: number) => - element.name === "w:t" && element.elements + element.name === "w:t" && element.elements && element.elements.length > 0 ? { text: element.elements[0].text?.toString() ?? "", index: i, diff --git a/src/templater/util.ts b/src/templater/util.ts index bea0b16c1d..f385a00025 100644 --- a/src/templater/util.ts +++ b/src/templater/util.ts @@ -1,6 +1,19 @@ import { xml2js, Element } from "xml-js"; +import * as xml from "xml"; + +import { Formatter } from "@export/formatter"; +import { Text } from "@file/paragraph/run/run-components/text"; + +const formatter = new Formatter(); export const toJson = (xmlData: string): Element => { const xmlObj = xml2js(xmlData, { compact: false }) as Element; return xmlObj; }; + +// eslint-disable-next-line functional/prefer-readonly-type +export const createTextElementContents = (text: string): Element[] => { + const textJson = toJson(xml(formatter.format(new Text({ text })))); + + return textJson.elements![0].elements ?? []; +};