From 5233b4b5e626ef56ad09a0c138e6b2191f27d54a Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Sat, 18 Feb 2023 20:36:24 +0000 Subject: [PATCH] Simple patcher working --- demo/85-template-document.ts | 4 +-- src/templater/from-docx.ts | 12 +++----- src/templater/replacer.ts | 41 ++++++++++++++++++++++--- src/templater/run-renderer.ts | 57 ++++++++++++++++++++++++++++------- src/templater/traverser.ts | 33 +++++++++++++++----- src/templater/util.ts | 6 ++++ 6 files changed, 121 insertions(+), 32 deletions(-) create mode 100644 src/templater/util.ts diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index 68537abaa6..5baa2397f4 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -1,12 +1,12 @@ // Simple template example // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; -import { Paragraph, patchDocument, TextRun } from "../build"; +import { patchDocument, TextRun } from "../build"; patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { patches: [ { - children: [new Paragraph("ff"), new TextRun("fgf")], + children: [new TextRun("John Doe")], text: "{{ name }}", }, ], diff --git a/src/templater/from-docx.ts b/src/templater/from-docx.ts index 4d5e640b96..842e600560 100644 --- a/src/templater/from-docx.ts +++ b/src/templater/from-docx.ts @@ -1,7 +1,8 @@ import * as JSZip from "jszip"; -import { xml2js, Element, js2xml } from "xml-js"; +import { Element, js2xml } from "xml-js"; import { replacer } from "./replacer"; import { findLocationOfText } from "./traverser"; +import { toJson } from "./util"; // eslint-disable-next-line functional/prefer-readonly-type type InputDataType = Buffer | string | number[] | Uint8Array | ArrayBuffer | Blob | NodeJS.ReadableStream; @@ -23,8 +24,8 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO const json = toJson(await value.async("text")); if (key === "word/document.xml") { for (const patch of options.patches) { - findLocationOfText(json, patch.text); - replacer(json, patch); + const renderedParagraphs = findLocationOfText(json, patch.text); + replacer(json, patch, renderedParagraphs); } } @@ -48,11 +49,6 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO return zipData; }; -const toJson = (xmlData: string): Element => { - const xmlObj = xml2js(xmlData, { compact: false }) as Element; - return xmlObj; -}; - const toXml = (jsonObj: Element): string => { const output = js2xml(jsonObj); return output; diff --git a/src/templater/replacer.ts b/src/templater/replacer.ts index be6e301ebf..8a42327d9f 100644 --- a/src/templater/replacer.ts +++ b/src/templater/replacer.ts @@ -1,15 +1,48 @@ +import { Formatter } from "@export/formatter"; import { Paragraph, TextRun } from "@file/paragraph"; -import { ElementCompact } from "xml-js"; -import { IPatch } from "./from-docx"; +import { Element } from "xml-js"; +import * as xml from "xml"; -export const replacer = (json: ElementCompact, options: IPatch): ElementCompact => { +import { IPatch } from "./from-docx"; +import { toJson } from "./util"; +import { IRenderedParagraphNode } from "./run-renderer"; + +const formatter = new Formatter(); + +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) { - console.log("is text"); + 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); } } return json; }; + +const goToElementFromPath = (json: Element, path: readonly number[]): Element => { + let element = json; + + // We start from 1 because the first element is the root element + // Which we do not want to double count + for (let i = 1; i < path.length; i++) { + const index = path[i]; + const nextElements = element.elements; + + if (!nextElements) { + throw new Error("Could not find element"); + } + + element = nextElements[index]; + } + + return element; +}; diff --git a/src/templater/run-renderer.ts b/src/templater/run-renderer.ts index 6efc7a04b9..b0e6132783 100644 --- a/src/templater/run-renderer.ts +++ b/src/templater/run-renderer.ts @@ -1,37 +1,55 @@ import { Element } from "xml-js"; +import { ElementWrapper } from "./traverser"; + export interface IRenderedParagraphNode { readonly text: string; readonly runs: readonly IRenderedRunNode[]; + readonly index: number; + readonly path: readonly number[]; } -interface IParts { +interface StartAndEnd { + readonly start: number; + readonly end: number; +} + +type IParts = { readonly text: string; readonly index: number; -} +} & StartAndEnd; -export interface IRenderedRunNode { +export type IRenderedRunNode = { readonly text: string; readonly parts: readonly IParts[]; readonly index: number; -} +} & StartAndEnd; -export const renderParagraphNode = (node: Element): IRenderedParagraphNode => { - if (node.name !== "w:p") { - throw new Error(`Invalid node type: ${node.name}`); +export const renderParagraphNode = (node: ElementWrapper): IRenderedParagraphNode => { + if (node.element.name !== "w:p") { + throw new Error(`Invalid node type: ${node.element.name}`); } - if (!node.elements) { + if (!node.element.elements) { return { text: "", runs: [], + index: -1, + path: [], }; } - const runs = node.elements + let currentRunStringLength = 0; + + const runs = node.element.elements .map((element, i) => ({ element, i })) .filter(({ element }) => element.name === "w:r") - .map(({ element, i }) => renderRunNode(element, i)) + .map(({ element, i }) => { + const renderedRunNode = renderRunNode(element, i, currentRunStringLength); + currentRunStringLength += renderedRunNode.text.length; + + return renderedRunNode; + }) .filter((e) => !!e) .map((e) => e as IRenderedRunNode); @@ -40,10 +58,12 @@ export const renderParagraphNode = (node: Element): IRenderedParagraphNode => { return { text, runs, + index: node.index, + path: buildNodePath(node), }; }; -const renderRunNode = (node: Element, index: number): IRenderedRunNode => { +const renderRunNode = (node: Element, index: number, currentRunStringIndex: number): IRenderedRunNode => { if (node.name !== "w:r") { throw new Error(`Invalid node type: ${node.name}`); } @@ -53,15 +73,25 @@ const renderRunNode = (node: Element, index: number): IRenderedRunNode => { text: "", parts: [], index: -1, + start: currentRunStringIndex, + end: currentRunStringIndex, }; } + let currentTextStringIndex = currentRunStringIndex; + const parts = node.elements .map((element, i: number) => element.name === "w:t" && element.elements ? { text: element.elements[0].text?.toString() ?? "", index: i, + start: currentTextStringIndex, + end: (() => { + // Side effect + currentTextStringIndex += (element.elements[0].text?.toString() ?? "").length - 1; + return currentTextStringIndex; + })(), } : undefined, ) @@ -74,5 +104,10 @@ const renderRunNode = (node: Element, index: number): IRenderedRunNode => { text, parts, index, + start: currentRunStringIndex, + end: currentTextStringIndex, }; }; + +const buildNodePath = (node: ElementWrapper): readonly number[] => + node.parent ? [...buildNodePath(node.parent), node.index] : [node.index]; diff --git a/src/templater/traverser.ts b/src/templater/traverser.ts index 61abf5f8d6..95f7829042 100644 --- a/src/templater/traverser.ts +++ b/src/templater/traverser.ts @@ -1,6 +1,13 @@ import { Element } from "xml-js"; + import { IRenderedParagraphNode, renderParagraphNode } from "./run-renderer"; +export interface ElementWrapper { + readonly element: Element; + readonly index: number; + readonly parent: ElementWrapper | undefined; +} + export interface ILocationOfText { readonly parent: Element; readonly startIndex: number; @@ -12,14 +19,27 @@ export interface ILocationOfText { readonly endElement?: Element; } -export const findLocationOfText = (node: Element, text: string): void => { +const elementsToWrapper = (wrapper: ElementWrapper): readonly ElementWrapper[] => + wrapper.element.elements?.map((e, i) => ({ + element: e, + index: i, + parent: wrapper, + })) ?? []; + +export const findLocationOfText = (node: Element, text: string): readonly IRenderedParagraphNode[] => { let renderedParagraphs: readonly IRenderedParagraphNode[] = []; // eslint-disable-next-line functional/prefer-readonly-type - const queue: Element[] = [...(node.elements ?? [])]; + const queue: ElementWrapper[] = [ + ...(elementsToWrapper({ + element: node, + index: 0, + parent: undefined, + }) ?? []), + ]; // eslint-disable-next-line functional/immutable-data - let currentNode: Element | undefined; + let currentNode: ElementWrapper | undefined; while (queue.length > 0) { // eslint-disable-next-line functional/immutable-data currentNode = queue.shift(); @@ -28,16 +48,15 @@ export const findLocationOfText = (node: Element, text: string): void => { break; } - if (currentNode.name === "w:p") { + if (currentNode.element.name === "w:p") { renderedParagraphs = [...renderedParagraphs, renderParagraphNode(currentNode)]; } else { // eslint-disable-next-line functional/immutable-data - queue.push(...(currentNode.elements ?? [])); + queue.push(...(elementsToWrapper(currentNode) ?? [])); } } const filteredParagraphs = renderedParagraphs.filter((p) => p.text.includes(text)); - console.log("paragrapghs", JSON.stringify(filteredParagraphs, null, 2)); - return undefined; + return filteredParagraphs; }; diff --git a/src/templater/util.ts b/src/templater/util.ts new file mode 100644 index 0000000000..bea0b16c1d --- /dev/null +++ b/src/templater/util.ts @@ -0,0 +1,6 @@ +import { xml2js, Element } from "xml-js"; + +export const toJson = (xmlData: string): Element => { + const xmlObj = xml2js(xmlData, { compact: false }) as Element; + return xmlObj; +};