Simple patcher working

This commit is contained in:
Dolan Miu
2023-02-18 20:36:24 +00:00
parent 86de252a52
commit 5233b4b5e6
6 changed files with 121 additions and 32 deletions

View File

@ -1,12 +1,12 @@
// Simple template example // Simple template example
// Import from 'docx' rather than '../build' if you install from npm // Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs"; import * as fs from "fs";
import { Paragraph, patchDocument, TextRun } from "../build"; import { patchDocument, TextRun } from "../build";
patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
patches: [ patches: [
{ {
children: [new Paragraph("ff"), new TextRun("fgf")], children: [new TextRun("John Doe")],
text: "{{ name }}", text: "{{ name }}",
}, },
], ],

View File

@ -1,7 +1,8 @@
import * as JSZip from "jszip"; import * as JSZip from "jszip";
import { xml2js, Element, js2xml } from "xml-js"; import { Element, js2xml } from "xml-js";
import { replacer } from "./replacer"; import { replacer } from "./replacer";
import { findLocationOfText } from "./traverser"; import { findLocationOfText } from "./traverser";
import { toJson } from "./util";
// eslint-disable-next-line functional/prefer-readonly-type // eslint-disable-next-line functional/prefer-readonly-type
type InputDataType = Buffer | string | number[] | Uint8Array | ArrayBuffer | Blob | NodeJS.ReadableStream; 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")); const json = toJson(await value.async("text"));
if (key === "word/document.xml") { if (key === "word/document.xml") {
for (const patch of options.patches) { for (const patch of options.patches) {
findLocationOfText(json, patch.text); const renderedParagraphs = findLocationOfText(json, patch.text);
replacer(json, patch); replacer(json, patch, renderedParagraphs);
} }
} }
@ -48,11 +49,6 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
return zipData; return zipData;
}; };
const toJson = (xmlData: string): Element => {
const xmlObj = xml2js(xmlData, { compact: false }) as Element;
return xmlObj;
};
const toXml = (jsonObj: Element): string => { const toXml = (jsonObj: Element): string => {
const output = js2xml(jsonObj); const output = js2xml(jsonObj);
return output; return output;

View File

@ -1,15 +1,48 @@
import { Formatter } from "@export/formatter";
import { Paragraph, TextRun } from "@file/paragraph"; import { Paragraph, TextRun } from "@file/paragraph";
import { ElementCompact } from "xml-js"; import { Element } from "xml-js";
import { IPatch } from "./from-docx"; 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) { for (const child of options.children) {
if (child instanceof Paragraph) { if (child instanceof Paragraph) {
console.log("is para"); console.log("is para");
} else if (child instanceof TextRun) { } 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; 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;
};

View File

@ -1,37 +1,55 @@
import { Element } from "xml-js"; import { Element } from "xml-js";
import { ElementWrapper } from "./traverser";
export interface IRenderedParagraphNode { export interface IRenderedParagraphNode {
readonly text: string; readonly text: string;
readonly runs: readonly IRenderedRunNode[]; 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 text: string;
readonly index: number; readonly index: number;
} } & StartAndEnd;
export interface IRenderedRunNode { export type IRenderedRunNode = {
readonly text: string; readonly text: string;
readonly parts: readonly IParts[]; readonly parts: readonly IParts[];
readonly index: number; readonly index: number;
} } & StartAndEnd;
export const renderParagraphNode = (node: Element): IRenderedParagraphNode => { export const renderParagraphNode = (node: ElementWrapper): IRenderedParagraphNode => {
if (node.name !== "w:p") { if (node.element.name !== "w:p") {
throw new Error(`Invalid node type: ${node.name}`); throw new Error(`Invalid node type: ${node.element.name}`);
} }
if (!node.elements) { if (!node.element.elements) {
return { return {
text: "", text: "",
runs: [], runs: [],
index: -1,
path: [],
}; };
} }
const runs = node.elements let currentRunStringLength = 0;
const runs = node.element.elements
.map((element, i) => ({ element, i })) .map((element, i) => ({ element, i }))
.filter(({ element }) => element.name === "w:r") .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) .filter((e) => !!e)
.map((e) => e as IRenderedRunNode); .map((e) => e as IRenderedRunNode);
@ -40,10 +58,12 @@ export const renderParagraphNode = (node: Element): IRenderedParagraphNode => {
return { return {
text, text,
runs, 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") { if (node.name !== "w:r") {
throw new Error(`Invalid node type: ${node.name}`); throw new Error(`Invalid node type: ${node.name}`);
} }
@ -53,15 +73,25 @@ const renderRunNode = (node: Element, index: number): IRenderedRunNode => {
text: "", text: "",
parts: [], parts: [],
index: -1, index: -1,
start: currentRunStringIndex,
end: currentRunStringIndex,
}; };
} }
let currentTextStringIndex = currentRunStringIndex;
const parts = node.elements const parts = node.elements
.map((element, i: number) => .map((element, i: number) =>
element.name === "w:t" && element.elements element.name === "w:t" && element.elements
? { ? {
text: element.elements[0].text?.toString() ?? "", text: element.elements[0].text?.toString() ?? "",
index: i, index: i,
start: currentTextStringIndex,
end: (() => {
// Side effect
currentTextStringIndex += (element.elements[0].text?.toString() ?? "").length - 1;
return currentTextStringIndex;
})(),
} }
: undefined, : undefined,
) )
@ -74,5 +104,10 @@ const renderRunNode = (node: Element, index: number): IRenderedRunNode => {
text, text,
parts, parts,
index, index,
start: currentRunStringIndex,
end: currentTextStringIndex,
}; };
}; };
const buildNodePath = (node: ElementWrapper): readonly number[] =>
node.parent ? [...buildNodePath(node.parent), node.index] : [node.index];

