This commit is contained in:
Dolan Miu
2023-02-25 22:18:56 +00:00
parent ce485dbc29
commit c9d86619de
9 changed files with 31 additions and 30 deletions

View File

@ -16,31 +16,31 @@ import {
patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
patches: { patches: {
"name":{ name: {
type: PatchType.PARAGRAPH, type: PatchType.PARAGRAPH,
children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")], children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")],
}, },
"table_heading_1": { table_heading_1: {
type: PatchType.PARAGRAPH, type: PatchType.PARAGRAPH,
children: [new TextRun("Heading wow!")], children: [new TextRun("Heading wow!")],
}, },
"item_1": { item_1: {
type: PatchType.PARAGRAPH, type: PatchType.PARAGRAPH,
children: [new TextRun("#657")], children: [new TextRun("#657")],
}, },
"paragraph_replace": { paragraph_replace: {
type: PatchType.DOCUMENT, type: PatchType.DOCUMENT,
children: [new Paragraph("Lorem ipsum paragraph"), new Paragraph("Another paragraph")], children: [new Paragraph("Lorem ipsum paragraph"), new Paragraph("Another paragraph")],
}, },
"header_adjective": { header_adjective: {
type: PatchType.PARAGRAPH, type: PatchType.PARAGRAPH,
children: [new TextRun("Delightful Header")], children: [new TextRun("Delightful Header")],
}, },
"footer_text": { footer_text: {
type: PatchType.PARAGRAPH, type: PatchType.PARAGRAPH,
children: [new TextRun("replaced just as well")], children: [new TextRun("replaced just as well")],
}, },
"table": { table: {
type: PatchType.DOCUMENT, type: PatchType.DOCUMENT,
children: [ children: [
new Table({ new Table({
@ -102,7 +102,6 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
], ],
}), }),
], ],
}), }),
], ],
}, },

View File

@ -8,7 +8,7 @@ const doc = new Document({
{ {
children: [ children: [
new Paragraph({ new Paragraph({
children: [new TextRun("{{ template }}")], children: [new TextRun("{{template}}")],
}), }),
], ],
}, },

Binary file not shown.

View File

@ -43,6 +43,7 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
for (const [patchKey, patchValue] of Object.entries(options.patches)) { for (const [patchKey, patchValue] of Object.entries(options.patches)) {
const patchText = `{{${patchKey}}}`; const patchText = `{{${patchKey}}}`;
const renderedParagraphs = findLocationOfText(json, patchText); const renderedParagraphs = findLocationOfText(json, patchText);
// TODO: mutates json. Make it immutable
replacer(json, patchValue, patchText, renderedParagraphs); replacer(json, patchValue, patchText, renderedParagraphs);
} }
} }

View File

