Simple patcher working
This commit is contained in:
@ -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 }}",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
@ -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];
|
||||||
|
@ -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
6
src/templater/util.ts
Normal 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;
|
||||||
|
};
|
Reference in New Issue
Block a user