View File

@ -1,6 +1,13 @@
import { Element } from "xml-js"; import { Element } from "xml-js";
import { IRenderedParagraphNode, renderParagraphNode } from "./run-renderer"; import { IRenderedParagraphNode, renderParagraphNode } from "./run-renderer";
export interface ElementWrapper {
readonly element: Element;
readonly index: number;
readonly parent: ElementWrapper | undefined;
}
export interface ILocationOfText { export interface ILocationOfText {
readonly parent: Element; readonly parent: Element;
readonly startIndex: number; readonly startIndex: number;
@ -12,14 +19,27 @@ export interface ILocationOfText {
readonly endElement?: Element; 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[] = []; let renderedParagraphs: readonly IRenderedParagraphNode[] = [];
// eslint-disable-next-line functional/prefer-readonly-type // 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 // eslint-disable-next-line functional/immutable-data
let currentNode: Element | undefined; let currentNode: ElementWrapper | undefined;
while (queue.length > 0) { while (queue.length > 0) {
// eslint-disable-next-line functional/immutable-data // eslint-disable-next-line functional/immutable-data
currentNode = queue.shift(); currentNode = queue.shift();
@ -28,16 +48,15 @@ export const findLocationOfText = (node: Element, text: string): void => {
break; break;
} }
if (currentNode.name === "w:p") { if (currentNode.element.name === "w:p") {
renderedParagraphs = [...renderedParagraphs, renderParagraphNode(currentNode)]; renderedParagraphs = [...renderedParagraphs, renderParagraphNode(currentNode)];
} else { } else {
// eslint-disable-next-line functional/immutable-data // eslint-disable-next-line functional/immutable-data
queue.push(...(currentNode.elements ?? [])); queue.push(...(elementsToWrapper(currentNode) ?? []));
} }
} }
const filteredParagraphs = renderedParagraphs.filter((p) => p.text.includes(text)); const filteredParagraphs = renderedParagraphs.filter((p) => p.text.includes(text));
console.log("paragrapghs", JSON.stringify(filteredParagraphs, null, 2)); return filteredParagraphs;
return undefined;
}; };

6
src/templater/util.ts Normal file
View File

@ -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;
};