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 dab8b9e7c2..83f972c48a 100644 Binary files a/demo/assets/simple-template.docx and b/demo/assets/simple-template.docx differ 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); } }