diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index 4e7608009f..0c95599e84 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -1,26 +1,107 @@ // Simple template example // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; -import { patchDocument, TextRun } from "../build"; +import { + HeadingLevel, + Paragraph, + patchDocument, + PatchType, + Table, + TableCell, + TableRow, + TextDirection, + TextRun, + VerticalAlign, +} from "../src"; patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { patches: [ { - children: [new TextRun("John Doe")], + type: PatchType.PARAGRAPH, + children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")], text: "{{ name }}", }, { + type: PatchType.PARAGRAPH, children: [new TextRun("Heading wow!")], text: "{{ table_heading_1 }}", }, { + type: PatchType.PARAGRAPH, children: [new TextRun("#657")], text: "{{ item_1 }}", }, { - children: [new TextRun("Lorem ipsum paragraph")], + type: PatchType.DOCUMENT, + children: [new Paragraph("Lorem ipsum paragraph"), new Paragraph("Another paragraph")], text: "{{ paragraph_replace }}", }, + { + type: PatchType.DOCUMENT, + children: [ + new Table({ + rows: [ + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph({}), new Paragraph({})], + verticalAlign: VerticalAlign.CENTER, + }), + new TableCell({ + children: [new Paragraph({}), new Paragraph({})], + verticalAlign: VerticalAlign.CENTER, + }), + new TableCell({ + children: [new Paragraph({ text: "bottom to top" }), new Paragraph({})], + textDirection: TextDirection.BOTTOM_TO_TOP_LEFT_TO_RIGHT, + }), + new TableCell({ + children: [new Paragraph({ text: "top to bottom" }), new Paragraph({})], + textDirection: TextDirection.TOP_TO_BOTTOM_RIGHT_TO_LEFT, + }), + ], + }), + new TableRow({ + children: [ + new TableCell({ + children: [ + new Paragraph({ + text: "Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah", + heading: HeadingLevel.HEADING_1, + }), + ], + }), + new TableCell({ + children: [ + new Paragraph({ + text: "This text should be in the middle of the cell", + }), + ], + verticalAlign: VerticalAlign.CENTER, + }), + new TableCell({ + children: [ + new Paragraph({ + text: "Text above should be vertical from bottom to top", + }), + ], + verticalAlign: VerticalAlign.CENTER, + }), + new TableCell({ + children: [ + new Paragraph({ + text: "Text above should be vertical from top to bottom", + }), + ], + verticalAlign: VerticalAlign.CENTER, + }), + ], + }), + ], + }), + ], + text: "{{ table }}", + }, ], }).then((doc) => { fs.writeFileSync("My Document.docx", doc); diff --git a/demo/assets/simple-template.docx b/demo/assets/simple-template.docx index 1ff29bc529..c926191881 100644 Binary files a/demo/assets/simple-template.docx and b/demo/assets/simple-template.docx differ diff --git a/src/templater/from-docx.ts b/src/templater/from-docx.ts index 842e600560..4117e04917 100644 --- a/src/templater/from-docx.ts +++ b/src/templater/from-docx.ts @@ -1,5 +1,9 @@ import * as JSZip from "jszip"; import { Element, js2xml } from "xml-js"; + +import { ParagraphChild } from "@file/paragraph"; +import { FileChild } from "@file/file-child"; + import { replacer } from "./replacer"; import { findLocationOfText } from "./traverser"; import { toJson } from "./util"; @@ -7,10 +11,25 @@ import { toJson } from "./util"; // eslint-disable-next-line functional/prefer-readonly-type type InputDataType = Buffer | string | number[] | Uint8Array | ArrayBuffer | Blob | NodeJS.ReadableStream; -export interface IPatch { - readonly children: any[]; - readonly text: string; +export enum PatchType { + DOCUMENT = "file", + PARAGRAPH = "paragraph", } + +type ParagraphPatch = { + readonly type: PatchType.PARAGRAPH; + readonly children: readonly ParagraphChild[]; +}; + +type FilePatch = { + readonly type: PatchType.DOCUMENT; + readonly children: readonly FileChild[]; +}; + +export type IPatch = { + readonly text: string; +} & (ParagraphPatch | FilePatch); + export interface PatchDocumentOptions { readonly patches: readonly IPatch[]; } diff --git a/src/templater/replacer.ts b/src/templater/replacer.ts index e40d66ca92..95e607a777 100644 --- a/src/templater/replacer.ts +++ b/src/templater/replacer.ts @@ -2,9 +2,9 @@ import { Element } from "xml-js"; import * as xml from "xml"; import { Formatter } from "@export/formatter"; -import { Paragraph, TextRun } from "@file/paragraph"; +import { XmlComponent } from "@file/xml-components"; -import { IPatch } from "./from-docx"; +import { IPatch, PatchType } from "./from-docx"; import { toJson } from "./util"; import { IRenderedParagraphNode } from "./run-renderer"; import { replaceTokenInParagraphElement } from "./paragraph-token-replacer"; @@ -15,39 +15,46 @@ 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) { - for (const renderedParagraph of renderedParagraphs) { - const textJson = toJson(xml(formatter.format(child))); - const paragraphElement = goToElementFromPath(json, renderedParagraph.path); + for (const renderedParagraph of renderedParagraphs) { + const textJson = options.children.map((c) => toJson(xml(formatter.format(c as XmlComponent)))).map((c) => c.elements![0]); - const startIndex = renderedParagraph.text.indexOf(options.text); - const endIndex = startIndex + options.text.length - 1; + if (options.type === PatchType.DOCUMENT) { + const parentElement = goToParentElementFromPath(json, renderedParagraph.path); + const elementIndex = getLastElementIndexFromPath(renderedParagraph.path); + // Easy case where the text is the entire paragraph + // We can assume that the Paragraph/Table only has one element + // eslint-disable-next-line functional/immutable-data, prefer-destructuring + parentElement.elements?.splice(elementIndex, 1, ...textJson); + // console.log(JSON.stringify(renderedParagraphs, null, 2)); + // console.log(JSON.stringify(textJson, null, 2)); + // console.log("paragraphElement after", JSON.stringify(parentElement.elements![elementIndex], null, 2)); + } else if (options.type === PatchType.PARAGRAPH) { + 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 + 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; + console.log(JSON.stringify(paragraphElement, null, 2)); + } else { + // Hard case where the text is only part of the paragraph - replaceTokenInParagraphElement({ - paragraphElement, - renderedParagraph, - originalText: options.text, - replacementText: SPLIT_TOKEN, - }); + replaceTokenInParagraphElement({ + paragraphElement, + renderedParagraph, + originalText: options.text, + replacementText: SPLIT_TOKEN, + }); - const index = findRunElementIndexWithToken(paragraphElement, SPLIT_TOKEN); + const index = findRunElementIndexWithToken(paragraphElement, SPLIT_TOKEN); - const { left, right } = splitRunElement(paragraphElement.elements![index], SPLIT_TOKEN); - // eslint-disable-next-line functional/immutable-data - paragraphElement.elements!.splice(index, 1, left, ...textJson.elements!, right); - console.log(index, JSON.stringify(paragraphElement.elements![index], null, 2)); - console.log("paragraphElement after", JSON.stringify(paragraphElement, null, 2)); - } + const { left, right } = splitRunElement(paragraphElement.elements![index], SPLIT_TOKEN); + // eslint-disable-next-line functional/immutable-data + paragraphElement.elements!.splice(index, 1, left, ...textJson, right); + // console.log(index, JSON.stringify(paragraphElement.elements![index], null, 2)); + // console.log("paragraphElement after", JSON.stringify(paragraphElement, null, 2)); } } } @@ -73,3 +80,8 @@ const goToElementFromPath = (json: Element, path: readonly number[]): Element => return element; }; + +const goToParentElementFromPath = (json: Element, path: readonly number[]): Element => + goToElementFromPath(json, path.slice(0, path.length - 1)); + +const getLastElementIndexFromPath = (path: readonly number[]): number => path[path.length - 1];