From ffd998cbf5fb67e6c4b1b7ce407afe3ccc86b2cb Mon Sep 17 00:00:00 2001 From: Dolan Date: Wed, 8 Feb 2023 03:18:11 +0000 Subject: [PATCH 01/26] Simple import and export of document --- demo/85-template-document.ts | 8 +++++++ demo/template.docx | Bin 0 -> 14233 bytes src/index.ts | 1 + src/templater/from-docx.ts | 42 +++++++++++++++++++++++++++++++++++ src/templater/index.ts | 1 + 5 files changed, 52 insertions(+) create mode 100644 demo/85-template-document.ts create mode 100644 demo/template.docx create mode 100644 src/templater/from-docx.ts create mode 100644 src/templater/index.ts diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts new file mode 100644 index 0000000000..d219170491 --- /dev/null +++ b/demo/85-template-document.ts @@ -0,0 +1,8 @@ +// Simple template example +// Import from 'docx' rather than '../build' if you install from npm +import * as fs from "fs"; +import { fromDocx } from "../build"; + +fromDocx(fs.readFileSync("demo/template.docx")).then((doc) => { + fs.writeFileSync("My Document.docx", doc); +}); diff --git a/demo/template.docx b/demo/template.docx new file mode 100644 index 0000000000000000000000000000000000000000..fd05df5947551e4ec2f75838e257d2b49f7512c1 GIT binary patch literal 14233 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 literal 0 HcmV?d00001 diff --git a/src/index.ts b/src/index.ts index 3d76a0f70f..7162ae5a1f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,4 @@ export * from "./file"; export * from "./export"; export * from "./import-dotx"; export * from "./util"; +export * from "./templater"; diff --git a/src/templater/from-docx.ts b/src/templater/from-docx.ts new file mode 100644 index 0000000000..cec1a84877 --- /dev/null +++ b/src/templater/from-docx.ts @@ -0,0 +1,42 @@ +import * as JSZip from "jszip"; +import { xml2js, ElementCompact, js2xml } from "xml-js"; + +// eslint-disable-next-line functional/prefer-readonly-type +type InputDataType = Buffer | string | number[] | Uint8Array | ArrayBuffer | Blob | NodeJS.ReadableStream; + +export const fromDocx = async (data: InputDataType): Promise => { + const zipContent = await JSZip.loadAsync(data); + + const map = new Map(); + + for (const [key, value] of Object.entries(zipContent.files)) { + const json = toJson(await value.async("text")); + map.set(key, json); + } + + const zip = new JSZip(); + + for (const [key, value] of map) { + const output = toXml(value); + + zip.file(key, output); + } + + const zipData = await zip.generateAsync({ + type: "nodebuffer", + mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + compression: "DEFLATE", + }); + + return zipData; +}; + +const toJson = (xmlData: string): ElementCompact => { + const xmlObj = xml2js(xmlData, { compact: false }) as ElementCompact; + return xmlObj; +}; + +const toXml = (jsonObj: ElementCompact): string => { + const output = js2xml(jsonObj); + return output; +}; diff --git a/src/templater/index.ts b/src/templater/index.ts new file mode 100644 index 0000000000..466cb3eda7 --- /dev/null +++ b/src/templater/index.ts @@ -0,0 +1 @@ +export * from "./from-docx"; From c206d23480d95f5a73df372b474f9fe06dfbb2b3 Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Thu, 16 Feb 2023 20:17:48 +0000 Subject: [PATCH 02/26] work on docx patcher --- demo/85-template-document.ts | 6 ++++-- demo/assets/simple-template.docx | Bin 0 -> 12379 bytes demo/template.docx | Bin 14233 -> 0 bytes src/templater/from-docx.ts | 13 ++++++++++++- src/templater/replacer.ts | 15 +++++++++++++++ 5 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 demo/assets/simple-template.docx delete mode 100644 demo/template.docx create mode 100644 src/templater/replacer.ts diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index d219170491..43e345b0b0 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -1,8 +1,10 @@ // Simple template example // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; -import { fromDocx } from "../build"; +import { Paragraph, patchDocument, TextRun } from "../build"; -fromDocx(fs.readFileSync("demo/template.docx")).then((doc) => { +patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { + children: [new Paragraph("ff"), new TextRun("fgf")], +}).then((doc) => { fs.writeFileSync("My Document.docx", doc); }); diff --git a/demo/assets/simple-template.docx b/demo/assets/simple-template.docx new file mode 100644 index 0000000000000000000000000000000000000000..c9afe4f66dd93bd80a191133b469030453b61a4c GIT binary patch literal 12379 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 diff --git a/src/templater/from-docx.ts b/src/templater/from-docx.ts index cec1a84877..b17bbf1f90 100644 --- a/src/templater/from-docx.ts +++ b/src/templater/from-docx.ts @@ -1,16 +1,27 @@ import * as JSZip from "jszip"; import { xml2js, ElementCompact, js2xml } from "xml-js"; +import { replacer } from "./replacer"; // eslint-disable-next-line functional/prefer-readonly-type type InputDataType = Buffer | string | number[] | Uint8Array | ArrayBuffer | Blob | NodeJS.ReadableStream; -export const fromDocx = async (data: InputDataType): Promise => { +export interface PatchDocumentOptions { + readonly children: any[]; +} + +export const patchDocument = async (data: InputDataType, options: PatchDocumentOptions): Promise => { const zipContent = await JSZip.loadAsync(data); const map = new Map(); + console.log(options); for (const [key, value] of Object.entries(zipContent.files)) { const json = toJson(await value.async("text")); + if (key === "word/document.xml") { + console.log(json); + replacer(json, options); + } + map.set(key, json); } diff --git a/src/templater/replacer.ts b/src/templater/replacer.ts new file mode 100644 index 0000000000..830c0a5054 --- /dev/null +++ b/src/templater/replacer.ts @@ -0,0 +1,15 @@ +import { Paragraph, TextRun } from "@file/paragraph"; +import { ElementCompact } from "xml-js"; +import { PatchDocumentOptions } from "./from-docx"; + +export const replacer = (json: ElementCompact, options: PatchDocumentOptions): ElementCompact => { + for (const child of options.children) { + if (child instanceof Paragraph) { + console.log("is para"); + } else if (child instanceof TextRun) { + console.log("is text"); + } + } + + return json; +}; From 86de252a52b053974501905ecd35def980589118 Mon Sep 17 00:00:00 2001 From: Dolan Date: Fri, 17 Feb 2023 10:38:03 +0000 Subject: [PATCH 03/26] Extract runs and text --- demo/85-template-document.ts | 7 ++- demo/assets/simple-template.docx | Bin 12379 -> 12624 bytes src/templater/from-docx.ts | 24 ++++++---- src/templater/replacer.ts | 4 +- src/templater/run-renderer.ts | 78 +++++++++++++++++++++++++++++++ src/templater/traverser.ts | 43 +++++++++++++++++ 6 files changed, 144 insertions(+), 12 deletions(-) create mode 100644 src/templater/run-renderer.ts create mode 100644 src/templater/traverser.ts diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index 43e345b0b0..68537abaa6 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -4,7 +4,12 @@ import * as fs from "fs"; import { Paragraph, patchDocument, TextRun } from "../build"; patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { - children: [new Paragraph("ff"), new TextRun("fgf")], + patches: [ + { + children: [new Paragraph("ff"), new TextRun("fgf")], + text: "{{ name }}", + }, + ], }).then((doc) => { fs.writeFileSync("My Document.docx", doc); }); diff --git a/demo/assets/simple-template.docx b/demo/assets/simple-template.docx index c9afe4f66dd93bd80a191133b469030453b61a4c..1ff29bc52974b12438f3e01b325c0a84c6113f19 100644 GIT binary patch delta 7443 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 => { const zipContent = await JSZip.loadAsync(data); - const map = new Map(); - console.log(options); + const map = new Map(); for (const [key, value] of Object.entries(zipContent.files)) { const json = toJson(await value.async("text")); if (key === "word/document.xml") { - console.log(json); - replacer(json, options); + for (const patch of options.patches) { + findLocationOfText(json, patch.text); + replacer(json, patch); + } } map.set(key, json); @@ -42,12 +48,12 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO return zipData; }; -const toJson = (xmlData: string): ElementCompact => { - const xmlObj = xml2js(xmlData, { compact: false }) as ElementCompact; +const toJson = (xmlData: string): Element => { + const xmlObj = xml2js(xmlData, { compact: false }) as Element; return xmlObj; }; -const toXml = (jsonObj: ElementCompact): string => { +const toXml = (jsonObj: Element): string => { const output = js2xml(jsonObj); return output; }; diff --git a/src/templater/replacer.ts b/src/templater/replacer.ts index 830c0a5054..be6e301ebf 100644 --- a/src/templater/replacer.ts +++ b/src/templater/replacer.ts @@ -1,8 +1,8 @@ import { Paragraph, TextRun } from "@file/paragraph"; import { ElementCompact } from "xml-js"; -import { PatchDocumentOptions } from "./from-docx"; +import { IPatch } from "./from-docx"; -export const replacer = (json: ElementCompact, options: PatchDocumentOptions): ElementCompact => { +export const replacer = (json: ElementCompact, options: IPatch): ElementCompact => { for (const child of options.children) { if (child instanceof Paragraph) { console.log("is para"); diff --git a/src/templater/run-renderer.ts b/src/templater/run-renderer.ts new file mode 100644 index 0000000000..6efc7a04b9 --- /dev/null +++ b/src/templater/run-renderer.ts @@ -0,0 +1,78 @@ +import { Element } from "xml-js"; + +export interface IRenderedParagraphNode { + readonly text: string; + readonly runs: readonly IRenderedRunNode[]; +} + +interface IParts { + readonly text: string; + readonly index: number; +} + +export interface IRenderedRunNode { + readonly text: string; + readonly parts: readonly IParts[]; + readonly index: number; +} + +export const renderParagraphNode = (node: Element): IRenderedParagraphNode => { + if (node.name !== "w:p") { + throw new Error(`Invalid node type: ${node.name}`); + } + + if (!node.elements) { + return { + text: "", + runs: [], + }; + } + + const runs = node.elements + .map((element, i) => ({ element, i })) + .filter(({ element }) => element.name === "w:r") + .map(({ element, i }) => renderRunNode(element, i)) + .filter((e) => !!e) + .map((e) => e as IRenderedRunNode); + + const text = runs.reduce((acc, curr) => acc + curr.text, ""); + + return { + text, + runs, + }; +}; + +const renderRunNode = (node: Element, index: number): IRenderedRunNode => { + if (node.name !== "w:r") { + throw new Error(`Invalid node type: ${node.name}`); + } + + if (!node.elements) { + return { + text: "", + parts: [], + index: -1, + }; + } + + const parts = node.elements + .map((element, i: number) => + element.name === "w:t" && element.elements + ? { + text: element.elements[0].text?.toString() ?? "", + index: i, + } + : undefined, + ) + .filter((e) => !!e) + .map((e) => e as IParts); + + const text = parts.reduce((acc, curr) => acc + curr.text, ""); + + return { + text, + parts, + index, + }; +}; diff --git a/src/templater/traverser.ts b/src/templater/traverser.ts new file mode 100644 index 0000000000..61abf5f8d6 --- /dev/null +++ b/src/templater/traverser.ts @@ -0,0 +1,43 @@ +import { Element } from "xml-js"; +import { IRenderedParagraphNode, renderParagraphNode } from "./run-renderer"; + +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; +} + +export const findLocationOfText = (node: Element, text: string): void => { + let renderedParagraphs: readonly IRenderedParagraphNode[] = []; + + // eslint-disable-next-line functional/prefer-readonly-type + const queue: Element[] = [...(node.elements ?? [])]; + + // eslint-disable-next-line functional/immutable-data + let currentNode: Element | undefined; + while (queue.length > 0) { + // eslint-disable-next-line functional/immutable-data + currentNode = queue.shift(); + + if (!currentNode) { + break; + } + + if (currentNode.name === "w:p") { + renderedParagraphs = [...renderedParagraphs, renderParagraphNode(currentNode)]; + } else { + // eslint-disable-next-line functional/immutable-data + queue.push(...(currentNode.elements ?? [])); + } + } + + const filteredParagraphs = renderedParagraphs.filter((p) => p.text.includes(text)); + + console.log("paragrapghs", JSON.stringify(filteredParagraphs, null, 2)); + return undefined; +}; From 5233b4b5e626ef56ad09a0c138e6b2191f27d54a Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Sat, 18 Feb 2023 20:36:24 +0000 Subject: [PATCH 04/26] 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; +}; From a4d96bbf6e0c9723a5c8d71734f48173c2bb2655 Mon Sep 17 00:00:00 2001 From: Dolan Date: Thu, 23 Feb 2023 19:43:19 +0000 Subject: [PATCH 05/26] Add paragraph token replacer --- src/templater/paragraph-token-replacer.ts | 72 +++++++++++++++++++++++ src/templater/replacer.ts | 42 ++++++++++--- 2 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 src/templater/paragraph-token-replacer.ts diff --git a/src/templater/paragraph-token-replacer.ts b/src/templater/paragraph-token-replacer.ts new file mode 100644 index 0000000000..52c06892f5 --- /dev/null +++ b/src/templater/paragraph-token-replacer.ts @@ -0,0 +1,72 @@ +import { Element } from "xml-js"; +import * as xml from "xml"; + +import { Formatter } from "@export/formatter"; +import { Text } from "@file/paragraph/run/run-components/text"; + +import { toJson } from "./util"; +import { IRenderedParagraphNode } from "./run-renderer"; + +enum ReplaceMode { + START, + MIDDLE, + END, +} + +const formatter = new Formatter(); + +export const replaceTokenInParagraphElement = ({ + paragraphElement, + renderedParagraph, + originalText, + replacementText, +}: { + readonly paragraphElement: Element; + readonly renderedParagraph: IRenderedParagraphNode; + readonly originalText: string; + readonly replacementText: string; +}): Element => { + const startIndex = renderedParagraph.text.indexOf(originalText); + const endIndex = startIndex + originalText.length - 1; + + let replaceMode = ReplaceMode.START; + + for (const run of renderedParagraph.runs) { + for (const { text, index, start, end } of run.parts) { + switch (replaceMode) { + case ReplaceMode.START: + if (startIndex >= start) { + const partToReplace = run.text.substring(Math.max(startIndex, start), Math.min(endIndex, end) + 1); + // We use a token to split the text if the replacement is within the same run + // If not, we just add text to the middle of the run later + const firstPart = text.replace(partToReplace, replacementText); + patchTextElement(paragraphElement.elements![run.index].elements![index], firstPart); + replaceMode = ReplaceMode.MIDDLE; + continue; + } + break; + case ReplaceMode.MIDDLE: + if (endIndex <= end) { + const lastPart = text.substring(endIndex - start + 1); + patchTextElement(paragraphElement.elements![run.index].elements![index], lastPart); + replaceMode = ReplaceMode.END; + } else { + patchTextElement(paragraphElement.elements![run.index].elements![index], ""); + } + break; + default: + } + } + } + + return paragraphElement; +}; + +const patchTextElement = (element: Element, text: string): Element => { + const textJson = toJson(xml(formatter.format(new Text({ text })))); + + // eslint-disable-next-line functional/immutable-data + element.elements = textJson.elements; + + return element; +}; diff --git a/src/templater/replacer.ts b/src/templater/replacer.ts index 8a42327d9f..5dfacc3a2a 100644 --- a/src/templater/replacer.ts +++ b/src/templater/replacer.ts @@ -1,27 +1,51 @@ -import { Formatter } from "@export/formatter"; -import { Paragraph, TextRun } from "@file/paragraph"; import { Element } from "xml-js"; import * as xml from "xml"; +import { Formatter } from "@export/formatter"; +import { Paragraph, TextRun } from "@file/paragraph"; + import { IPatch } from "./from-docx"; import { toJson } from "./util"; import { IRenderedParagraphNode } from "./run-renderer"; +import { replaceTokenInParagraphElement } from "./paragraph-token-replacer"; 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) { - 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); + for (const renderedParagraph of renderedParagraphs) { + const textJson = toJson(xml(formatter.format(child))); + 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 + + console.log("hard case"); + console.log("paragraphElement", JSON.stringify(paragraphElement, null, 2)); + + replaceTokenInParagraphElement({ + paragraphElement, + renderedParagraph, + originalText: options.text, + replacementText: SPLIT_TOKEN, + }); + + console.log("paragraphElement after", JSON.stringify(paragraphElement, null, 2)); + } + } } } From f3dc1f071257914166ded96d01f171884f42e935 Mon Sep 17 00:00:00 2001 From: Dolan Date: Fri, 24 Feb 2023 01:05:43 +0000 Subject: [PATCH 06/26] Work on replacer logic --- demo/85-template-document.ts | 12 +++++ src/templater/paragraph-split-inject.ts | 56 +++++++++++++++++++++++ src/templater/paragraph-token-replacer.ts | 12 +---- src/templater/replacer.ts | 11 +++-- src/templater/run-renderer.ts | 2 +- src/templater/util.ts | 13 ++++++ 6 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 src/templater/paragraph-split-inject.ts diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index 5baa2397f4..4e7608009f 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -9,6 +9,18 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { children: [new TextRun("John Doe")], text: "{{ name }}", }, + { + children: [new TextRun("Heading wow!")], + text: "{{ table_heading_1 }}", + }, + { + children: [new TextRun("#657")], + text: "{{ item_1 }}", + }, + { + children: [new TextRun("Lorem ipsum paragraph")], + text: "{{ paragraph_replace }}", + }, ], }).then((doc) => { fs.writeFileSync("My Document.docx", doc); diff --git a/src/templater/paragraph-split-inject.ts b/src/templater/paragraph-split-inject.ts new file mode 100644 index 0000000000..23a68159c8 --- /dev/null +++ b/src/templater/paragraph-split-inject.ts @@ -0,0 +1,56 @@ +import { Element } from "xml-js"; +import { createTextElementContents } from "./util"; + +export const findRunElementIndexWithToken = (paragraphElement: Element, token: string): number => { + for (let i = 0; i < (paragraphElement.elements ?? []).length; i++) { + const element = paragraphElement.elements![i]; + if (element.type === "element" && element.name === "w:r") { + const textElement = (element.elements ?? []).filter((e) => e.type === "element" && e.name === "w:t"); + + for (const text of textElement) { + if ((text.elements?.[0].text as string)?.includes(token)) { + return i; + } + } + } + } + // return -1; + throw new Error("Token not found"); +}; + +export const splitRunElement = (runElement: Element, token: string): { readonly left: Element; readonly right: Element } => { + let splitIndex = 0; + + const splitElements = + runElement.elements + ?.map((e, i) => { + if (e.type === "element" && e.name === "w:t") { + const text = e.elements?.[0].text as string; + const splitText = text.split(token); + const newElements = splitText.map((t) => ({ + ...e, + attributes: { + "xml:space": "preserve", + }, + elements: createTextElementContents(t), + })); + splitIndex = i; + return newElements; + } else { + return e; + } + }) + .flat() ?? []; + + const leftRunElement: Element = { + ...JSON.parse(JSON.stringify(runElement)), + elements: splitElements.slice(0, splitIndex + 1), + }; + + const rightRunElement: Element = { + ...JSON.parse(JSON.stringify(runElement)), + elements: splitElements.slice(splitIndex + 1), + }; + + return { left: leftRunElement, right: rightRunElement }; +}; diff --git a/src/templater/paragraph-token-replacer.ts b/src/templater/paragraph-token-replacer.ts index 52c06892f5..45fb2bc302 100644 --- a/src/templater/paragraph-token-replacer.ts +++ b/src/templater/paragraph-token-replacer.ts @@ -1,10 +1,6 @@ import { Element } from "xml-js"; -import * as xml from "xml"; -import { Formatter } from "@export/formatter"; -import { Text } from "@file/paragraph/run/run-components/text"; - -import { toJson } from "./util"; +import { createTextElementContents } from "./util"; import { IRenderedParagraphNode } from "./run-renderer"; enum ReplaceMode { @@ -13,8 +9,6 @@ enum ReplaceMode { END, } -const formatter = new Formatter(); - export const replaceTokenInParagraphElement = ({ paragraphElement, renderedParagraph, @@ -63,10 +57,8 @@ export const replaceTokenInParagraphElement = ({ }; const patchTextElement = (element: Element, text: string): Element => { - const textJson = toJson(xml(formatter.format(new Text({ text })))); - // eslint-disable-next-line functional/immutable-data - element.elements = textJson.elements; + element.elements = createTextElementContents(text); return element; }; diff --git a/src/templater/replacer.ts b/src/templater/replacer.ts index 5dfacc3a2a..e40d66ca92 100644 --- a/src/templater/replacer.ts +++ b/src/templater/replacer.ts @@ -8,6 +8,7 @@ import { IPatch } from "./from-docx"; import { toJson } from "./util"; import { IRenderedParagraphNode } from "./run-renderer"; import { replaceTokenInParagraphElement } from "./paragraph-token-replacer"; +import { findRunElementIndexWithToken, splitRunElement } from "./paragraph-split-inject"; const formatter = new Formatter(); @@ -18,7 +19,6 @@ export const replacer = (json: Element, options: IPatch, renderedParagraphs: rea if (child instanceof Paragraph) { console.log("is para"); } else if (child instanceof TextRun) { - console.log("paragrapghs", JSON.stringify(renderedParagraphs, null, 2)); for (const renderedParagraph of renderedParagraphs) { const textJson = toJson(xml(formatter.format(child))); const paragraphElement = goToElementFromPath(json, renderedParagraph.path); @@ -33,9 +33,6 @@ export const replacer = (json: Element, options: IPatch, renderedParagraphs: rea } else { // Hard case where the text is only part of the paragraph - console.log("hard case"); - console.log("paragraphElement", JSON.stringify(paragraphElement, null, 2)); - replaceTokenInParagraphElement({ paragraphElement, renderedParagraph, @@ -43,6 +40,12 @@ export const replacer = (json: Element, options: IPatch, renderedParagraphs: rea replacementText: 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)); } } diff --git a/src/templater/run-renderer.ts b/src/templater/run-renderer.ts index b0e6132783..344898c10a 100644 --- a/src/templater/run-renderer.ts +++ b/src/templater/run-renderer.ts @@ -82,7 +82,7 @@ const renderRunNode = (node: Element, index: number, currentRunStringIndex: numb const parts = node.elements .map((element, i: number) => - element.name === "w:t" && element.elements + element.name === "w:t" && element.elements && element.elements.length > 0 ? { text: element.elements[0].text?.toString() ?? "", index: i, diff --git a/src/templater/util.ts b/src/templater/util.ts index bea0b16c1d..f385a00025 100644 --- a/src/templater/util.ts +++ b/src/templater/util.ts @@ -1,6 +1,19 @@ import { xml2js, Element } from "xml-js"; +import * as xml from "xml"; + +import { Formatter } from "@export/formatter"; +import { Text } from "@file/paragraph/run/run-components/text"; + +const formatter = new Formatter(); export const toJson = (xmlData: string): Element => { const xmlObj = xml2js(xmlData, { compact: false }) as Element; return xmlObj; }; + +// eslint-disable-next-line functional/prefer-readonly-type +export const createTextElementContents = (text: string): Element[] => { + const textJson = toJson(xml(formatter.format(new Text({ text })))); + + return textJson.elements![0].elements ?? []; +}; From c37d9ca5b3265639226940cfc5260aa460be24bf Mon Sep 17 00:00:00 2001 From: Dolan Date: Fri, 24 Feb 2023 01:44:53 +0000 Subject: [PATCH 07/26] Add ts-node to tsconfig --- tsconfig.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tsconfig.json b/tsconfig.json index 53cf5a12fe..41262ad1d5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,5 +20,10 @@ "@shared": ["./shared/index.ts"] } }, + "ts-node": { + "compilerOptions": { + "module": "commonjs" + } + }, "include": ["src"] } From 8c0fe00c6f9f9edd5d4836c5db865372062fe6c8 Mon Sep 17 00:00:00 2001 From: Dolan Date: Fri, 24 Feb 2023 02:55:53 +0000 Subject: [PATCH 08/26] Add launch json --- .vscode/launch.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..ef4e0bdeb5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Demo", + "type": "node", + "request": "launch", + "runtimeArgs": [ + "-r", + "${workspaceFolder}/node_modules/ts-node/register", + "-r", + "${workspaceFolder}/node_modules/tsconfig-paths/register" + ], + "cwd": "${workspaceRoot}", + "program": "${workspaceFolder}/demo/85-template-document.ts" + } + ] +} From f8f5d43b0c26dc424711250a9a7231d77e799676 Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Sat, 25 Feb 2023 19:33:12 +0000 Subject: [PATCH 09/26] Allow for Paragraph and array patching --- demo/85-template-document.ts | 87 +++++++++++++++++++++++++++++-- demo/assets/simple-template.docx | Bin 12624 -> 13057 bytes src/templater/from-docx.ts | 25 +++++++-- src/templater/replacer.ts | 72 ++++++++++++++----------- 4 files changed, 148 insertions(+), 36 deletions(-) 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 1ff29bc52974b12438f3e01b325c0a84c6113f19..c926191881f5028f5e816be14089314c6924c787 100644 GIT binary patch delta 7990 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 B{<8o8 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]; From 3f6c006716ca0f3ca7b71c4a7ebbc0bda623b81f Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Sat, 25 Feb 2023 19:34:04 +0000 Subject: [PATCH 10/26] Allow for it to work with all xml files under word/ --- demo/85-template-document.ts | 10 ++++++++++ demo/assets/simple-template.docx | Bin 13057 -> 19659 bytes src/templater/from-docx.ts | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index 0c95599e84..b4f674b967 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -36,6 +36,16 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { children: [new Paragraph("Lorem ipsum paragraph"), new Paragraph("Another paragraph")], text: "{{ paragraph_replace }}", }, + { + type: PatchType.PARAGRAPH, + children: [new TextRun("Delightful Header")], + text: "{{ header_adjective }}", + }, + { + type: PatchType.PARAGRAPH, + children: [new TextRun("replaced just as well")], + text: "{{ footer_text }}", + }, { type: PatchType.DOCUMENT, children: [ diff --git a/demo/assets/simple-template.docx b/demo/assets/simple-template.docx index c926191881f5028f5e816be14089314c6924c787..dab8b9e7c235bc4aa16db95fd84d816a3422952c 100644 GIT binary patch delta 14137 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 ATmS$7 delta 7484 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Ξ_s+ig+l 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 diff --git a/src/templater/from-docx.ts b/src/templater/from-docx.ts index 4117e04917..e380d9973e 100644 --- a/src/templater/from-docx.ts +++ b/src/templater/from-docx.ts @@ -41,7 +41,7 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO for (const [key, value] of Object.entries(zipContent.files)) { const json = toJson(await value.async("text")); - if (key === "word/document.xml") { + if (key.startsWith("word/")) { for (const patch of options.patches) { const renderedParagraphs = findLocationOfText(json, patch.text); replacer(json, patch, renderedParagraphs); From 4e875b47445f6c87de4c3f809c203c2358a37241 Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Sat, 25 Feb 2023 20:18:00 +0000 Subject: [PATCH 11/26] Re-name templater to patcher --- src/index.ts | 2 +- src/{templater => patcher}/from-docx.ts | 0 src/{templater => patcher}/index.ts | 0 src/{templater => patcher}/paragraph-split-inject.ts | 0 src/{templater => patcher}/paragraph-token-replacer.ts | 0 src/{templater => patcher}/replacer.ts | 0 src/{templater => patcher}/run-renderer.ts | 0 src/{templater => patcher}/traverser.ts | 0 src/{templater => patcher}/util.ts | 0 9 files changed, 1 insertion(+), 1 deletion(-) rename src/{templater => patcher}/from-docx.ts (100%) rename src/{templater => patcher}/index.ts (100%) rename src/{templater => patcher}/paragraph-split-inject.ts (100%) rename src/{templater => patcher}/paragraph-token-replacer.ts (100%) rename src/{templater => patcher}/replacer.ts (100%) rename src/{templater => patcher}/run-renderer.ts (100%) rename src/{templater => patcher}/traverser.ts (100%) rename src/{templater => patcher}/util.ts (100%) diff --git a/src/index.ts b/src/index.ts index 7162ae5a1f..40b344721b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,4 +5,4 @@ export * from "./file"; export * from "./export"; export * from "./import-dotx"; export * from "./util"; -export * from "./templater"; +export * from "./patcher"; diff --git a/src/templater/from-docx.ts b/src/patcher/from-docx.ts similarity index 100% rename from src/templater/from-docx.ts rename to src/patcher/from-docx.ts diff --git a/src/templater/index.ts b/src/patcher/index.ts similarity index 100% rename from src/templater/index.ts rename to src/patcher/index.ts diff --git a/src/templater/paragraph-split-inject.ts b/src/patcher/paragraph-split-inject.ts similarity index 100% rename from src/templater/paragraph-split-inject.ts rename to src/patcher/paragraph-split-inject.ts diff --git a/src/templater/paragraph-token-replacer.ts b/src/patcher/paragraph-token-replacer.ts similarity index 100% rename from src/templater/paragraph-token-replacer.ts rename to src/patcher/paragraph-token-replacer.ts diff --git a/src/templater/replacer.ts b/src/patcher/replacer.ts similarity index 100% rename from src/templater/replacer.ts rename to src/patcher/replacer.ts diff --git a/src/templater/run-renderer.ts b/src/patcher/run-renderer.ts similarity index 100% rename from src/templater/run-renderer.ts rename to src/patcher/run-renderer.ts diff --git a/src/templater/traverser.ts b/src/patcher/traverser.ts similarity index 100% rename from src/templater/traverser.ts rename to src/patcher/traverser.ts diff --git a/src/templater/util.ts b/src/patcher/util.ts similarity index 100% rename from src/templater/util.ts rename to src/patcher/util.ts From ce485dbc29d39ada9569cd672967bd3756411da5 Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Sat, 25 Feb 2023 22:18:31 +0000 Subject: [PATCH 12/26] Simplify patcher Use map notation --- demo/85-template-document.ts | 28 ++-- demo/86-generate-template.ts | 20 +++ demo/assets/simple-template.docx | Bin 19659 -> 16661 bytes package-lock.json | 212 ------------------------------- src/patcher/from-docx.ts | 13 +- src/patcher/replacer.ts | 47 +++---- 6 files changed, 52 insertions(+), 268 deletions(-) create mode 100644 demo/86-generate-template.ts diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index b4f674b967..b36e6be4c3 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -1,4 +1,4 @@ -// Simple template example +// Patch a document with patches // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; import { @@ -15,38 +15,32 @@ import { } from "../src"; patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { - patches: [ - { + patches: { + "name":{ type: PatchType.PARAGRAPH, children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")], - text: "{{ name }}", }, - { + "table_heading_1": { type: PatchType.PARAGRAPH, children: [new TextRun("Heading wow!")], - text: "{{ table_heading_1 }}", }, - { + "item_1": { type: PatchType.PARAGRAPH, children: [new TextRun("#657")], - text: "{{ item_1 }}", }, - { + "paragraph_replace": { type: PatchType.DOCUMENT, children: [new Paragraph("Lorem ipsum paragraph"), new Paragraph("Another paragraph")], - text: "{{ paragraph_replace }}", }, - { + "header_adjective": { type: PatchType.PARAGRAPH, children: [new TextRun("Delightful Header")], - text: "{{ header_adjective }}", }, - { + "footer_text": { type: PatchType.PARAGRAPH, children: [new TextRun("replaced just as well")], - text: "{{ footer_text }}", }, - { + "table": { type: PatchType.DOCUMENT, children: [ new Table({ @@ -108,11 +102,11 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { ], }), ], + }), ], - text: "{{ table }}", }, - ], + }, }).then((doc) => { fs.writeFileSync("My Document.docx", doc); }); diff --git a/demo/86-generate-template.ts b/demo/86-generate-template.ts new file mode 100644 index 0000000000..4af0a2dfd8 --- /dev/null +++ b/demo/86-generate-template.ts @@ -0,0 +1,20 @@ +// Generate a template document +// Import from 'docx' rather than '../build' if you install from npm +import * as fs from "fs"; +import { Document, Packer, Paragraph, TextRun } from "../build"; + +const doc = new Document({ + sections: [ + { + children: [ + new Paragraph({ + children: [new TextRun("{{ template }}")], + }), + ], + }, + ], +}); + +Packer.toBuffer(doc).then((buffer) => { + fs.writeFileSync("My Document.docx", buffer); +}); diff --git a/demo/assets/simple-template.docx b/demo/assets/simple-template.docx index dab8b9e7c235bc4aa16db95fd84d816a3422952c..83f972c48acaf56cb49f81a80939785c46e028c8 100644 GIT binary patch delta 7459 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 diff --git a/package-lock.json b/package-lock.json index 2b5728e020..13f3f32f2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1669,23 +1669,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz", - "integrity": "sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/visitor-keys": "5.51.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/type-utils": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.52.0.tgz", @@ -1826,102 +1809,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/@typescript-eslint/types": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.51.0.tgz", - "integrity": "sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz", - "integrity": "sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/visitor-keys": "5.51.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/utils": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.52.0.tgz", @@ -2078,23 +1965,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz", - "integrity": "sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.51.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -13041,16 +12911,6 @@ } } }, - "@typescript-eslint/scope-manager": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz", - "integrity": "sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/visitor-keys": "5.51.0" - } - }, "@typescript-eslint/type-utils": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.52.0.tgz", @@ -13135,68 +12995,6 @@ } } }, - "@typescript-eslint/types": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.51.0.tgz", - "integrity": "sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz", - "integrity": "sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.51.0", - "@typescript-eslint/visitor-keys": "5.51.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, "@typescript-eslint/utils": { "version": "5.52.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.52.0.tgz", @@ -13295,16 +13093,6 @@ } } }, - "@typescript-eslint/visitor-keys": { - "version": "5.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz", - "integrity": "sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.51.0", - "eslint-visitor-keys": "^3.3.0" - } - }, "@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", diff --git a/src/patcher/from-docx.ts b/src/patcher/from-docx.ts index e380d9973e..0dd38f91c3 100644 --- a/src/patcher/from-docx.ts +++ b/src/patcher/from-docx.ts @@ -26,12 +26,10 @@ type FilePatch = { readonly children: readonly FileChild[]; }; -export type IPatch = { - readonly text: string; -} & (ParagraphPatch | FilePatch); +export type IPatch = ParagraphPatch | FilePatch; export interface PatchDocumentOptions { - readonly patches: readonly IPatch[]; + readonly patches: { readonly [key: string]: IPatch }; } export const patchDocument = async (data: InputDataType, options: PatchDocumentOptions): Promise => { @@ -42,9 +40,10 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO for (const [key, value] of Object.entries(zipContent.files)) { const json = toJson(await value.async("text")); if (key.startsWith("word/")) { - for (const patch of options.patches) { - const renderedParagraphs = findLocationOfText(json, patch.text); - replacer(json, patch, renderedParagraphs); + for (const [patchKey, patchValue] of Object.entries(options.patches)) { + const patchText = `{{${patchKey}}}`; + const renderedParagraphs = findLocationOfText(json, patchText); + replacer(json, patchValue, patchText, renderedParagraphs); } } diff --git a/src/patcher/replacer.ts b/src/patcher/replacer.ts index 95e607a777..ce6e987d62 100644 --- a/src/patcher/replacer.ts +++ b/src/patcher/replacer.ts @@ -14,48 +14,31 @@ const formatter = new Formatter(); const SPLIT_TOKEN = "ɵ"; -export const replacer = (json: Element, options: IPatch, renderedParagraphs: readonly IRenderedParagraphNode[]): Element => { +export const replacer = (json: Element, patch: IPatch, patchText: string, renderedParagraphs: readonly IRenderedParagraphNode[]): Element => { for (const renderedParagraph of renderedParagraphs) { - const textJson = options.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]); - if (options.type === PatchType.DOCUMENT) { + if (patch.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) { + } else if (patch.type === PatchType.PARAGRAPH) { + // Hard case where the text is only part of the 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; - console.log(JSON.stringify(paragraphElement, null, 2)); - } else { - // Hard case where the text is only part of the paragraph + replaceTokenInParagraphElement({ + paragraphElement, + renderedParagraph, + originalText: patchText, + 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, 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); } } From c9d86619de14cff72a0e1136c0405cbbf5abe314 Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Sat, 25 Feb 2023 22:18:56 +0000 Subject: [PATCH 13/26] Clean up --- demo/85-template-document.ts | 15 +++++++-------- demo/86-generate-template.ts | 2 +- demo/assets/generated-template.docx | Bin 0 -> 6684 bytes src/patcher/from-docx.ts | 1 + src/patcher/paragraph-split-inject.ts | 6 ++---- src/patcher/paragraph-token-replacer.ts | 7 ++++++- src/patcher/replacer.ts | 8 ++++++-- src/patcher/traverser.ts | 15 +-------------- src/patcher/util.ts | 7 +++++++ 9 files changed, 31 insertions(+), 30 deletions(-) create mode 100644 demo/assets/generated-template.docx diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index b36e6be4c3..f015dbe7cf 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -16,31 +16,31 @@ import { patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { patches: { - "name":{ + name: { type: PatchType.PARAGRAPH, children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")], }, - "table_heading_1": { + table_heading_1: { type: PatchType.PARAGRAPH, children: [new TextRun("Heading wow!")], }, - "item_1": { + item_1: { type: PatchType.PARAGRAPH, children: [new TextRun("#657")], }, - "paragraph_replace": { + paragraph_replace: { type: PatchType.DOCUMENT, children: [new Paragraph("Lorem ipsum paragraph"), new Paragraph("Another paragraph")], }, - "header_adjective": { + header_adjective: { type: PatchType.PARAGRAPH, children: [new TextRun("Delightful Header")], }, - "footer_text": { + footer_text: { type: PatchType.PARAGRAPH, children: [new TextRun("replaced just as well")], }, - "table": { + table: { type: PatchType.DOCUMENT, children: [ new Table({ @@ -102,7 +102,6 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { ], }), ], - }), ], }, diff --git a/demo/86-generate-template.ts b/demo/86-generate-template.ts index 4af0a2dfd8..e16060f6a6 100644 --- a/demo/86-generate-template.ts +++ b/demo/86-generate-template.ts @@ -8,7 +8,7 @@ const doc = new Document({ { children: [ new Paragraph({ - children: [new TextRun("{{ template }}")], + children: [new TextRun("{{template}}")], }), ], }, diff --git a/demo/assets/generated-template.docx b/demo/assets/generated-template.docx new file mode 100644 index 0000000000000000000000000000000000000000..f7244e0c4b55308315f54489957fb7fbf7ee7378 GIT binary patch literal 6684 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 literal 0 HcmV?d00001 diff --git a/src/patcher/from-docx.ts b/src/patcher/from-docx.ts index 0dd38f91c3..535336e70d 100644 --- a/src/patcher/from-docx.ts +++ b/src/patcher/from-docx.ts @@ -43,6 +43,7 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO for (const [patchKey, patchValue] of Object.entries(options.patches)) { const patchText = `{{${patchKey}}}`; const renderedParagraphs = findLocationOfText(json, patchText); + // TODO: mutates json. Make it immutable replacer(json, patchValue, patchText, renderedParagraphs); } } diff --git a/src/patcher/paragraph-split-inject.ts b/src/patcher/paragraph-split-inject.ts index 23a68159c8..d3fe8d5e6c 100644 --- a/src/patcher/paragraph-split-inject.ts +++ b/src/patcher/paragraph-split-inject.ts @@ -1,5 +1,5 @@ import { Element } from "xml-js"; -import { createTextElementContents } from "./util"; +import { createTextElementContents, patchSpaceAttribute } from "./util"; export const findRunElementIndexWithToken = (paragraphElement: Element, token: string): number => { 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 newElements = splitText.map((t) => ({ ...e, - attributes: { - "xml:space": "preserve", - }, + ...patchSpaceAttribute(e), elements: createTextElementContents(t), })); splitIndex = i; diff --git a/src/patcher/paragraph-token-replacer.ts b/src/patcher/paragraph-token-replacer.ts index 45fb2bc302..06593c625f 100644 --- a/src/patcher/paragraph-token-replacer.ts +++ b/src/patcher/paragraph-token-replacer.ts @@ -1,6 +1,6 @@ import { Element } from "xml-js"; -import { createTextElementContents } from "./util"; +import { createTextElementContents, patchSpaceAttribute } from "./util"; import { IRenderedParagraphNode } from "./run-renderer"; enum ReplaceMode { @@ -43,6 +43,11 @@ export const replaceTokenInParagraphElement = ({ if (endIndex <= end) { const lastPart = text.substring(endIndex - start + 1); 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; } else { patchTextElement(paragraphElement.elements![run.index].elements![index], ""); diff --git a/src/patcher/replacer.ts b/src/patcher/replacer.ts index ce6e987d62..488b59760d 100644 --- a/src/patcher/replacer.ts +++ b/src/patcher/replacer.ts @@ -14,7 +14,12 @@ const formatter = new Formatter(); 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) { 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 parentElement.elements?.splice(elementIndex, 1, ...textJson); } else if (patch.type === PatchType.PARAGRAPH) { - // Hard case where the text is only part of the paragraph const paragraphElement = goToElementFromPath(json, renderedParagraph.path); replaceTokenInParagraphElement({ diff --git a/src/patcher/traverser.ts b/src/patcher/traverser.ts index 95f7829042..5ee2f792ff 100644 --- a/src/patcher/traverser.ts +++ b/src/patcher/traverser.ts @@ -8,17 +8,6 @@ export interface ElementWrapper { 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[] => wrapper.element.elements?.map((e, i) => ({ 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 filteredParagraphs; + return renderedParagraphs.filter((p) => p.text.includes(text)); }; diff --git a/src/patcher/util.ts b/src/patcher/util.ts index f385a00025..bdb79eb0d0 100644 --- a/src/patcher/util.ts +++ b/src/patcher/util.ts @@ -17,3 +17,10 @@ export const createTextElementContents = (text: string): Element[] => { return textJson.elements![0].elements ?? []; }; + +export const patchSpaceAttribute = (element: Element): Element => ({ + ...element, + attributes: { + "xml:space": "preserve", + }, +}); From fe9b438a5169c0c8ca5ca130171425785a4327b6 Mon Sep 17 00:00:00 2001 From: Dolan Date: Mon, 27 Feb 2023 21:06:54 +0000 Subject: [PATCH 14/26] Update documentation --- docs/contribution-guidelines.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/contribution-guidelines.md b/docs/contribution-guidelines.md index dede758ecf..9f53eff40c 100644 --- a/docs/contribution-guidelines.md +++ b/docs/contribution-guidelines.md @@ -1,11 +1,20 @@ # Contribution Guidelines -- Include documentation reference(s) at the top of each file: +- Include documentation reference(s) at the top of each file as a comment. For example: ```ts // http://officeopenxml.com/WPdocument.php ``` + It can be a link to `officeopenxml.com` or `datypic.com` etc. + It could also be a reference to the official ECMA-376 standard: https://www.ecma-international.org/publications-and-standards/standards/ecma-376/ + +- Include a portion of the schema as a comment for cross reference. For example: + + ```ts + // + ``` + - Follow Prettier standards, and consider using the [Prettier VSCode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) plugin. - Follow the `ESLint` rules From 66a1992da0cd7f79bdb8f41af01563fece3d865d Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Fri, 3 Mar 2023 23:47:50 +0000 Subject: [PATCH 15/26] Add ImageRun feature --- demo/85-template-document.ts | 5 ++ demo/assets/simple-template.docx | Bin 16661 -> 16565 bytes src/export/packer/next-compiler.ts | 5 +- src/file/media/media.ts | 1 + src/patcher/content-types-manager.ts | 16 ++++++ src/patcher/from-docx.ts | 82 +++++++++++++++++++++++++-- src/patcher/relationship-manager.ts | 30 ++++++++++ src/patcher/replacer.ts | 9 ++- src/patcher/util.ts | 4 ++ 9 files changed, 141 insertions(+), 11 deletions(-) create mode 100644 src/patcher/content-types-manager.ts create mode 100644 src/patcher/relationship-manager.ts diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index f015dbe7cf..921a9befbb 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -3,6 +3,7 @@ import * as fs from "fs"; import { HeadingLevel, + ImageRun, Paragraph, patchDocument, PatchType, @@ -40,6 +41,10 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { type: PatchType.PARAGRAPH, children: [new TextRun("replaced just as well")], }, + image_test: { + type: PatchType.PARAGRAPH, + children: [new ImageRun({ data: fs.readFileSync("./demo/images/image1.jpeg"), transformation: { width: 100, height: 100 } })], + }, table: { type: PatchType.DOCUMENT, children: [ diff --git a/demo/assets/simple-template.docx b/demo/assets/simple-template.docx index 83f972c48acaf56cb49f81a80939785c46e028c8..a3e71cdf6f1fb731a01ebae47ec08adcba662d77 100644 GIT binary patch delta 6473 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 } + // TODO: Unused public addMedia(data: Buffer | string | Uint8Array | ArrayBuffer, transformation: IMediaTransformation): IMediaData { const key = `${uniqueId()}.png`; diff --git a/src/patcher/content-types-manager.ts b/src/patcher/content-types-manager.ts new file mode 100644 index 0000000000..8032073bfc --- /dev/null +++ b/src/patcher/content-types-manager.ts @@ -0,0 +1,16 @@ +import { Element } from "xml-js"; + +import { getFirstLevelElements } from "./util"; + +export const appendContentType = (element: Element, contentType: string, extension: string): void => { + const relationshipElements = getFirstLevelElements(element, "Types"); + // eslint-disable-next-line functional/immutable-data + relationshipElements.push({ + attributes: { + ContentType: contentType, + Extension: extension, + }, + name: "Default", + type: "element", + }); +}; diff --git a/src/patcher/from-docx.ts b/src/patcher/from-docx.ts index 535336e70d..339ba964ec 100644 --- a/src/patcher/from-docx.ts +++ b/src/patcher/from-docx.ts @@ -3,10 +3,17 @@ import { Element, js2xml } from "xml-js"; import { ParagraphChild } from "@file/paragraph"; import { FileChild } from "@file/file-child"; +import { IMediaData, Media } from "@file/media"; +import { IViewWrapper } from "@file/document-wrapper"; +import { File } from "@file/file"; +import { IContext } from "@file/xml-components"; +import { ImageReplacer } from "@export/packer/image-replacer"; import { replacer } from "./replacer"; import { findLocationOfText } from "./traverser"; import { toJson } from "./util"; +import { appendRelationship, getNextRelationshipIndex } from "./relationship-manager"; +import { appendContentType } from "./content-types-manager"; // eslint-disable-next-line functional/prefer-readonly-type type InputDataType = Buffer | string | number[] | Uint8Array | ArrayBuffer | Blob | NodeJS.ReadableStream; @@ -26,31 +33,94 @@ type FilePatch = { readonly children: readonly FileChild[]; }; +interface IRelationshipReplacement { + readonly key: string; + readonly mediaDatas: readonly IMediaData[]; +} + export type IPatch = ParagraphPatch | FilePatch; export interface PatchDocumentOptions { readonly patches: { readonly [key: string]: IPatch }; } +const imageReplacer = new ImageReplacer(); + export const patchDocument = async (data: InputDataType, options: PatchDocumentOptions): Promise => { const zipContent = await JSZip.loadAsync(data); + const context: IContext = { + file: { + Media: new Media(), + } as unknown as File, + viewWrapper: {} as unknown as IViewWrapper, + stack: [], + }; + const map = new Map(); + // eslint-disable-next-line functional/prefer-readonly-type + const relationshipReplacement: IRelationshipReplacement[] = []; + let hasMedia = false; + for (const [key, value] of Object.entries(zipContent.files)) { const json = toJson(await value.async("text")); - if (key.startsWith("word/")) { + if (key.startsWith("word/") && !key.endsWith(".xml.rels")) { for (const [patchKey, patchValue] of Object.entries(options.patches)) { const patchText = `{{${patchKey}}}`; const renderedParagraphs = findLocationOfText(json, patchText); // TODO: mutates json. Make it immutable - replacer(json, patchValue, patchText, renderedParagraphs); + replacer(json, patchValue, patchText, renderedParagraphs, context); + } + + const mediaDatas = imageReplacer.getMediaData(JSON.stringify(json), context.file.Media); + if (mediaDatas.length > 0) { + hasMedia = true; + // eslint-disable-next-line functional/immutable-data + relationshipReplacement.push({ + key, + mediaDatas, + }); } } map.set(key, json); } + for (const { key, mediaDatas } of relationshipReplacement) { + // eslint-disable-next-line functional/immutable-data + const relationshipsJson = map.get(`word/_rels/${key.split("/").pop()}.rels`); + + if (relationshipsJson) { + const index = getNextRelationshipIndex(relationshipsJson); + const newJson = imageReplacer.replace(JSON.stringify(map.get(key)), mediaDatas, index); + map.set(key, JSON.parse(newJson) as Element); + + for (const { fileName } of mediaDatas) { + appendRelationship( + relationshipsJson, + index, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${fileName}`, + ); + } + } + } + + if (hasMedia) { + const contentTypesJson = map.get("[Content_Types].xml"); + + if (!contentTypesJson) { + throw new Error("Could not find content types file"); + } + + appendContentType(contentTypesJson, "image/png", "png"); + appendContentType(contentTypesJson, "image/jpeg", "jpeg"); + appendContentType(contentTypesJson, "image/jpeg", "jpg"); + appendContentType(contentTypesJson, "image/bmp", "bmp"); + appendContentType(contentTypesJson, "image/gif", "gif"); + } + const zip = new JSZip(); for (const [key, value] of map) { @@ -59,13 +129,15 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO zip.file(key, output); } - const zipData = await zip.generateAsync({ + for (const { stream, fileName } of context.file.Media.Array) { + zip.file(`word/media/${fileName}`, stream); + } + + return zip.generateAsync({ type: "nodebuffer", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", compression: "DEFLATE", }); - - return zipData; }; const toXml = (jsonObj: Element): string => { diff --git a/src/patcher/relationship-manager.ts b/src/patcher/relationship-manager.ts new file mode 100644 index 0000000000..30f3bd425e --- /dev/null +++ b/src/patcher/relationship-manager.ts @@ -0,0 +1,30 @@ +import { Element } from "xml-js"; + +import { RelationshipType } from "@file/relationships/relationship/relationship"; +import { getFirstLevelElements } from "./util"; + +const getIdFromRelationshipId = (relationshipId: string): number => parseInt(relationshipId.substring(3), 10); + +export const getNextRelationshipIndex = (relationships: Element): number => { + const relationshipElements = getFirstLevelElements(relationships, "Relationships"); + + return ( + (relationshipElements + .map((e) => getIdFromRelationshipId(e.attributes?.Id?.toString() ?? "")) + .reduce((acc, curr) => Math.max(acc, curr), 0) ?? 0) + 1 + ); +}; + +export const appendRelationship = (relationships: Element, id: number, type: RelationshipType, target: string): void => { + const relationshipElements = getFirstLevelElements(relationships, "Relationships"); + // eslint-disable-next-line functional/immutable-data + relationshipElements.push({ + attributes: { + Id: `rId${id}`, + Type: type, + Target: target, + }, + name: "Relationship", + type: "element", + }); +}; diff --git a/src/patcher/replacer.ts b/src/patcher/replacer.ts index 488b59760d..45861de8e7 100644 --- a/src/patcher/replacer.ts +++ b/src/patcher/replacer.ts @@ -2,7 +2,7 @@ import { Element } from "xml-js"; import * as xml from "xml"; import { Formatter } from "@export/formatter"; -import { XmlComponent } from "@file/xml-components"; +import { IContext, XmlComponent } from "@file/xml-components"; import { IPatch, PatchType } from "./from-docx"; import { toJson } from "./util"; @@ -19,9 +19,13 @@ export const replacer = ( patch: IPatch, patchText: string, renderedParagraphs: readonly IRenderedParagraphNode[], + context: IContext, ): Element => { 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 + // eslint-disable-next-line no-loop-func + .map((c) => toJson(xml(formatter.format(c as XmlComponent, context)))) + .map((c) => c.elements![0]); if (patch.type === PatchType.DOCUMENT) { const parentElement = goToParentElementFromPath(json, renderedParagraph.path); @@ -30,7 +34,6 @@ export const replacer = ( parentElement.elements?.splice(elementIndex, 1, ...textJson); } else if (patch.type === PatchType.PARAGRAPH) { const paragraphElement = goToElementFromPath(json, renderedParagraph.path); - replaceTokenInParagraphElement({ paragraphElement, renderedParagraph, diff --git a/src/patcher/util.ts b/src/patcher/util.ts index bdb79eb0d0..6f9ed57dd0 100644 --- a/src/patcher/util.ts +++ b/src/patcher/util.ts @@ -24,3 +24,7 @@ export const patchSpaceAttribute = (element: Element): Element => ({ "xml:space": "preserve", }, }); + +// eslint-disable-next-line functional/prefer-readonly-type +export const getFirstLevelElements = (relationships: Element, id: string): Element[] => + relationships.elements?.filter((e) => e.name === id)[0].elements ?? []; From 6ad18420e55c3fea1e94efeebeb473c739397d4d Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Sun, 5 Mar 2023 18:46:02 +0000 Subject: [PATCH 16/26] Add hyperlink to demo --- demo/85-template-document.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index 921a9befbb..64d4f0a58b 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -2,6 +2,7 @@ // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; import { + ExternalHyperlink, HeadingLevel, ImageRun, Paragraph, @@ -39,7 +40,19 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { }, footer_text: { type: PatchType.PARAGRAPH, - children: [new TextRun("replaced just as well")], + children: [ + new TextRun("replaced just as"), + new TextRun(" well"), + new ExternalHyperlink({ + children: [ + new TextRun({ + text: "BBC News Link", + style: "Hyperlink", + }), + ], + link: "https://www.bbc.co.uk/news", + }), + ], }, image_test: { type: PatchType.PARAGRAPH, From 0fba450c9ab057c07ff96a9d6f8246449f2320e1 Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Wed, 8 Mar 2023 23:30:51 +0000 Subject: [PATCH 17/26] Allow patching of ExternalHyperlinks --- demo/85-template-document.ts | 13 ++- src/patcher/from-docx.ts | 125 +++++++++++++++++++++++----- src/patcher/relationship-manager.ts | 11 ++- 3 files changed, 125 insertions(+), 24 deletions(-) diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index 64d4f0a58b..2a032586fb 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -28,7 +28,17 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { }, item_1: { type: PatchType.PARAGRAPH, - children: [new TextRun("#657")], + children: [ + new TextRun("#657"), + new ExternalHyperlink({ + children: [ + new TextRun({ + text: "BBC News Link", + }), + ], + link: "https://www.bbc.co.uk/news", + }), + ], }, paragraph_replace: { type: PatchType.DOCUMENT, @@ -47,7 +57,6 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { children: [ new TextRun({ text: "BBC News Link", - style: "Hyperlink", }), ], link: "https://www.bbc.co.uk/news", diff --git a/src/patcher/from-docx.ts b/src/patcher/from-docx.ts index 339ba964ec..c05ccbb45d 100644 --- a/src/patcher/from-docx.ts +++ b/src/patcher/from-docx.ts @@ -1,13 +1,15 @@ import * as JSZip from "jszip"; import { Element, js2xml } from "xml-js"; -import { ParagraphChild } from "@file/paragraph"; +import { ConcreteHyperlink, ExternalHyperlink, ParagraphChild } from "@file/paragraph"; import { FileChild } from "@file/file-child"; import { IMediaData, Media } from "@file/media"; import { IViewWrapper } from "@file/document-wrapper"; import { File } from "@file/file"; import { IContext } from "@file/xml-components"; import { ImageReplacer } from "@export/packer/image-replacer"; +import { TargetModeType } from "@file/relationships/relationship/relationship"; +import { uniqueId } from "@util/convenience-functions"; import { replacer } from "./replacer"; import { findLocationOfText } from "./traverser"; @@ -33,11 +35,16 @@ type FilePatch = { readonly children: readonly FileChild[]; }; -interface IRelationshipReplacement { +interface IImageRelationshipAddition { readonly key: string; readonly mediaDatas: readonly IMediaData[]; } +interface IHyperlinkRelationshipAddition { + readonly key: string; + readonly hyperlink: { readonly id: string; readonly link: string }; +} + export type IPatch = ParagraphPatch | FilePatch; export interface PatchDocumentOptions { @@ -60,7 +67,9 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO const map = new Map(); // eslint-disable-next-line functional/prefer-readonly-type - const relationshipReplacement: IRelationshipReplacement[] = []; + const imageRelationshipAdditions: IImageRelationshipAddition[] = []; + // eslint-disable-next-line functional/prefer-readonly-type + const hyperlinkRelationshipAdditions: IHyperlinkRelationshipAddition[] = []; let hasMedia = false; for (const [key, value] of Object.entries(zipContent.files)) { @@ -70,14 +79,39 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO const patchText = `{{${patchKey}}}`; const renderedParagraphs = findLocationOfText(json, patchText); // TODO: mutates json. Make it immutable - replacer(json, patchValue, patchText, renderedParagraphs, context); + replacer( + json, + { + ...patchValue, + children: patchValue.children.map((element) => { + // We need to replace external hyperlinks with concrete hyperlinks + if (element instanceof ExternalHyperlink) { + const concreteHyperlink = new ConcreteHyperlink(element.options.children, uniqueId()); + // eslint-disable-next-line functional/immutable-data + hyperlinkRelationshipAdditions.push({ + key, + hyperlink: { + id: concreteHyperlink.linkId, + link: element.options.link, + }, + }); + return concreteHyperlink; + } else { + return element; + } + }), + }, + patchText, + renderedParagraphs, + context, + ); } const mediaDatas = imageReplacer.getMediaData(JSON.stringify(json), context.file.Media); if (mediaDatas.length > 0) { hasMedia = true; // eslint-disable-next-line functional/immutable-data - relationshipReplacement.push({ + imageRelationshipAdditions.push({ key, mediaDatas, }); @@ -87,24 +121,55 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO map.set(key, json); } - for (const { key, mediaDatas } of relationshipReplacement) { + for (const { key, mediaDatas } of imageRelationshipAdditions) { // eslint-disable-next-line functional/immutable-data - const relationshipsJson = map.get(`word/_rels/${key.split("/").pop()}.rels`); + const relationshipKey = `word/_rels/${key.split("/").pop()}.rels`; - if (relationshipsJson) { - const index = getNextRelationshipIndex(relationshipsJson); - const newJson = imageReplacer.replace(JSON.stringify(map.get(key)), mediaDatas, index); - map.set(key, JSON.parse(newJson) as Element); - - for (const { fileName } of mediaDatas) { - appendRelationship( - relationshipsJson, - index, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - `media/${fileName}`, - ); - } + if (!map.has(relationshipKey)) { + map.set(relationshipKey, createRelationshipFile()); } + + const relationshipsJson = map.get(relationshipKey); + + if (!relationshipsJson) { + throw new Error("Could not find relationships file"); + } + + const index = getNextRelationshipIndex(relationshipsJson); + const newJson = imageReplacer.replace(JSON.stringify(map.get(key)), mediaDatas, index); + map.set(key, JSON.parse(newJson) as Element); + + for (const { fileName } of mediaDatas) { + appendRelationship( + relationshipsJson, + index, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${fileName}`, + ); + } + } + + for (const { key, hyperlink } of hyperlinkRelationshipAdditions) { + // eslint-disable-next-line functional/immutable-data + const relationshipKey = `word/_rels/${key.split("/").pop()}.rels`; + + if (!map.has(relationshipKey)) { + map.set(relationshipKey, createRelationshipFile()); + } + + const relationshipsJson = map.get(relationshipKey); + + if (!relationshipsJson) { + throw new Error("Could not find relationships file"); + } + + appendRelationship( + relationshipsJson, + hyperlink.id, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", + hyperlink.link, + TargetModeType.EXTERNAL, + ); } if (hasMedia) { @@ -144,3 +209,23 @@ const toXml = (jsonObj: Element): string => { const output = js2xml(jsonObj); return output; }; + +const createRelationshipFile = (): Element => ({ + declaration: { + attributes: { + version: "1.0", + encoding: "UTF-8", + standalone: "yes", + }, + }, + elements: [ + { + type: "element", + name: "Relationships", + attributes: { + xmlns: "http://schemas.openxmlformats.org/package/2006/relationships", + }, + elements: [], + }, + ], +}); diff --git a/src/patcher/relationship-manager.ts b/src/patcher/relationship-manager.ts index 30f3bd425e..35eaab8e71 100644 --- a/src/patcher/relationship-manager.ts +++ b/src/patcher/relationship-manager.ts @@ -1,6 +1,6 @@ import { Element } from "xml-js"; -import { RelationshipType } from "@file/relationships/relationship/relationship"; +import { RelationshipType, TargetModeType } from "@file/relationships/relationship/relationship"; import { getFirstLevelElements } from "./util"; const getIdFromRelationshipId = (relationshipId: string): number => parseInt(relationshipId.substring(3), 10); @@ -15,7 +15,13 @@ export const getNextRelationshipIndex = (relationships: Element): number => { ); }; -export const appendRelationship = (relationships: Element, id: number, type: RelationshipType, target: string): void => { +export const appendRelationship = ( + relationships: Element, + id: number | string, + type: RelationshipType, + target: string, + targetMode?: TargetModeType, +): void => { const relationshipElements = getFirstLevelElements(relationships, "Relationships"); // eslint-disable-next-line functional/immutable-data relationshipElements.push({ @@ -23,6 +29,7 @@ export const appendRelationship = (relationships: Element, id: number, type: Rel Id: `rId${id}`, Type: type, Target: target, + TargetMode: targetMode, }, name: "Relationship", type: "element", From 8ce057e25cd6f3b19e2150524c2ac5495f8323ee Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Mon, 13 Mar 2023 21:35:16 +0000 Subject: [PATCH 18/26] Context per view wrapper --- demo/85-template-document.ts | 18 +++++++++++++++++- src/patcher/from-docx.ts | 33 +++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index 2a032586fb..d3aca499ee 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -42,7 +42,23 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { }, paragraph_replace: { type: PatchType.DOCUMENT, - children: [new Paragraph("Lorem ipsum paragraph"), new Paragraph("Another paragraph")], + children: [ + new Paragraph("Lorem ipsum paragraph"), + new Paragraph("Another paragraph"), + new Paragraph({ + children: [ + new TextRun("This is a "), + new ExternalHyperlink({ + children: [ + new TextRun({ + text: "Google Link", + }), + ], + link: "https://www.google.co.uk", + }), + ], + }), + ], }, header_adjective: { type: PatchType.PARAGRAPH, diff --git a/src/patcher/from-docx.ts b/src/patcher/from-docx.ts index c05ccbb45d..38d45b23c8 100644 --- a/src/patcher/from-docx.ts +++ b/src/patcher/from-docx.ts @@ -56,13 +56,10 @@ const imageReplacer = new ImageReplacer(); export const patchDocument = async (data: InputDataType, options: PatchDocumentOptions): Promise => { const zipContent = await JSZip.loadAsync(data); - const context: IContext = { - file: { - Media: new Media(), - } as unknown as File, - viewWrapper: {} as unknown as IViewWrapper, - stack: [], - }; + const contexts = new Map(); + const file = { + Media: new Media(), + } as unknown as File; const map = new Map(); @@ -75,6 +72,26 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO for (const [key, value] of Object.entries(zipContent.files)) { const json = toJson(await value.async("text")); if (key.startsWith("word/") && !key.endsWith(".xml.rels")) { + const context: IContext = { + file, + viewWrapper: { + Relationships: { + createRelationship: (linkId: string, _: string, target: string, __: TargetModeType) => { + // eslint-disable-next-line functional/immutable-data + hyperlinkRelationshipAdditions.push({ + key, + hyperlink: { + id: linkId, + link: target, + }, + }); + }, + }, + } as unknown as IViewWrapper, + stack: [], + }; + contexts.set(key, context); + for (const [patchKey, patchValue] of Object.entries(options.patches)) { const patchText = `{{${patchKey}}}`; const renderedParagraphs = findLocationOfText(json, patchText); @@ -194,7 +211,7 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO zip.file(key, output); } - for (const { stream, fileName } of context.file.Media.Array) { + for (const { stream, fileName } of file.Media.Array) { zip.file(`word/media/${fileName}`, stream); } From 352511bb5564b7050482626a99a648bf5aa582a7 Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Tue, 14 Mar 2023 03:13:23 +0000 Subject: [PATCH 19/26] Add tests --- src/patcher/content-types-manager.spec.ts | 51 +++++++++++++++++++++++ src/patcher/util.spec.ts | 46 ++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/patcher/content-types-manager.spec.ts create mode 100644 src/patcher/util.spec.ts diff --git a/src/patcher/content-types-manager.spec.ts b/src/patcher/content-types-manager.spec.ts new file mode 100644 index 0000000000..0dd8eb38c4 --- /dev/null +++ b/src/patcher/content-types-manager.spec.ts @@ -0,0 +1,51 @@ +import { expect } from "chai"; +import { appendContentType } from "./content-types-manager"; + +describe("content-types-manager", () => { + describe("appendContentType", () => { + it("should append a content type", () => { + const element = { + type: "element", + name: "xml", + elements: [ + { + type: "element", + name: "Types", + elements: [ + { + type: "element", + name: "Default", + }, + ], + }, + ], + }; + appendContentType(element, "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", "docx"); + + expect(element).to.deep.equal({ + elements: [ + { + elements: [ + { + name: "Default", + type: "element", + }, + { + attributes: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", + Extension: "docx", + }, + name: "Default", + type: "element", + }, + ], + name: "Types", + type: "element", + }, + ], + name: "xml", + type: "element", + }); + }); + }); +}); diff --git a/src/patcher/util.spec.ts b/src/patcher/util.spec.ts new file mode 100644 index 0000000000..6482298754 --- /dev/null +++ b/src/patcher/util.spec.ts @@ -0,0 +1,46 @@ +import { expect } from "chai"; + +import { createTextElementContents, getFirstLevelElements, patchSpaceAttribute, toJson } from "./util"; + +describe("util", () => { + describe("toJson", () => { + it("should return an Element", () => { + const output = toJson(""); + expect(output).to.be.an("object"); + }); + }); + + describe("createTextElementContents", () => { + it("should return an array of elements", () => { + const output = createTextElementContents("hello"); + expect(output).to.deep.equal([{ type: "text", text: "hello" }]); + }); + }); + + describe("patchSpaceAttribute", () => { + it("should return an element with the xml:space attribute", () => { + const output = patchSpaceAttribute({ type: "element", name: "xml" }); + expect(output).to.deep.equal({ + type: "element", + name: "xml", + attributes: { + "xml:space": "preserve", + }, + }); + }); + }); + + describe("getFirstLevelElements", () => { + it("should return an empty array if no elements are found", () => { + const output = toJson(""); + const elements = getFirstLevelElements(output, "Relationships"); + expect(elements).to.deep.equal([]); + }); + + it("should return an array if elements are found", () => { + const output = toJson(""); + const elements = getFirstLevelElements(output, "Relationships"); + expect(elements).to.deep.equal([{ type: "element", name: "Relationship" }]); + }); + }); +}); From 811dd615629df89c938d12703c9bf944a3f090aa Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Wed, 15 Mar 2023 02:46:39 +0000 Subject: [PATCH 20/26] Add tests --- src/patcher/from-docx.spec.ts | 41 ++ src/patcher/from-docx.ts | 1 - src/patcher/paragraph-split-inject.spec.ts | 101 ++++ src/patcher/paragraph-split-inject.ts | 2 +- src/patcher/paragraph-token-replacer.spec.ts | 74 +++ src/patcher/relationship-manager.spec.ts | 61 ++ src/patcher/relationship-manager.ts | 4 +- src/patcher/replacer.spec.ts | 153 +++++ src/patcher/run-renderer.spec.ts | 96 +++ src/patcher/run-renderer.ts | 4 - src/patcher/traverser.spec.ts | 600 +++++++++++++++++++ src/patcher/util.spec.ts | 12 +- 12 files changed, 1138 insertions(+), 11 deletions(-) create mode 100644 src/patcher/from-docx.spec.ts create mode 100644 src/patcher/paragraph-split-inject.spec.ts create mode 100644 src/patcher/paragraph-token-replacer.spec.ts create mode 100644 src/patcher/relationship-manager.spec.ts create mode 100644 src/patcher/replacer.spec.ts create mode 100644 src/patcher/run-renderer.spec.ts create mode 100644 src/patcher/traverser.spec.ts diff --git a/src/patcher/from-docx.spec.ts b/src/patcher/from-docx.spec.ts new file mode 100644 index 0000000000..1e82f86059 --- /dev/null +++ b/src/patcher/from-docx.spec.ts @@ -0,0 +1,41 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as JSZip from "jszip"; + +import { TextRun } from "@file/paragraph"; + +import { patchDocument, PatchType } from "./from-docx"; + +describe("from-docx", () => { + describe("patchDocument", () => { + before(() => { + sinon.createStubInstance(JSZip, {}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sinon.stub(JSZip, "loadAsync").callsFake( + () => + new Promise((resolve) => { + const zip = new JSZip(); + + zip.file("word/document.xml", ``); + resolve(zip); + }), + ); + }); + + after(() => { + (JSZip.loadAsync as unknown as sinon.SinonStub).restore(); + }); + + it("should find the index of a run element with a token", async () => { + const output = await patchDocument(Buffer.from(""), { + patches: { + name: { + type: PatchType.PARAGRAPH, + children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")], + }, + }, + }); + expect(output).to.not.be.undefined; + }); + }); +}); diff --git a/src/patcher/from-docx.ts b/src/patcher/from-docx.ts index 38d45b23c8..c464ce6031 100644 --- a/src/patcher/from-docx.ts +++ b/src/patcher/from-docx.ts @@ -55,7 +55,6 @@ const imageReplacer = new ImageReplacer(); export const patchDocument = async (data: InputDataType, options: PatchDocumentOptions): Promise => { const zipContent = await JSZip.loadAsync(data); - const contexts = new Map(); const file = { Media: new Media(), diff --git a/src/patcher/paragraph-split-inject.spec.ts b/src/patcher/paragraph-split-inject.spec.ts new file mode 100644 index 0000000000..bfb3cbdb0b --- /dev/null +++ b/src/patcher/paragraph-split-inject.spec.ts @@ -0,0 +1,101 @@ +import { expect } from "chai"; + +import { findRunElementIndexWithToken, splitRunElement } from "./paragraph-split-inject"; + +describe("paragraph-split-inject", () => { + describe("findRunElementIndexWithToken", () => { + it("should find the index of a run element with a token", () => { + const output = findRunElementIndexWithToken( + { + name: "w:p", + type: "element", + elements: [ + { + name: "w:r", + type: "element", + elements: [ + { + name: "w:t", + type: "element", + elements: [ + { + type: "text", + text: "hello world", + }, + ], + }, + ], + }, + ], + }, + "hello", + ); + expect(output).to.deep.equal(0); + }); + }); + + describe("splitRunElement", () => { + it("should split a run element", () => { + const output = splitRunElement( + { + name: "w:r", + type: "element", + elements: [ + { + name: "w:t", + type: "element", + elements: [ + { + type: "text", + text: "hello*world", + }, + ], + }, + ], + }, + "*", + ); + + expect(output).to.deep.equal({ + left: { + elements: [ + { + attributes: { + "xml:space": "preserve", + }, + elements: [ + { + text: "hello", + type: "text", + }, + ], + name: "w:t", + type: "element", + }, + ], + name: "w:r", + type: "element", + }, + right: { + elements: [ + { + attributes: { + "xml:space": "preserve", + }, + elements: [ + { + text: "world", + type: "text", + }, + ], + name: "w:t", + type: "element", + }, + ], + name: "w:r", + type: "element", + }, + }); + }); + }); +}); diff --git a/src/patcher/paragraph-split-inject.ts b/src/patcher/paragraph-split-inject.ts index d3fe8d5e6c..1a2f06ad06 100644 --- a/src/patcher/paragraph-split-inject.ts +++ b/src/patcher/paragraph-split-inject.ts @@ -14,7 +14,7 @@ export const findRunElementIndexWithToken = (paragraphElement: Element, token: s } } } - // return -1; + throw new Error("Token not found"); }; diff --git a/src/patcher/paragraph-token-replacer.spec.ts b/src/patcher/paragraph-token-replacer.spec.ts new file mode 100644 index 0000000000..1ce88b1b80 --- /dev/null +++ b/src/patcher/paragraph-token-replacer.spec.ts @@ -0,0 +1,74 @@ +import { expect } from "chai"; + +import { replaceTokenInParagraphElement } from "./paragraph-token-replacer"; + +describe("paragraph-token-replacer", () => { + describe("replaceTokenInParagraphElement", () => { + it("should replace token in paragraph", () => { + const output = replaceTokenInParagraphElement({ + paragraphElement: { + name: "w:p", + elements: [ + { + name: "w:r", + elements: [ + { + name: "w:t", + elements: [ + { + type: "text", + text: "hello", + }, + ], + }, + ], + }, + ], + }, + renderedParagraph: { + index: 0, + path: [0], + runs: [ + { + end: 4, + index: 0, + parts: [ + { + end: 4, + index: 0, + start: 0, + text: "hello", + }, + ], + start: 0, + text: "hello", + }, + ], + text: "hello", + }, + originalText: "hello", + replacementText: "world", + }); + + expect(output).to.deep.equal({ + elements: [ + { + elements: [ + { + elements: [ + { + text: "world", + type: "text", + }, + ], + name: "w:t", + }, + ], + name: "w:r", + }, + ], + name: "w:p", + }); + }); + }); +}); diff --git a/src/patcher/relationship-manager.spec.ts b/src/patcher/relationship-manager.spec.ts new file mode 100644 index 0000000000..9161ed1dec --- /dev/null +++ b/src/patcher/relationship-manager.spec.ts @@ -0,0 +1,61 @@ +import { TargetModeType } from "@file/relationships/relationship/relationship"; +import { expect } from "chai"; + +import { appendRelationship, getNextRelationshipIndex } from "./relationship-manager"; + +describe("relationship-manager", () => { + describe("getNextRelationshipIndex", () => { + it("should get next relationship index", () => { + const output = getNextRelationshipIndex({ + elements: [ + { + type: "element", + name: "Relationships", + elements: [ + { type: "element", attributes: { Id: "rId1" }, name: "Relationship" }, + { type: "element", attributes: { Id: "rId1" }, name: "Relationship" }, + ], + }, + ], + }); + expect(output).to.deep.equal(2); + }); + }); + + describe("appendRelationship", () => { + it("should append a relationship", () => { + const output = appendRelationship( + { + elements: [ + { + type: "element", + name: "Relationships", + elements: [ + { type: "element", attributes: { Id: "rId1" }, name: "Relationship" }, + { type: "element", attributes: { Id: "rId1" }, name: "Relationship" }, + ], + }, + ], + }, + 1, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + "test", + TargetModeType.EXTERNAL, + ); + expect(output).to.deep.equal([ + { type: "element", attributes: { Id: "rId1" }, name: "Relationship" }, + { type: "element", attributes: { Id: "rId1" }, name: "Relationship" }, + { + attributes: { + Id: "rId1", + Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + TargetMode: TargetModeType.EXTERNAL, + Target: "test", + }, + name: "Relationship", + type: "element", + }, + ]); + }); + }); +}); diff --git a/src/patcher/relationship-manager.ts b/src/patcher/relationship-manager.ts index 35eaab8e71..3779500ffe 100644 --- a/src/patcher/relationship-manager.ts +++ b/src/patcher/relationship-manager.ts @@ -21,7 +21,7 @@ export const appendRelationship = ( type: RelationshipType, target: string, targetMode?: TargetModeType, -): void => { +): readonly Element[] => { const relationshipElements = getFirstLevelElements(relationships, "Relationships"); // eslint-disable-next-line functional/immutable-data relationshipElements.push({ @@ -34,4 +34,6 @@ export const appendRelationship = ( name: "Relationship", type: "element", }); + + return relationshipElements; }; diff --git a/src/patcher/replacer.spec.ts b/src/patcher/replacer.spec.ts new file mode 100644 index 0000000000..af747a3f4a --- /dev/null +++ b/src/patcher/replacer.spec.ts @@ -0,0 +1,153 @@ +import { IViewWrapper } from "@file/document-wrapper"; +import { File } from "@file/file"; +import { TextRun } from "@file/paragraph"; +import { IContext } from "@file/xml-components"; +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { PatchType } from "./from-docx"; + +import { replacer } from "./replacer"; + +const MOCK_JSON = { + elements: [ + { + type: "element", + name: "w:hdr", + attributes: { + "xmlns:wpc": "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", + "xmlns:cx": "http://schemas.microsoft.com/office/drawing/2014/chartex", + "xmlns:cx1": "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex", + "xmlns:cx2": "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex", + "xmlns:cx3": "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex", + "xmlns:cx4": "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex", + "xmlns:cx5": "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex", + "xmlns:cx6": "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex", + "xmlns:cx7": "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex", + "xmlns:cx8": "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex", + "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006", + "xmlns:aink": "http://schemas.microsoft.com/office/drawing/2016/ink", + "xmlns:am3d": "http://schemas.microsoft.com/office/drawing/2017/model3d", + "xmlns:o": "urn:schemas-microsoft-com:office:office", + "xmlns:oel": "http://schemas.microsoft.com/office/2019/extlst", + "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "xmlns:m": "http://schemas.openxmlformats.org/officeDocument/2006/math", + "xmlns:v": "urn:schemas-microsoft-com:vml", + "xmlns:wp14": "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing", + "xmlns:wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "xmlns:w10": "urn:schemas-microsoft-com:office:word", + "xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "xmlns:w14": "http://schemas.microsoft.com/office/word/2010/wordml", + "xmlns:w15": "http://schemas.microsoft.com/office/word/2012/wordml", + "xmlns:w16cex": "http://schemas.microsoft.com/office/word/2018/wordml/cex", + "xmlns:w16cid": "http://schemas.microsoft.com/office/word/2016/wordml/cid", + "xmlns:w16": "http://schemas.microsoft.com/office/word/2018/wordml", + "xmlns:w16sdtdh": "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash", + "xmlns:w16se": "http://schemas.microsoft.com/office/word/2015/wordml/symex", + "xmlns:wpg": "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup", + "xmlns:wpi": "http://schemas.microsoft.com/office/word/2010/wordprocessingInk", + "xmlns:wne": "http://schemas.microsoft.com/office/word/2006/wordml", + "xmlns:wps": "http://schemas.microsoft.com/office/word/2010/wordprocessingShape", + }, + elements: [ + { + type: "element", + name: "w:p", + attributes: { "w14:paraId": "3BE1A671", "w14:textId": "74E856C4", "w:rsidR": "000D38A7", "w:rsidRDefault": "000D38A7" }, + elements: [ + { + type: "element", + name: "w:pPr", + elements: [{ type: "element", name: "w:pStyle", attributes: { "w:val": "Header" } }], + }, + { + type: "element", + name: "w:r", + elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "This is a {{head" }] }], + }, + { + type: "element", + name: "w:r", + attributes: { "w:rsidR": "004A3A99" }, + elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "er" }] }], + }, + { + type: "element", + name: "w:r", + elements: [ + { type: "element", name: "w:t", elements: [{ type: "text", text: "_adjective}} don’t you think?" }] }, + ], + }, + ], + }, + ], + }, + ], +}; + +describe("replacer", () => { + describe("replacer", () => { + it("should return the same object if nothing is added", () => { + const output = replacer( + { + elements: [], + }, + { + type: PatchType.PARAGRAPH, + children: [], + }, + "hello", + [], + sinon.mock() as unknown as IContext, + ); + + expect(output).to.deep.equal({ + elements: [], + }); + }); + + it("should return the same object if nothing is added", () => { + const output = replacer( + MOCK_JSON, + { + type: PatchType.PARAGRAPH, + children: [new TextRun("Delightful Header")], + }, + "{{header_adjective}}", + [ + { + text: "This is a {{header_adjective}} don’t you think?", + runs: [ + { + text: "This is a {{head", + parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }], + index: 1, + start: 0, + end: 15, + }, + { text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 }, + { + text: "_adjective}} don’t you think?", + parts: [{ text: "_adjective}} don’t you think?", index: 0, start: 18, end: 46 }], + index: 3, + start: 18, + end: 46, + }, + ], + index: 0, + path: [0, 0, 0], + }, + ], + { + file: {} as unknown as File, + viewWrapper: { + Relationships: {}, + } as unknown as IViewWrapper, + stack: [], + }, + ); + + expect(JSON.stringify(output)).to.contain("Delightful Header"); + }); + }); +}); diff --git a/src/patcher/run-renderer.spec.ts b/src/patcher/run-renderer.spec.ts new file mode 100644 index 0000000000..949f4a7635 --- /dev/null +++ b/src/patcher/run-renderer.spec.ts @@ -0,0 +1,96 @@ +import { expect } from "chai"; +import { renderParagraphNode } from "./run-renderer"; + +describe("run-renderer", () => { + describe("renderParagraphNode", () => { + it("should return a rendered paragraph node if theres no elements", () => { + const output = renderParagraphNode({ element: { name: "w:p" }, index: 0, parent: undefined }); + expect(output).to.deep.equal({ + index: -1, + path: [], + runs: [], + text: "", + }); + }); + + it("should return a rendered paragraph node if there are elements", () => { + const output = renderParagraphNode({ + element: { + name: "w:p", + elements: [ + { + name: "w:r", + elements: [ + { + name: "w:t", + elements: [ + { + type: "text", + text: "hello", + }, + ], + }, + ], + }, + ], + }, + index: 0, + parent: undefined, + }); + expect(output).to.deep.equal({ + index: 0, + path: [0], + runs: [ + { + end: 4, + index: 0, + parts: [ + { + end: 4, + index: 0, + start: 0, + text: "hello", + }, + ], + start: 0, + text: "hello", + }, + ], + text: "hello", + }); + }); + + it("should throw an error if the element is not a paragraph", () => { + expect(() => renderParagraphNode({ element: { name: "w:r" }, index: 0, parent: undefined })).to.throw(); + }); + + it("should return blank defaults if run is empty", () => { + const output = renderParagraphNode({ + element: { + name: "w:p", + elements: [ + { + name: "w:r", + }, + ], + }, + index: 0, + parent: undefined, + }); + expect(output).to.deep.equal({ + index: 0, + path: [0], + runs: [ + { + end: 0, + index: -1, + parts: [], + start: 0, + text: "", + }, + ], + text: "", + }); + }); + }); +}); diff --git a/src/patcher/run-renderer.ts b/src/patcher/run-renderer.ts index 344898c10a..262d6f265a 100644 --- a/src/patcher/run-renderer.ts +++ b/src/patcher/run-renderer.ts @@ -64,10 +64,6 @@ export const renderParagraphNode = (node: ElementWrapper): IRenderedParagraphNod }; const renderRunNode = (node: Element, index: number, currentRunStringIndex: number): IRenderedRunNode => { - if (node.name !== "w:r") { - throw new Error(`Invalid node type: ${node.name}`); - } - if (!node.elements) { return { text: "", diff --git a/src/patcher/traverser.spec.ts b/src/patcher/traverser.spec.ts new file mode 100644 index 0000000000..65ec717e3f --- /dev/null +++ b/src/patcher/traverser.spec.ts @@ -0,0 +1,600 @@ +import { expect } from "chai"; + +import { findLocationOfText } from "./traverser"; + +const MOCK_JSON = { + elements: [ + { + type: "element", + name: "w:document", + attributes: { + "xmlns:wpc": "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", + "xmlns:cx": "http://schemas.microsoft.com/office/drawing/2014/chartex", + "xmlns:cx1": "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex", + "xmlns:cx2": "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex", + "xmlns:cx3": "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex", + "xmlns:cx4": "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex", + "xmlns:cx5": "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex", + "xmlns:cx6": "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex", + "xmlns:cx7": "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex", + "xmlns:cx8": "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex", + "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006", + "xmlns:aink": "http://schemas.microsoft.com/office/drawing/2016/ink", + "xmlns:am3d": "http://schemas.microsoft.com/office/drawing/2017/model3d", + "xmlns:o": "urn:schemas-microsoft-com:office:office", + "xmlns:oel": "http://schemas.microsoft.com/office/2019/extlst", + "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "xmlns:m": "http://schemas.openxmlformats.org/officeDocument/2006/math", + "xmlns:v": "urn:schemas-microsoft-com:vml", + "xmlns:wp14": "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing", + "xmlns:wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "xmlns:w10": "urn:schemas-microsoft-com:office:word", + "xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "xmlns:w14": "http://schemas.microsoft.com/office/word/2010/wordml", + "xmlns:w15": "http://schemas.microsoft.com/office/word/2012/wordml", + "xmlns:w16cex": "http://schemas.microsoft.com/office/word/2018/wordml/cex", + "xmlns:w16cid": "http://schemas.microsoft.com/office/word/2016/wordml/cid", + "xmlns:w16": "http://schemas.microsoft.com/office/word/2018/wordml", + "xmlns:w16sdtdh": "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash", + "xmlns:w16se": "http://schemas.microsoft.com/office/word/2015/wordml/symex", + "xmlns:wpg": "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup", + "xmlns:wpi": "http://schemas.microsoft.com/office/word/2010/wordprocessingInk", + "xmlns:wne": "http://schemas.microsoft.com/office/word/2006/wordml", + "xmlns:wps": "http://schemas.microsoft.com/office/word/2010/wordprocessingShape", + "mc:Ignorable": "w14 w15 w16se w16cid w16 w16cex w16sdtdh wp14", + }, + elements: [ + { + type: "element", + name: "w:body", + elements: [ + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "2499FE9F", + "w14:textId": "0A3D130F", + "w:rsidR": "00B51233", + "w:rsidRDefault": "007B52ED", + "w:rsidP": "007B52ED", + }, + elements: [ + { + type: "element", + name: "w:pPr", + elements: [{ type: "element", name: "w:pStyle", attributes: { "w:val": "Title" } }], + }, + { + type: "element", + name: "w:r", + elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "Hello World" }] }], + }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "6410D9A0", + "w14:textId": "7579AB49", + "w:rsidR": "007B52ED", + "w:rsidRDefault": "007B52ED", + }, + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "57ACF964", + "w14:textId": "315D7A05", + "w:rsidR": "007B52ED", + "w:rsidRDefault": "007B52ED", + }, + elements: [ + { + type: "element", + name: "w:r", + elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "Hello {{name}}," }] }], + }, + { + type: "element", + name: "w:r", + attributes: { "w:rsidR": "008126CB" }, + elements: [ + { + type: "element", + name: "w:t", + attributes: { "xml:space": "preserve" }, + elements: [{ type: "text", text: " how are you?" }], + }, + ], + }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "38C7DF4A", + "w14:textId": "66CDEC9A", + "w:rsidR": "007B52ED", + "w:rsidRDefault": "007B52ED", + }, + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "04FABE2B", + "w14:textId": "3DACA001", + "w:rsidR": "007B52ED", + "w:rsidRDefault": "007B52ED", + }, + elements: [ + { + type: "element", + name: "w:r", + elements: [ + { type: "element", name: "w:t", elements: [{ type: "text", text: "{{paragraph_replace}}" }] }, + ], + }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "7AD7975D", + "w14:textId": "77777777", + "w:rsidR": "00EF161F", + "w:rsidRDefault": "00EF161F", + }, + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "3BD6D75A", + "w14:textId": "19AE3121", + "w:rsidR": "00EF161F", + "w:rsidRDefault": "00EF161F", + }, + elements: [ + { + type: "element", + name: "w:r", + elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "{{table}}" }] }], + }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "76023962", + "w14:textId": "4E606AB9", + "w:rsidR": "007B52ED", + "w:rsidRDefault": "007B52ED", + }, + }, + { + type: "element", + name: "w:tbl", + elements: [ + { + type: "element", + name: "w:tblPr", + elements: [ + { type: "element", name: "w:tblStyle", attributes: { "w:val": "TableGrid" } }, + { type: "element", name: "w:tblW", attributes: { "w:w": "0", "w:type": "auto" } }, + { + type: "element", + name: "w:tblLook", + attributes: { + "w:val": "04A0", + "w:firstRow": "1", + "w:lastRow": "0", + "w:firstColumn": "1", + "w:lastColumn": "0", + "w:noHBand": "0", + "w:noVBand": "1", + }, + }, + ], + }, + { + type: "element", + name: "w:tblGrid", + elements: [ + { type: "element", name: "w:gridCol", attributes: { "w:w": "3003" } }, + { type: "element", name: "w:gridCol", attributes: { "w:w": "3003" } }, + { type: "element", name: "w:gridCol", attributes: { "w:w": "3004" } }, + ], + }, + { + type: "element", + name: "w:tr", + attributes: { + "w:rsidR": "00EF161F", + "w14:paraId": "1DEC5955", + "w14:textId": "77777777", + "w:rsidTr": "00EF161F", + }, + elements: [ + { + type: "element", + name: "w:tc", + elements: [ + { + type: "element", + name: "w:tcPr", + elements: [ + { type: "element", name: "w:tcW", attributes: { "w:w": "3003", "w:type": "dxa" } }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "54DA5587", + "w14:textId": "625BAC60", + "w:rsidR": "00EF161F", + "w:rsidRDefault": "00EF161F", + }, + elements: [ + { + type: "element", + name: "w:r", + elements: [ + { + type: "element", + name: "w:t", + elements: [{ type: "text", text: "{{table_heading_1}}" }], + }, + ], + }, + ], + }, + ], + }, + { + type: "element", + name: "w:tc", + elements: [ + { + type: "element", + name: "w:tcPr", + elements: [ + { type: "element", name: "w:tcW", attributes: { "w:w": "3003", "w:type": "dxa" } }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "57100910", + "w14:textId": "71FD5616", + "w:rsidR": "00EF161F", + "w:rsidRDefault": "00EF161F", + }, + }, + ], + }, + { + type: "element", + name: "w:tc", + elements: [ + { + type: "element", + name: "w:tcPr", + elements: [ + { type: "element", name: "w:tcW", attributes: { "w:w": "3004", "w:type": "dxa" } }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "1D388FAB", + "w14:textId": "77777777", + "w:rsidR": "00EF161F", + "w:rsidRDefault": "00EF161F", + }, + }, + ], + }, + ], + }, + { + type: "element", + name: "w:tr", + attributes: { + "w:rsidR": "00EF161F", + "w14:paraId": "0F53D2DC", + "w14:textId": "77777777", + "w:rsidTr": "00EF161F", + }, + elements: [ + { + type: "element", + name: "w:tc", + elements: [ + { + type: "element", + name: "w:tcPr", + elements: [ + { type: "element", name: "w:tcW", attributes: { "w:w": "3003", "w:type": "dxa" } }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "0F2BCCED", + "w14:textId": "3C3B6706", + "w:rsidR": "00EF161F", + "w:rsidRDefault": "00EF161F", + }, + elements: [ + { + type: "element", + name: "w:r", + elements: [ + { + type: "element", + name: "w:t", + elements: [{ type: "text", text: "Item: {{item_1}}" }], + }, + ], + }, + ], + }, + ], + }, + { + type: "element", + name: "w:tc", + elements: [ + { + type: "element", + name: "w:tcPr", + elements: [ + { type: "element", name: "w:tcW", attributes: { "w:w": "3003", "w:type": "dxa" } }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "1E6158AC", + "w14:textId": "77777777", + "w:rsidR": "00EF161F", + "w:rsidRDefault": "00EF161F", + }, + }, + ], + }, + { + type: "element", + name: "w:tc", + elements: [ + { + type: "element", + name: "w:tcPr", + elements: [ + { type: "element", name: "w:tcW", attributes: { "w:w": "3004", "w:type": "dxa" } }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "17937748", + "w14:textId": "77777777", + "w:rsidR": "00EF161F", + "w:rsidRDefault": "00EF161F", + }, + }, + ], + }, + ], + }, + { + type: "element", + name: "w:tr", + attributes: { + "w:rsidR": "00EF161F", + "w14:paraId": "781DAC1A", + "w14:textId": "77777777", + "w:rsidTr": "00EF161F", + }, + elements: [ + { + type: "element", + name: "w:tc", + elements: [ + { + type: "element", + name: "w:tcPr", + elements: [ + { type: "element", name: "w:tcW", attributes: { "w:w": "3003", "w:type": "dxa" } }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "1DCD0343", + "w14:textId": "77777777", + "w:rsidR": "00EF161F", + "w:rsidRDefault": "00EF161F", + }, + }, + ], + }, + { + type: "element", + name: "w:tc", + elements: [ + { + type: "element", + name: "w:tcPr", + elements: [ + { type: "element", name: "w:tcW", attributes: { "w:w": "3003", "w:type": "dxa" } }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "5D02E3CD", + "w14:textId": "77777777", + "w:rsidR": "00EF161F", + "w:rsidRDefault": "00EF161F", + }, + }, + ], + }, + { + type: "element", + name: "w:tc", + elements: [ + { + type: "element", + name: "w:tcPr", + elements: [ + { type: "element", name: "w:tcW", attributes: { "w:w": "3004", "w:type": "dxa" } }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "52EA0DBB", + "w14:textId": "77777777", + "w:rsidR": "00EF161F", + "w:rsidRDefault": "00EF161F", + }, + }, + ], + }, + ], + }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "47CD1FBC", + "w14:textId": "23474CBC", + "w:rsidR": "007B52ED", + "w:rsidRDefault": "007B52ED", + }, + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "0ACCEE90", + "w14:textId": "67907499", + "w:rsidR": "00EF161F", + "w:rsidRDefault": "0077578F", + }, + elements: [ + { + type: "element", + name: "w:r", + elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "{{image_test}}" }] }], + }, + ], + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "23FA9862", + "w14:textId": "77777777", + "w:rsidR": "0077578F", + "w:rsidRDefault": "0077578F", + }, + }, + { + type: "element", + name: "w:p", + attributes: { + "w14:paraId": "01578F2F", + "w14:textId": "3BDC6C85", + "w:rsidR": "007B52ED", + "w:rsidRDefault": "007B52ED", + }, + elements: [ + { + type: "element", + name: "w:r", + elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "Thank you" }] }], + }, + ], + }, + { + type: "element", + name: "w:sectPr", + attributes: { "w:rsidR": "007B52ED", "w:rsidSect": "0072043F" }, + elements: [ + { type: "element", name: "w:headerReference", attributes: { "w:type": "default", "r:id": "rId6" } }, + { type: "element", name: "w:footerReference", attributes: { "w:type": "default", "r:id": "rId7" } }, + { type: "element", name: "w:pgSz", attributes: { "w:w": "11900", "w:h": "16840" } }, + { + type: "element", + name: "w:pgMar", + attributes: { + "w:top": "1440", + "w:right": "1440", + "w:bottom": "1440", + "w:left": "1440", + "w:header": "708", + "w:footer": "708", + "w:gutter": "0", + }, + }, + { type: "element", name: "w:cols", attributes: { "w:space": "708" } }, + { type: "element", name: "w:docGrid", attributes: { "w:linePitch": "360" } }, + ], + }, + ], + }, + ], + }, + ], +}; + +describe("traverser", () => { + describe("findLocationOfText", () => { + it("should find the location of text", () => { + const output = findLocationOfText(MOCK_JSON, "{{table_heading_1}}"); + expect(output).to.deep.equal([ + { + index: 1, + path: [0, 0, 0, 8, 2, 0, 1], + runs: [ + { + end: 18, + index: 0, + parts: [ + { + end: 18, + index: 0, + start: 0, + text: "{{table_heading_1}}", + }, + ], + start: 0, + text: "{{table_heading_1}}", + }, + ], + text: "{{table_heading_1}}", + }, + ]); + }); + }); +}); diff --git a/src/patcher/util.spec.ts b/src/patcher/util.spec.ts index 6482298754..c303b4ffe6 100644 --- a/src/patcher/util.spec.ts +++ b/src/patcher/util.spec.ts @@ -32,14 +32,18 @@ describe("util", () => { describe("getFirstLevelElements", () => { it("should return an empty array if no elements are found", () => { - const output = toJson(""); - const elements = getFirstLevelElements(output, "Relationships"); + const elements = getFirstLevelElements( + { elements: [{ type: "element", name: "Relationships", elements: [] }] }, + "Relationships", + ); expect(elements).to.deep.equal([]); }); it("should return an array if elements are found", () => { - const output = toJson(""); - const elements = getFirstLevelElements(output, "Relationships"); + const elements = getFirstLevelElements( + { elements: [{ type: "element", name: "Relationships", elements: [{ type: "element", name: "Relationship" }] }] }, + "Relationships", + ); expect(elements).to.deep.equal([{ type: "element", name: "Relationship" }]); }); }); From 262f6323d024ffb001cbf1b94cd53624e6974c4a Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Wed, 15 Mar 2023 03:14:38 +0000 Subject: [PATCH 21/26] Write more tests and simplify code --- src/patcher/from-docx.spec.ts | 241 +++++++++++++++++++++++++++++++++- src/patcher/from-docx.ts | 11 +- 2 files changed, 241 insertions(+), 11 deletions(-) diff --git a/src/patcher/from-docx.spec.ts b/src/patcher/from-docx.spec.ts index 1e82f86059..147bdd29d0 100644 --- a/src/patcher/from-docx.spec.ts +++ b/src/patcher/from-docx.spec.ts @@ -2,10 +2,203 @@ import { expect } from "chai"; import * as sinon from "sinon"; import * as JSZip from "jszip"; -import { TextRun } from "@file/paragraph"; +import { ExternalHyperlink, ImageRun, Paragraph, TextRun } from "@file/paragraph"; import { patchDocument, PatchType } from "./from-docx"; +const MOCK_XML = ` + + + + + + + + + Hello World + + + + + + Hello {{name}}, + + + how are you? + + + + + + {{paragraph_replace}} + + + + + + {{table}} + + + + + + + + + + + + + + + + + + + + + + {{table_heading_1}} + + + + + + + + + + + + + + + + + + + + + + + + Item: {{item_1}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{image_test}} + + + + + + Thank you + + + + + + + + + + + + +`; + describe("from-docx", () => { describe("patchDocument", () => { before(() => { @@ -16,7 +209,8 @@ describe("from-docx", () => { new Promise((resolve) => { const zip = new JSZip(); - zip.file("word/document.xml", ``); + zip.file("word/document.xml", MOCK_XML); + zip.file("[Content_Types].xml", ``); resolve(zip); }), ); @@ -33,6 +227,49 @@ describe("from-docx", () => { type: PatchType.PARAGRAPH, children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")], }, + item_1: { + type: PatchType.PARAGRAPH, + children: [ + new TextRun("#657"), + new ExternalHyperlink({ + children: [ + new TextRun({ + text: "BBC News Link", + }), + ], + link: "https://www.bbc.co.uk/news", + }), + ], + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + paragraph_replace: { + type: PatchType.DOCUMENT, + children: [ + new Paragraph({ + children: [ + new TextRun("This is a "), + new ExternalHyperlink({ + children: [ + new TextRun({ + text: "Google Link", + }), + ], + link: "https://www.google.co.uk", + }), + ], + }), + ], + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + image_test: { + type: PatchType.PARAGRAPH, + children: [ + new ImageRun({ + data: Buffer.from(""), + transformation: { width: 100, height: 100 }, + }), + ], + }, }, }); expect(output).to.not.be.undefined; diff --git a/src/patcher/from-docx.ts b/src/patcher/from-docx.ts index c464ce6031..3d27bd402d 100644 --- a/src/patcher/from-docx.ts +++ b/src/patcher/from-docx.ts @@ -169,15 +169,8 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO // eslint-disable-next-line functional/immutable-data const relationshipKey = `word/_rels/${key.split("/").pop()}.rels`; - if (!map.has(relationshipKey)) { - map.set(relationshipKey, createRelationshipFile()); - } - - const relationshipsJson = map.get(relationshipKey); - - if (!relationshipsJson) { - throw new Error("Could not find relationships file"); - } + const relationshipsJson = map.get(relationshipKey) ?? createRelationshipFile(); + map.set(relationshipKey, relationshipsJson); appendRelationship( relationshipsJson, From 7e9884081e30b62ed8ff0df1711d6785afa7ee62 Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Thu, 16 Mar 2023 01:55:18 +0000 Subject: [PATCH 22/26] Add tests to patcher --- demo/85-template-document.ts | 1 + src/patcher/from-docx.spec.ts | 80 +++++++++++- src/patcher/from-docx.ts | 17 +-- src/patcher/paragraph-split-inject.spec.ts | 123 ++++++++++++++++++ src/patcher/paragraph-split-inject.ts | 2 +- src/patcher/paragraph-token-replacer.spec.ts | 91 +++++++++++++ src/patcher/relationship-manager.spec.ts | 26 ++++ src/patcher/relationship-manager.ts | 9 +- src/patcher/replacer.spec.ts | 127 +++++++++++++------ src/patcher/replacer.ts | 40 +++--- src/patcher/traverser.ts | 12 +- 11 files changed, 447 insertions(+), 81 deletions(-) diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index d3aca499ee..69015202f1 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -56,6 +56,7 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { ], link: "https://www.google.co.uk", }), + new ImageRun({ data: fs.readFileSync("./demo/images/dog.png"), transformation: { width: 100, height: 100 } }), ], }), ], diff --git a/src/patcher/from-docx.spec.ts b/src/patcher/from-docx.spec.ts index 147bdd29d0..9fa8b45119 100644 --- a/src/patcher/from-docx.spec.ts +++ b/src/patcher/from-docx.spec.ts @@ -201,7 +201,7 @@ const MOCK_XML = ` describe("from-docx", () => { describe("patchDocument", () => { - before(() => { + beforeEach(() => { sinon.createStubInstance(JSZip, {}); // eslint-disable-next-line @typescript-eslint/no-explicit-any sinon.stub(JSZip, "loadAsync").callsFake( @@ -216,11 +216,11 @@ describe("from-docx", () => { ); }); - after(() => { + afterEach(() => { (JSZip.loadAsync as unknown as sinon.SinonStub).restore(); }); - it("should find the index of a run element with a token", async () => { + it("should patch the document", async () => { const output = await patchDocument(Buffer.from(""), { patches: { name: { @@ -256,6 +256,10 @@ describe("from-docx", () => { ], link: "https://www.google.co.uk", }), + new ImageRun({ + data: Buffer.from(""), + transformation: { width: 100, height: 100 }, + }), ], }), ], @@ -274,5 +278,75 @@ describe("from-docx", () => { }); expect(output).to.not.be.undefined; }); + + it("should patch the document", async () => { + const output = await patchDocument(Buffer.from(""), { + patches: {}, + }); + expect(output).to.not.be.undefined; + }); + + it("should use the relationships file rather than create one", () => { + (JSZip.loadAsync as unknown as sinon.SinonStub).restore(); + sinon.createStubInstance(JSZip, {}); + sinon.stub(JSZip, "loadAsync").callsFake( + () => + new Promise((resolve) => { + const zip = new JSZip(); + + zip.file("word/document.xml", MOCK_XML); + zip.file("word/_rels/document.xml.rels", ``); + zip.file("[Content_Types].xml", ``); + resolve(zip); + }), + ); + + const output = patchDocument(Buffer.from(""), { + patches: { + // eslint-disable-next-line @typescript-eslint/naming-convention + image_test: { + type: PatchType.PARAGRAPH, + children: [ + new ImageRun({ + data: Buffer.from(""), + transformation: { width: 100, height: 100 }, + }), + ], + }, + }, + }); + expect(output).to.not.be.undefined; + }); + + it("should throw an error if the content types is not found", () => { + (JSZip.loadAsync as unknown as sinon.SinonStub).restore(); + sinon.createStubInstance(JSZip, {}); + sinon.stub(JSZip, "loadAsync").callsFake( + () => + new Promise((resolve) => { + const zip = new JSZip(); + + zip.file("word/document.xml", MOCK_XML); + resolve(zip); + }), + ); + + expect(() => + patchDocument(Buffer.from(""), { + patches: { + // eslint-disable-next-line @typescript-eslint/naming-convention + image_test: { + type: PatchType.PARAGRAPH, + children: [ + new ImageRun({ + data: Buffer.from(""), + transformation: { width: 100, height: 100 }, + }), + ], + }, + }, + }), + ).to.throw(); + }); }); }); diff --git a/src/patcher/from-docx.ts b/src/patcher/from-docx.ts index 3d27bd402d..675f72c5a5 100644 --- a/src/patcher/from-docx.ts +++ b/src/patcher/from-docx.ts @@ -140,25 +140,18 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO for (const { key, mediaDatas } of imageRelationshipAdditions) { // eslint-disable-next-line functional/immutable-data const relationshipKey = `word/_rels/${key.split("/").pop()}.rels`; - - if (!map.has(relationshipKey)) { - map.set(relationshipKey, createRelationshipFile()); - } - - const relationshipsJson = map.get(relationshipKey); - - if (!relationshipsJson) { - throw new Error("Could not find relationships file"); - } + const relationshipsJson = map.get(relationshipKey) ?? createRelationshipFile(); + map.set(relationshipKey, relationshipsJson); const index = getNextRelationshipIndex(relationshipsJson); const newJson = imageReplacer.replace(JSON.stringify(map.get(key)), mediaDatas, index); map.set(key, JSON.parse(newJson) as Element); - for (const { fileName } of mediaDatas) { + for (let i = 0; i < mediaDatas.length; i++) { + const { fileName } = mediaDatas[i]; appendRelationship( relationshipsJson, - index, + index + i, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", `media/${fileName}`, ); diff --git a/src/patcher/paragraph-split-inject.spec.ts b/src/patcher/paragraph-split-inject.spec.ts index bfb3cbdb0b..5228a88d2d 100644 --- a/src/patcher/paragraph-split-inject.spec.ts +++ b/src/patcher/paragraph-split-inject.spec.ts @@ -32,6 +32,60 @@ describe("paragraph-split-inject", () => { ); expect(output).to.deep.equal(0); }); + + it("should throw an exception when ran with empty elements", () => { + expect(() => + findRunElementIndexWithToken( + { + name: "w:p", + type: "element", + }, + "hello", + ), + ).to.throw(); + }); + + it("should throw an exception when ran with empty elements", () => { + expect(() => + findRunElementIndexWithToken( + { + name: "w:p", + type: "element", + elements: [ + { + name: "w:r", + type: "element", + }, + ], + }, + "hello", + ), + ).to.throw(); + }); + + it("should throw an exception when ran with empty elements", () => { + expect(() => + findRunElementIndexWithToken( + { + name: "w:p", + type: "element", + elements: [ + { + name: "w:r", + type: "element", + elements: [ + { + name: "w:t", + type: "element", + }, + ], + }, + ], + }, + "hello", + ), + ).to.throw(); + }); }); describe("splitRunElement", () => { @@ -51,6 +105,10 @@ describe("paragraph-split-inject", () => { }, ], }, + { + name: "w:x", + type: "element", + }, ], }, "*", @@ -91,11 +149,76 @@ describe("paragraph-split-inject", () => { name: "w:t", type: "element", }, + { + name: "w:x", + type: "element", + }, ], name: "w:r", type: "element", }, }); }); + + it("should try to split even if elements is empty for text", () => { + const output = splitRunElement( + { + name: "w:r", + type: "element", + elements: [ + { + name: "w:t", + type: "element", + }, + ], + }, + "*", + ); + + expect(output).to.deep.equal({ + left: { + elements: [ + { + attributes: { + "xml:space": "preserve", + }, + elements: [], + name: "w:t", + type: "element", + }, + ], + name: "w:r", + type: "element", + }, + right: { + elements: [], + name: "w:r", + type: "element", + }, + }); + }); + + it("should return empty elements", () => { + const output = splitRunElement( + { + name: "w:r", + type: "element", + }, + "*", + ); + + expect(output).to.deep.equal({ + left: { + elements: [], + name: "w:r", + type: "element", + }, + right: { + elements: [], + name: "w:r", + type: "element", + }, + }); + }); }); }); diff --git a/src/patcher/paragraph-split-inject.ts b/src/patcher/paragraph-split-inject.ts index 1a2f06ad06..a32cea407f 100644 --- a/src/patcher/paragraph-split-inject.ts +++ b/src/patcher/paragraph-split-inject.ts @@ -25,7 +25,7 @@ export const splitRunElement = (runElement: Element, token: string): { readonly runElement.elements ?.map((e, i) => { if (e.type === "element" && e.name === "w:t") { - const text = e.elements?.[0].text as string; + const text = (e.elements?.[0].text as string) ?? ""; const splitText = text.split(token); const newElements = splitText.map((t) => ({ ...e, diff --git a/src/patcher/paragraph-token-replacer.spec.ts b/src/patcher/paragraph-token-replacer.spec.ts index 1ce88b1b80..bcfc72e75c 100644 --- a/src/patcher/paragraph-token-replacer.spec.ts +++ b/src/patcher/paragraph-token-replacer.spec.ts @@ -70,5 +70,96 @@ describe("paragraph-token-replacer", () => { name: "w:p", }); }); + + // Try to fill rest of test coverage + // it("should replace token in paragraph", () => { + // const output = replaceTokenInParagraphElement({ + // paragraphElement: { + // name: "w:p", + // elements: [ + // { + // name: "w:r", + // elements: [ + // { + // name: "w:t", + // elements: [ + // { + // type: "text", + // text: "test ", + // }, + // ], + // }, + // { + // name: "w:t", + // elements: [ + // { + // type: "text", + // text: " hello ", + // }, + // ], + // }, + // ], + // }, + // ], + // }, + // renderedParagraph: { + // index: 0, + // path: [0], + // runs: [ + // { + // end: 4, + // index: 0, + // parts: [ + // { + // end: 4, + // index: 0, + // start: 0, + // text: "test ", + // }, + // ], + // start: 0, + // text: "test ", + // }, + // { + // end: 10, + // index: 0, + // parts: [ + // { + // end: 10, + // index: 0, + // start: 5, + // text: "hello ", + // }, + // ], + // start: 5, + // text: "hello ", + // }, + // ], + // text: "test hello ", + // }, + // originalText: "hello", + // replacementText: "world", + // }); + + // expect(output).to.deep.equal({ + // elements: [ + // { + // elements: [ + // { + // elements: [ + // { + // text: "test world ", + // type: "text", + // }, + // ], + // name: "w:t", + // }, + // ], + // name: "w:r", + // }, + // ], + // name: "w:p", + // }); + // }); }); }); diff --git a/src/patcher/relationship-manager.spec.ts b/src/patcher/relationship-manager.spec.ts index 9161ed1dec..4d22e7df8b 100644 --- a/src/patcher/relationship-manager.spec.ts +++ b/src/patcher/relationship-manager.spec.ts @@ -20,6 +20,32 @@ describe("relationship-manager", () => { }); expect(output).to.deep.equal(2); }); + + it("should work with an empty relationship Id", () => { + const output = getNextRelationshipIndex({ + elements: [ + { + type: "element", + name: "Relationships", + elements: [{ type: "element", name: "Relationship" }], + }, + ], + }); + expect(output).to.deep.equal(1); + }); + + it("should work with no relationships", () => { + const output = getNextRelationshipIndex({ + elements: [ + { + type: "element", + name: "Relationships", + elements: [], + }, + ], + }); + expect(output).to.deep.equal(1); + }); }); describe("appendRelationship", () => { diff --git a/src/patcher/relationship-manager.ts b/src/patcher/relationship-manager.ts index 3779500ffe..4f14035703 100644 --- a/src/patcher/relationship-manager.ts +++ b/src/patcher/relationship-manager.ts @@ -3,15 +3,18 @@ import { Element } from "xml-js"; import { RelationshipType, TargetModeType } from "@file/relationships/relationship/relationship"; import { getFirstLevelElements } from "./util"; -const getIdFromRelationshipId = (relationshipId: string): number => parseInt(relationshipId.substring(3), 10); +const getIdFromRelationshipId = (relationshipId: string): number => { + const output = parseInt(relationshipId.substring(3), 10); + return isNaN(output) ? 0 : output; +}; export const getNextRelationshipIndex = (relationships: Element): number => { const relationshipElements = getFirstLevelElements(relationships, "Relationships"); return ( - (relationshipElements + relationshipElements .map((e) => getIdFromRelationshipId(e.attributes?.Id?.toString() ?? "")) - .reduce((acc, curr) => Math.max(acc, curr), 0) ?? 0) + 1 + .reduce((acc, curr) => Math.max(acc, curr), 0) + 1 ); }; diff --git a/src/patcher/replacer.spec.ts b/src/patcher/replacer.spec.ts index af747a3f4a..8b15bd1fa9 100644 --- a/src/patcher/replacer.spec.ts +++ b/src/patcher/replacer.spec.ts @@ -1,6 +1,6 @@ import { IViewWrapper } from "@file/document-wrapper"; import { File } from "@file/file"; -import { TextRun } from "@file/paragraph"; +import { Paragraph, TextRun } from "@file/paragraph"; import { IContext } from "@file/xml-components"; import { expect } from "chai"; import * as sinon from "sinon"; @@ -14,41 +14,6 @@ const MOCK_JSON = { { type: "element", name: "w:hdr", - attributes: { - "xmlns:wpc": "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", - "xmlns:cx": "http://schemas.microsoft.com/office/drawing/2014/chartex", - "xmlns:cx1": "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex", - "xmlns:cx2": "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex", - "xmlns:cx3": "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex", - "xmlns:cx4": "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex", - "xmlns:cx5": "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex", - "xmlns:cx6": "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex", - "xmlns:cx7": "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex", - "xmlns:cx8": "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex", - "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006", - "xmlns:aink": "http://schemas.microsoft.com/office/drawing/2016/ink", - "xmlns:am3d": "http://schemas.microsoft.com/office/drawing/2017/model3d", - "xmlns:o": "urn:schemas-microsoft-com:office:office", - "xmlns:oel": "http://schemas.microsoft.com/office/2019/extlst", - "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", - "xmlns:m": "http://schemas.openxmlformats.org/officeDocument/2006/math", - "xmlns:v": "urn:schemas-microsoft-com:vml", - "xmlns:wp14": "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing", - "xmlns:wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", - "xmlns:w10": "urn:schemas-microsoft-com:office:word", - "xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", - "xmlns:w14": "http://schemas.microsoft.com/office/word/2010/wordml", - "xmlns:w15": "http://schemas.microsoft.com/office/word/2012/wordml", - "xmlns:w16cex": "http://schemas.microsoft.com/office/word/2018/wordml/cex", - "xmlns:w16cid": "http://schemas.microsoft.com/office/word/2016/wordml/cid", - "xmlns:w16": "http://schemas.microsoft.com/office/word/2018/wordml", - "xmlns:w16sdtdh": "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash", - "xmlns:w16se": "http://schemas.microsoft.com/office/word/2015/wordml/symex", - "xmlns:wpg": "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup", - "xmlns:wpi": "http://schemas.microsoft.com/office/word/2010/wordprocessingInk", - "xmlns:wne": "http://schemas.microsoft.com/office/word/2006/wordml", - "xmlns:wps": "http://schemas.microsoft.com/office/word/2010/wordprocessingShape", - }, elements: [ { type: "element", @@ -106,7 +71,7 @@ describe("replacer", () => { }); }); - it("should return the same object if nothing is added", () => { + it("should replace paragraph type", () => { const output = replacer( MOCK_JSON, { @@ -149,5 +114,93 @@ describe("replacer", () => { expect(JSON.stringify(output)).to.contain("Delightful Header"); }); + + it("should replace document type", () => { + const output = replacer( + MOCK_JSON, + { + type: PatchType.DOCUMENT, + children: [new Paragraph("Lorem ipsum paragraph")], + }, + "{{header_adjective}}", + [ + { + text: "This is a {{header_adjective}} don’t you think?", + runs: [ + { + text: "This is a {{head", + parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }], + index: 1, + start: 0, + end: 15, + }, + { text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 }, + { + text: "_adjective}} don’t you think?", + parts: [{ text: "_adjective}} don’t you think?", index: 0, start: 18, end: 46 }], + index: 3, + start: 18, + end: 46, + }, + ], + index: 0, + path: [0, 0, 0], + }, + ], + { + file: {} as unknown as File, + viewWrapper: { + Relationships: {}, + } as unknown as IViewWrapper, + stack: [], + }, + ); + + expect(JSON.stringify(output)).to.contain("Lorem ipsum paragraph"); + }); + + it("should throw an error if the type is not supported", () => { + expect(() => + replacer( + {}, + { + type: PatchType.DOCUMENT, + children: [new Paragraph("Lorem ipsum paragraph")], + }, + "{{header_adjective}}", + [ + { + text: "This is a {{header_adjective}} don’t you think?", + runs: [ + { + text: "This is a {{head", + parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }], + index: 1, + start: 0, + end: 15, + }, + { text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 }, + { + text: "_adjective}} don’t you think?", + parts: [{ text: "_adjective}} don’t you think?", index: 0, start: 18, end: 46 }], + index: 3, + start: 18, + end: 46, + }, + ], + index: 0, + path: [0, 0, 0], + }, + ], + { + file: {} as unknown as File, + viewWrapper: { + Relationships: {}, + } as unknown as IViewWrapper, + stack: [], + }, + ), + ).to.throw(); + }); }); }); diff --git a/src/patcher/replacer.ts b/src/patcher/replacer.ts index 45861de8e7..bb0190ac51 100644 --- a/src/patcher/replacer.ts +++ b/src/patcher/replacer.ts @@ -27,25 +27,31 @@ export const replacer = ( .map((c) => toJson(xml(formatter.format(c as XmlComponent, context)))) .map((c) => c.elements![0]); - if (patch.type === PatchType.DOCUMENT) { - const parentElement = goToParentElementFromPath(json, renderedParagraph.path); - const elementIndex = getLastElementIndexFromPath(renderedParagraph.path); - // eslint-disable-next-line functional/immutable-data, prefer-destructuring - parentElement.elements?.splice(elementIndex, 1, ...textJson); - } else if (patch.type === PatchType.PARAGRAPH) { - const paragraphElement = goToElementFromPath(json, renderedParagraph.path); - replaceTokenInParagraphElement({ - paragraphElement, - renderedParagraph, - originalText: patchText, - replacementText: SPLIT_TOKEN, - }); + switch (patch.type) { + case PatchType.DOCUMENT: { + const parentElement = goToParentElementFromPath(json, renderedParagraph.path); + const elementIndex = getLastElementIndexFromPath(renderedParagraph.path); + // eslint-disable-next-line functional/immutable-data, prefer-destructuring + parentElement.elements!.splice(elementIndex, 1, ...textJson); + break; + } + case PatchType.PARAGRAPH: + default: { + const paragraphElement = goToElementFromPath(json, renderedParagraph.path); + replaceTokenInParagraphElement({ + paragraphElement, + renderedParagraph, + originalText: patchText, + 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, right); + const { left, right } = splitRunElement(paragraphElement.elements![index], SPLIT_TOKEN); + // eslint-disable-next-line functional/immutable-data + paragraphElement.elements!.splice(index, 1, left, ...textJson, right); + break; + } } } diff --git a/src/patcher/traverser.ts b/src/patcher/traverser.ts index 5ee2f792ff..52112e10b9 100644 --- a/src/patcher/traverser.ts +++ b/src/patcher/traverser.ts @@ -20,28 +20,24 @@ export const findLocationOfText = (node: Element, text: string): readonly IRende // eslint-disable-next-line functional/prefer-readonly-type const queue: ElementWrapper[] = [ - ...(elementsToWrapper({ + ...elementsToWrapper({ element: node, index: 0, parent: undefined, - }) ?? []), + }), ]; // eslint-disable-next-line functional/immutable-data let currentNode: ElementWrapper | undefined; while (queue.length > 0) { // eslint-disable-next-line functional/immutable-data - currentNode = queue.shift(); - - if (!currentNode) { - break; - } + currentNode = queue.shift()!; // This is safe because we check the length of the queue if (currentNode.element.name === "w:p") { renderedParagraphs = [...renderedParagraphs, renderParagraphNode(currentNode)]; } else { // eslint-disable-next-line functional/immutable-data - queue.push(...(elementsToWrapper(currentNode) ?? [])); + queue.push(...elementsToWrapper(currentNode)); } } From b63a6e6e163c88a92605eab7e8a56e27169da8bd Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Thu, 16 Mar 2023 02:02:02 +0000 Subject: [PATCH 23/26] Update coverage stats --- .nycrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.nycrc b/.nycrc index 3dc8ad302a..6c47fc4974 100644 --- a/.nycrc +++ b/.nycrc @@ -1,9 +1,9 @@ { "check-coverage": true, "statements": 99.79, - "branches": 98.41, + "branches": 98.17, "functions": 100, - "lines": 99.73, + "lines": 99.78, "include": [ "src/**/*.ts" ], From 338f7be967757b1be4da105701267d10a86c692a Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Thu, 16 Mar 2023 03:14:01 +0000 Subject: [PATCH 24/26] Add chai as promised and fix async test --- package-lock.json | 41 ++++++ package.json | 2 + src/patcher/from-docx.spec.ts | 266 ++++++++++++++++++---------------- 3 files changed, 185 insertions(+), 124 deletions(-) diff --git a/package-lock.json b/package-lock.json index 13f3f32f2b..c4f335e75c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ }, "devDependencies": { "@types/chai": "^4.2.15", + "@types/chai-as-promised": "^7.1.5", "@types/glob": "^8.0.0", "@types/mocha": "^10.0.0", "@types/prompt": "^1.1.1", @@ -29,6 +30,7 @@ "@typescript-eslint/parser": "^5.36.1", "buffer": "^6.0.3", "chai": "^4.3.6", + "chai-as-promised": "^7.1.1", "cspell": "^6.2.2", "docsify-cli": "^4.3.0", "eslint": "^8.23.0", @@ -1189,6 +1191,15 @@ "integrity": "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==", "dev": true }, + "node_modules/@types/chai-as-promised": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.5.tgz", + "integrity": "sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -2865,6 +2876,18 @@ "node": ">=4" } }, + "node_modules/chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 5" + } + }, "node_modules/chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", @@ -12547,6 +12570,15 @@ "integrity": "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==", "dev": true }, + "@types/chai-as-promised": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.5.tgz", + "integrity": "sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -13778,6 +13810,15 @@ "type-detect": "^4.0.5" } }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "^1.0.2" + } + }, "chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", diff --git a/package.json b/package.json index c08a2678b2..621c6b9fb9 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "homepage": "https://github.com/dolanmiu/docx#readme", "devDependencies": { "@types/chai": "^4.2.15", + "@types/chai-as-promised": "^7.1.5", "@types/glob": "^8.0.0", "@types/mocha": "^10.0.0", "@types/prompt": "^1.1.1", @@ -76,6 +77,7 @@ "@typescript-eslint/parser": "^5.36.1", "buffer": "^6.0.3", "chai": "^4.3.6", + "chai-as-promised": "^7.1.1", "cspell": "^6.2.2", "docsify-cli": "^4.3.0", "eslint": "^8.23.0", diff --git a/src/patcher/from-docx.spec.ts b/src/patcher/from-docx.spec.ts index 9fa8b45119..296c3d93d7 100644 --- a/src/patcher/from-docx.spec.ts +++ b/src/patcher/from-docx.spec.ts @@ -1,11 +1,15 @@ -import { expect } from "chai"; +import * as chai from "chai"; import * as sinon from "sinon"; import * as JSZip from "jszip"; +import * as chaiAsPromised from "chai-as-promised"; import { ExternalHyperlink, ImageRun, Paragraph, TextRun } from "@file/paragraph"; import { patchDocument, PatchType } from "./from-docx"; +chai.use(chaiAsPromised); +const { expect } = chai; + const MOCK_XML = ` { describe("patchDocument", () => { - beforeEach(() => { - sinon.createStubInstance(JSZip, {}); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sinon.stub(JSZip, "loadAsync").callsFake( - () => - new Promise((resolve) => { - const zip = new JSZip(); + describe("document.xml and [Content_Types].xml", () => { + before(() => { + sinon.createStubInstance(JSZip, {}); + sinon.stub(JSZip, "loadAsync").callsFake( + () => + new Promise((resolve) => { + const zip = new JSZip(); - zip.file("word/document.xml", MOCK_XML); - zip.file("[Content_Types].xml", ``); - resolve(zip); - }), - ); - }); - - afterEach(() => { - (JSZip.loadAsync as unknown as sinon.SinonStub).restore(); - }); - - it("should patch the document", async () => { - const output = await patchDocument(Buffer.from(""), { - patches: { - name: { - type: PatchType.PARAGRAPH, - children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")], - }, - item_1: { - type: PatchType.PARAGRAPH, - children: [ - new TextRun("#657"), - new ExternalHyperlink({ - children: [ - new TextRun({ - text: "BBC News Link", - }), - ], - link: "https://www.bbc.co.uk/news", - }), - ], - }, - // eslint-disable-next-line @typescript-eslint/naming-convention - paragraph_replace: { - type: PatchType.DOCUMENT, - children: [ - new Paragraph({ - children: [ - new TextRun("This is a "), - new ExternalHyperlink({ - children: [ - new TextRun({ - text: "Google Link", - }), - ], - link: "https://www.google.co.uk", - }), - new ImageRun({ - data: Buffer.from(""), - transformation: { width: 100, height: 100 }, - }), - ], - }), - ], - }, - // eslint-disable-next-line @typescript-eslint/naming-convention - image_test: { - type: PatchType.PARAGRAPH, - children: [ - new ImageRun({ - data: Buffer.from(""), - transformation: { width: 100, height: 100 }, - }), - ], - }, - }, + zip.file("word/document.xml", MOCK_XML); + zip.file("[Content_Types].xml", ``); + resolve(zip); + }), + ); }); - expect(output).to.not.be.undefined; - }); - it("should patch the document", async () => { - const output = await patchDocument(Buffer.from(""), { - patches: {}, + after(() => { + (JSZip.loadAsync as unknown as sinon.SinonStub).restore(); }); - expect(output).to.not.be.undefined; - }); - it("should use the relationships file rather than create one", () => { - (JSZip.loadAsync as unknown as sinon.SinonStub).restore(); - sinon.createStubInstance(JSZip, {}); - sinon.stub(JSZip, "loadAsync").callsFake( - () => - new Promise((resolve) => { - const zip = new JSZip(); - - zip.file("word/document.xml", MOCK_XML); - zip.file("word/_rels/document.xml.rels", ``); - zip.file("[Content_Types].xml", ``); - resolve(zip); - }), - ); - - const output = patchDocument(Buffer.from(""), { - patches: { - // eslint-disable-next-line @typescript-eslint/naming-convention - image_test: { - type: PatchType.PARAGRAPH, - children: [ - new ImageRun({ - data: Buffer.from(""), - transformation: { width: 100, height: 100 }, - }), - ], + it("should patch the document", async () => { + const output = await patchDocument(Buffer.from(""), { + patches: { + name: { + type: PatchType.PARAGRAPH, + children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")], + }, + item_1: { + type: PatchType.PARAGRAPH, + children: [ + new TextRun("#657"), + new ExternalHyperlink({ + children: [ + new TextRun({ + text: "BBC News Link", + }), + ], + link: "https://www.bbc.co.uk/news", + }), + ], + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + paragraph_replace: { + type: PatchType.DOCUMENT, + children: [ + new Paragraph({ + children: [ + new TextRun("This is a "), + new ExternalHyperlink({ + children: [ + new TextRun({ + text: "Google Link", + }), + ], + link: "https://www.google.co.uk", + }), + new ImageRun({ + data: Buffer.from(""), + transformation: { width: 100, height: 100 }, + }), + ], + }), + ], + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + image_test: { + type: PatchType.PARAGRAPH, + children: [ + new ImageRun({ + data: Buffer.from(""), + transformation: { width: 100, height: 100 }, + }), + ], + }, }, - }, + }); + expect(output).to.not.be.undefined; + }); + + it("should patch the document", async () => { + const output = await patchDocument(Buffer.from(""), { + patches: {}, + }); + expect(output).to.not.be.undefined; }); - expect(output).to.not.be.undefined; }); - it("should throw an error if the content types is not found", () => { - (JSZip.loadAsync as unknown as sinon.SinonStub).restore(); - sinon.createStubInstance(JSZip, {}); - sinon.stub(JSZip, "loadAsync").callsFake( - () => - new Promise((resolve) => { - const zip = new JSZip(); + describe("document.xml and [Content_Types].xml with relationships", () => { + before(() => { + sinon.createStubInstance(JSZip, {}); + sinon.stub(JSZip, "loadAsync").callsFake( + () => + new Promise((resolve) => { + const zip = new JSZip(); - zip.file("word/document.xml", MOCK_XML); - resolve(zip); - }), - ); + zip.file("word/document.xml", MOCK_XML); + zip.file("word/_rels/document.xml.rels", ``); + zip.file("[Content_Types].xml", ``); + resolve(zip); + }), + ); + }); - expect(() => - patchDocument(Buffer.from(""), { + after(() => { + (JSZip.loadAsync as unknown as sinon.SinonStub).restore(); + }); + + it("should use the relationships file rather than create one", async () => { + const output = await patchDocument(Buffer.from(""), { patches: { // eslint-disable-next-line @typescript-eslint/naming-convention image_test: { @@ -345,8 +325,46 @@ describe("from-docx", () => { ], }, }, - }), - ).to.throw(); + }); + expect(output).to.not.be.undefined; + }); + }); + + describe("document.xml", () => { + before(() => { + sinon.createStubInstance(JSZip, {}); + sinon.stub(JSZip, "loadAsync").callsFake( + () => + new Promise((resolve) => { + const zip = new JSZip(); + + zip.file("word/document.xml", MOCK_XML); + resolve(zip); + }), + ); + }); + + after(() => { + (JSZip.loadAsync as unknown as sinon.SinonStub).restore(); + }); + + it("should throw an error if the content types is not found", () => + expect( + patchDocument(Buffer.from(""), { + patches: { + // eslint-disable-next-line @typescript-eslint/naming-convention + image_test: { + type: PatchType.PARAGRAPH, + children: [ + new ImageRun({ + data: Buffer.from(""), + transformation: { width: 100, height: 100 }, + }), + ], + }, + }, + }), + ).to.eventually.be.rejected); }); }); }); From d02f98956f24e8ca44a6e352a61f6aec8501c51d Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Thu, 16 Mar 2023 03:24:51 +0000 Subject: [PATCH 25/26] Fix spelling errors --- docs/contribution-guidelines.md | 1 + src/patcher/traverser.spec.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contribution-guidelines.md b/docs/contribution-guidelines.md index 9f53eff40c..8aca6bcabc 100644 --- a/docs/contribution-guidelines.md +++ b/docs/contribution-guidelines.md @@ -6,6 +6,7 @@ // http://officeopenxml.com/WPdocument.php ``` + It can be a link to `officeopenxml.com` or `datypic.com` etc. It could also be a reference to the official ECMA-376 standard: https://www.ecma-international.org/publications-and-standards/standards/ecma-376/ diff --git a/src/patcher/traverser.spec.ts b/src/patcher/traverser.spec.ts index 65ec717e3f..d961bae0bb 100644 --- a/src/patcher/traverser.spec.ts +++ b/src/patcher/traverser.spec.ts @@ -41,7 +41,6 @@ const MOCK_JSON = { "xmlns:wpi": "http://schemas.microsoft.com/office/word/2010/wordprocessingInk", "xmlns:wne": "http://schemas.microsoft.com/office/word/2006/wordml", "xmlns:wps": "http://schemas.microsoft.com/office/word/2010/wordprocessingShape", - "mc:Ignorable": "w14 w15 w16se w16cid w16 w16cex w16sdtdh wp14", }, elements: [ { From 4fb8d277b4c4540c4ab2f2ceedcac643f326d314 Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Thu, 16 Mar 2023 20:33:54 +0000 Subject: [PATCH 26/26] Change demo import from src to build --- demo/85-template-document.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index 69015202f1..6e41fa1a43 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -14,7 +14,7 @@ import { TextDirection, TextRun, VerticalAlign, -} from "../src"; +} from "../build"; patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { patches: {