@ -1,5 +1,5 @@
import { Element } from "xml-js"; import { Element } from "xml-js";
import { createTextElementContents } from "./util"; import { createTextElementContents, patchSpaceAttribute } from "./util";
export const findRunElementIndexWithToken = (paragraphElement: Element, token: string): number => { export const findRunElementIndexWithToken = (paragraphElement: Element, token: string): number => {
for (let i = 0; i < (paragraphElement.elements ?? []).length; i++) { for (let i = 0; i < (paragraphElement.elements ?? []).length; i++) {
@ -29,9 +29,7 @@ export const splitRunElement = (runElement: Element, token: string): { readonly
const splitText = text.split(token); const splitText = text.split(token);
const newElements = splitText.map((t) => ({ const newElements = splitText.map((t) => ({
...e, ...e,
attributes: { ...patchSpaceAttribute(e),
"xml:space": "preserve",
},
elements: createTextElementContents(t), elements: createTextElementContents(t),
})); }));
splitIndex = i; splitIndex = i;

View File

@ -1,6 +1,6 @@
import { Element } from "xml-js"; import { Element } from "xml-js";
import { createTextElementContents } from "./util"; import { createTextElementContents, patchSpaceAttribute } from "./util";
import { IRenderedParagraphNode } from "./run-renderer"; import { IRenderedParagraphNode } from "./run-renderer";
enum ReplaceMode { enum ReplaceMode {
@ -43,6 +43,11 @@ export const replaceTokenInParagraphElement = ({
if (endIndex <= end) { if (endIndex <= end) {
const lastPart = text.substring(endIndex - start + 1); const lastPart = text.substring(endIndex - start + 1);
patchTextElement(paragraphElement.elements![run.index].elements![index], lastPart); patchTextElement(paragraphElement.elements![run.index].elements![index], lastPart);
const currentElement = paragraphElement.elements![run.index].elements![index];
// We need to add xml:space="preserve" to the last element to preserve the whitespace
// Otherwise, the text will be merged with the next element
// eslint-disable-next-line functional/immutable-data
paragraphElement.elements![run.index].elements![index] = patchSpaceAttribute(currentElement);
replaceMode = ReplaceMode.END; replaceMode = ReplaceMode.END;
} else { } else {
patchTextElement(paragraphElement.elements![run.index].elements![index], ""); patchTextElement(paragraphElement.elements![run.index].elements![index], "");

View File

@ -14,7 +14,12 @@ const formatter = new Formatter();
const SPLIT_TOKEN = "ɵ"; const SPLIT_TOKEN = "ɵ";
export const replacer = (json: Element, patch: IPatch, patchText: string, renderedParagraphs: readonly IRenderedParagraphNode[]): Element => { export const replacer = (
json: Element,
patch: IPatch,
patchText: string,
renderedParagraphs: readonly IRenderedParagraphNode[],
): Element => {
for (const renderedParagraph of renderedParagraphs) { for (const renderedParagraph of renderedParagraphs) {
const textJson = patch.children.map((c) => toJson(xml(formatter.format(c as XmlComponent)))).map((c) => c.elements![0]); const textJson = patch.children.map((c) => toJson(xml(formatter.format(c as XmlComponent)))).map((c) => c.elements![0]);
@ -24,7 +29,6 @@ export const replacer = (json: Element, patch: IPatch, patchText: string, render
// eslint-disable-next-line functional/immutable-data, prefer-destructuring // eslint-disable-next-line functional/immutable-data, prefer-destructuring
parentElement.elements?.splice(elementIndex, 1, ...textJson); parentElement.elements?.splice(elementIndex, 1, ...textJson);
} else if (patch.type === PatchType.PARAGRAPH) { } else if (patch.type === PatchType.PARAGRAPH) {
// Hard case where the text is only part of the paragraph
const paragraphElement = goToElementFromPath(json, renderedParagraph.path); const paragraphElement = goToElementFromPath(json, renderedParagraph.path);
replaceTokenInParagraphElement({ replaceTokenInParagraphElement({

View File

@ -8,17 +8,6 @@ export interface ElementWrapper {
readonly parent: ElementWrapper | undefined; readonly parent: ElementWrapper | undefined;
} }
export interface ILocationOfText {
readonly parent: Element;
readonly startIndex: number;
readonly endIndex: number;
readonly currentText: string;
// This is optional because the text could start in the middle of a tag
readonly startElement?: Element;
// This is optional because the text could end in the middle of a tag
readonly endElement?: Element;
}
const elementsToWrapper = (wrapper: ElementWrapper): readonly ElementWrapper[] => const elementsToWrapper = (wrapper: ElementWrapper): readonly ElementWrapper[] =>
wrapper.element.elements?.map((e, i) => ({ wrapper.element.elements?.map((e, i) => ({
element: e, element: e,
@ -56,7 +45,5 @@ export const findLocationOfText = (node: Element, text: string): readonly IRende
} }
} }
const filteredParagraphs = renderedParagraphs.filter((p) => p.text.includes(text)); return renderedParagraphs.filter((p) => p.text.includes(text));
return filteredParagraphs;
}; };

View File

@ -17,3 +17,10 @@ export const createTextElementContents = (text: string): Element[] => {
return textJson.elements![0].elements ?? []; return textJson.elements![0].elements ?? [];
}; };
export const patchSpaceAttribute = (element: Element): Element => ({
...element,
attributes: {
"xml:space": "preserve",
},
});