diff --git a/src/templater/paragraph-token-replacer.ts b/src/templater/paragraph-token-replacer.ts new file mode 100644 index 0000000000..52c06892f5 --- /dev/null +++ b/src/templater/paragraph-token-replacer.ts @@ -0,0 +1,72 @@ +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 { IRenderedParagraphNode } from "./run-renderer"; + +enum ReplaceMode { + START, + MIDDLE, + END, +} + +const formatter = new Formatter(); + +export const replaceTokenInParagraphElement = ({ + paragraphElement, + renderedParagraph, + originalText, + replacementText, +}: { + readonly paragraphElement: Element; + readonly renderedParagraph: IRenderedParagraphNode; + readonly originalText: string; + readonly replacementText: string; +}): Element => { + const startIndex = renderedParagraph.text.indexOf(originalText); + const endIndex = startIndex + originalText.length - 1; + + let replaceMode = ReplaceMode.START; + + for (const run of renderedParagraph.runs) { + for (const { text, index, start, end } of run.parts) { + switch (replaceMode) { + case ReplaceMode.START: + if (startIndex >= start) { + const partToReplace = run.text.substring(Math.max(startIndex, start), Math.min(endIndex, end) + 1); + // We use a token to split the text if the replacement is within the same run + // If not, we just add text to the middle of the run later + const firstPart = text.replace(partToReplace, replacementText); + patchTextElement(paragraphElement.elements![run.index].elements![index], firstPart); + replaceMode = ReplaceMode.MIDDLE; + continue; + } + break; + case ReplaceMode.MIDDLE: + if (endIndex <= end) { + const lastPart = text.substring(endIndex - start + 1); + patchTextElement(paragraphElement.elements![run.index].elements![index], lastPart); + replaceMode = ReplaceMode.END; + } else { + patchTextElement(paragraphElement.elements![run.index].elements![index], ""); + } + break; + default: + } + } + } + + return paragraphElement; +}; + +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; + + return element; +}; diff --git a/src/templater/replacer.ts b/src/templater/replacer.ts index 8a42327d9f..5dfacc3a2a 100644 --- a/src/templater/replacer.ts +++ b/src/templater/replacer.ts @@ -1,27 +1,51 @@ -import { Formatter } from "@export/formatter"; -import { Paragraph, TextRun } from "@file/paragraph"; import { Element } from "xml-js"; import * as xml from "xml"; +import { Formatter } from "@export/formatter"; +import { Paragraph, TextRun } from "@file/paragraph"; + import { IPatch } from "./from-docx"; import { toJson } from "./util"; import { IRenderedParagraphNode } from "./run-renderer"; +import { replaceTokenInParagraphElement } from "./paragraph-token-replacer"; const formatter = new Formatter(); +const SPLIT_TOKEN = "ɵ"; + export const replacer = (json: Element, options: IPatch, renderedParagraphs: readonly IRenderedParagraphNode[]): Element => { for (const child of options.children) { if (child instanceof Paragraph) { console.log("is para"); } else if (child instanceof TextRun) { - const text = formatter.format(child); - const textJson = toJson(xml(text)); console.log("paragrapghs", JSON.stringify(renderedParagraphs, null, 2)); - const paragraphElement = goToElementFromPath(json, renderedParagraphs[0].path); - console.log(paragraphElement); - // eslint-disable-next-line functional/immutable-data - paragraphElement.elements = textJson.elements; - console.log("is text", text); + for (const renderedParagraph of renderedParagraphs) { + const textJson = toJson(xml(formatter.format(child))); + const paragraphElement = goToElementFromPath(json, renderedParagraph.path); + + const startIndex = renderedParagraph.text.indexOf(options.text); + const endIndex = startIndex + options.text.length - 1; + + if (startIndex === 0 && endIndex === renderedParagraph.text.length - 1) { + // Easy case where the text is the entire paragraph + // eslint-disable-next-line functional/immutable-data + paragraphElement.elements = textJson.elements; + } 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, + originalText: options.text, + replacementText: SPLIT_TOKEN, + }); + + console.log("paragraphElement after", JSON.stringify(paragraphElement, null, 2)); + } + } } }