UNPKG

docxtemplater

Version:

Generate docx, pptx, and xlsx from templates (Word, Powerpoint and Excel documents), from Node.js, the Browser and the command line

949 lines (943 loc) 29.3 kB
"use strict"; var _require = require("../utils.js"), expectToThrow = _require.expectToThrow, expectToThrowSnapshot = _require.expectToThrowSnapshot, shouldBeSame = _require.shouldBeSame, createDocV4 = _require.createDocV4, captureLogs = _require.captureLogs, expect = _require.expect, getZip = _require.getZip; var expressionParser = require("../../expressions.js"); var proofStateModule = require("../../proof-state-module.js"); var inspectModule = require("../../inspect-module.js"); var Docxtemplater = require("../../docxtemplater.js"); var _require2 = require("../../doc-utils.js"), pushArray = _require2.pushArray, traits = _require2.traits, uniq = _require2.uniq; var fixDocPrCorruption = require("../../modules/fix-doc-pr-corruption.js"); function uniqTimes(arr) { var times = {}, result = []; for (var _i2 = 0; _i2 < arr.length; _i2++) { var el = arr[_i2]; if (times[el] == null) { times[el] = 0; result.push([el]); } times[el] += 1; } for (var _i4 = 0; _i4 < result.length; _i4++) { var item = result[_i4]; item[1] = times[item[0]]; } return result; } describe("Verify apiversion", function () { it("should work with valid api version", function () { var module = { name: "Mymod", requiredAPIVersion: "3.23.0", render: function render(part) { return part.value; } }; createDocV4("tag-example.docx", { modules: [module] }); }); it("should fail with invalid api version", function () { var module = { name: "Mymod", requiredAPIVersion: "3.92.0", render: function render(part) { return part.value; } }; expectToThrowSnapshot(function () { return createDocV4("loop-valid.docx", { modules: [module] }); }); }); it("should fail when trying to attach null module", function () { expectToThrow(function () { return new Docxtemplater(getZip("loop-valid.docx"), { modules: [null] }); }, Error, { message: "Cannot attachModule with a falsy value", name: "InternalError", properties: {} }); }); }); describe("Module attachment", function () { it("should not allow to attach the same module twice", function () { var module = { name: "TestModule", requiredAPIVersion: "3.0.0", render: function render(part) { return part.value; } }; createDocV4("loop-valid.docx", { modules: [module] }); var errMessage = null; try { createDocV4("tag-example.docx", { modules: [module] }); } catch (e) { errMessage = e.message; } expect(errMessage).to.equal('Cannot attach a module that was already attached : "TestModule". The most likely cause is that you are instantiating the module at the root level, and using it for multiple instances of Docxtemplater'); }); it("should allow to attach the same module twice if it has a clone method", function () { var module = { name: "TestModule", requiredAPIVersion: "3.0.0", render: function render(part) { return part.value; }, clone: function clone() { return this; } }; createDocV4("loop-valid.docx", { modules: [module] }); createDocV4("tag-example.docx", { modules: [module] }); createDocV4("tag-example.docx", { modules: [module] }); }); it("should automatically detach inspect module", function () { var imodule = inspectModule(); createDocV4("loop-valid.docx", { modules: [imodule] }).render(); createDocV4("loop-valid.docx", { modules: [imodule] }).render(); }); }); describe("Module xml parse", function () { it("should not mutate options (regression for issue #526)", function () { var module = { name: "FooModule", requiredAPIVersion: "3.0.0", optionsTransformer: function optionsTransformer(options, docxtemplater) { var relsFiles = docxtemplater.zip.file(/document.xml.rels/).map(function (file) { return file.name; }); pushArray(options.xmlFileNames, relsFiles); return options; } }; var opts = { modules: [module] }; createDocV4("tag-example.docx", opts); delete opts.modules; expect(opts).to.deep.equal({}); }); it("should be possible to parse xml files : xmlFileNames.push() without side effect fixed since v3.55.0", function () { var xmlDocuments; var module = { name: "ParseXMLModule", requiredAPIVersion: "3.0.0", optionsTransformer: function optionsTransformer(options, docxtemplater) { var relsFiles = docxtemplater.zip.file(/document.xml.rels/).map(function (_ref) { var name = _ref.name; return name; }); /* * This part tests that you can mutate the options here without * mutating it for future documents * Fixed since 3.55.0 */ pushArray(options.xmlFileNames, relsFiles); return options; }, set: function set(options) { if (options.xmlDocuments) { xmlDocuments = options.xmlDocuments; } } }; var doc = createDocV4("tag-example.docx", { modules: [module] }); var xmlKeys = Object.keys(xmlDocuments); expect(xmlKeys).to.deep.equal(["[Content_Types].xml", "word/_rels/document.xml.rels"]); var rels = xmlDocuments["word/_rels/document.xml.rels"].getElementsByTagName("Relationship"); expect(rels.length).to.equal(10); rels[5].setAttribute("Foobar", "Baz"); doc.render(); shouldBeSame({ doc: doc, expectedName: "expected-module-change-rels.docx" }); }); }); describe("Module unique tags xml", function () { it("should not cause an issue if tagsXmlLexedArray contains duplicates", function () { var module = { name: "FooModule", requiredAPIVersion: "3.0.0", optionsTransformer: function optionsTransformer(options, docxtemplater) { docxtemplater.fileTypeConfig.tagsXmlLexedArray.push("w:p", "w:r", "w:p"); return options; } }; var doc = createDocV4("tag-example.docx", { modules: [module] }); doc.render({ first_name: "Hipp", last_name: "Edgar", phone: "0652455478", description: "New Website" }); shouldBeSame({ doc: doc, expectedName: "expected-tag-example.docx" }); }); }); describe("Module traits", function () { it("should not cause an issue if using traits.expandTo containing loop", function () { var moduleName = "comment-module"; function getInner(_ref2) { var part = _ref2.part, leftParts = _ref2.leftParts, rightParts = _ref2.rightParts, postparse = _ref2.postparse; part.subparsed = postparse([].concat(leftParts).concat(rightParts), { basePart: part }); return part; } var module = { name: "Test module", requiredAPIVersion: "3.0.0", parse: function parse(placeHolderContent) { if (placeHolderContent[0] === "£") { var type = "placeholder"; return { type: type, value: placeHolderContent.substr(1), module: moduleName }; } }, postparse: function postparse(parsed, _ref3) { var postparse = _ref3.postparse; parsed = traits.expandToOne(parsed, { moduleName: moduleName, getInner: getInner, expandTo: ["w:p"], postparse: postparse }); return parsed; }, render: function render(part) { if (part.module === moduleName) { return { value: "" }; } } }; var doc = createDocV4("comment-with-loop.docx", { modules: [module] }); doc.render({}); shouldBeSame({ doc: doc, expectedName: "expected-comment-example.docx" }); }); }); describe("Module errors", function () { it("should pass the errors to errorsTransformer", function () { var moduleName = "ErrorModule"; var catched = null; var myErrors = []; var module = { name: "Error module", requiredAPIVersion: "3.0.0", parse: function parse(placeHolderContent) { var type = "placeholder"; return { type: type, value: placeHolderContent, module: moduleName }; }, render: function render(part) { if (part.module === moduleName) { return { errors: [new Error("foobar ".concat(part.value))] }; } }, errorsTransformer: function errorsTransformer(errors) { pushArray(myErrors, errors); return errors.map(function (e) { e.xyz = "xxx"; return e; }); } }; var doc = createDocV4("tag-example.docx", { modules: [module] }); var capture = captureLogs(); try { doc.render(); } catch (e) { catched = e; } capture.stop(); expect(catched.properties.errors[0].xyz).to.equal("xxx"); expect(myErrors.length).to.equal(9); expect(myErrors[0].message).to.equal("foobar last_name"); }); it("should log the error that is returned from render", function () { var moduleName = "ErrorModule"; var module = { name: "Error module", requiredAPIVersion: "3.0.0", parse: function parse(placeHolderContent) { var type = "placeholder"; return { type: type, value: placeHolderContent, module: moduleName }; }, render: function render(part) { if (part.module === moduleName) { return { errors: [new Error("foobar ".concat(part.value))] }; } } }; var error = null; var doc = createDocV4("tag-example.docx", { modules: [module] }); var capture = captureLogs(); try { doc.render(); } catch (e) { error = e; } capture.stop(); expect(error).to.be.an("object"); expect(error.message).to.equal("Multi error"); expect(error.properties.errors.length).to.equal(9); expect(error.properties.errors[4].properties.file).to.equal("word/document.xml"); expect(error.properties.errors[4].message).to.equal("foobar last_name"); expect(error.properties.errors[5].message).to.equal("foobar first_name"); // expect(error.properties.errors[2].message).to.equal("foobar phone"); var logs = capture.logs(); expect(logs.length).to.equal(1, "Incorrect logs count"); expect(logs[0]).to.contain("foobar last_name"); expect(logs[0]).to.contain("foobar first_name"); expect(logs[0]).to.contain("foobar phone"); expect(logs[0]).to.satisfy(function (log) { return ( // for chrome log.indexOf(".render") !== -1 || // for firefox log.indexOf("render@") !== -1 || // for bun (https://bun.sh/) log.indexOf("render (") !== -1 ); }); var parsedLog = JSON.parse(logs[0]); expect(parsedLog.error.length).to.equal(9); expect(error.properties.errors[0].properties.file).to.equal("word/header1.xml"); expect(error.properties.errors[0].message).to.equal("foobar last_name"); expect(error.properties.errors[1].message).to.equal("foobar first_name"); expect(error.properties.errors[2].message).to.equal("foobar phone"); expect(error.properties.errors[6].properties.file).to.equal("word/footer1.xml"); expect(error.properties.errors[6].message).to.equal("foobar last_name"); }); it("should throw specific error if adding same module twice", function () { var mod1 = { name: "TestModule", set: function set() { return null; } }; var mod2 = { name: "TestModule", set: function set() { return null; } }; // This test will test the case where the fixDocPrCorruption is used on two different instances of the docxtemplater library expectToThrow(function () { return createDocV4("loop-image-footer.docx", { modules: [mod1, mod2] }); }, Error, { message: 'Detected duplicate module "TestModule"', name: "InternalError", properties: {} }); }); }); describe("Module should pass options to module.parse, module.postparse, module.render, module.postrender", function () { it("should pass filePath and contentType options", function () { var filePaths = [], relsType = [], ct = []; var renderFP = "", renderCT = "", postrenderFP = "", postrenderCT = "", postparseFP = "", postparseCT = ""; var module = { name: "Test module", requiredAPIVersion: "3.0.0", parse: function parse(a, options) { filePaths.push(options.filePath); ct.push(options.contentType); relsType.push(options.relsType); }, postparse: function postparse(a, options) { postparseFP = options.filePath; postparseCT = options.contentType; return a; }, render: function render(a, options) { renderFP = options.filePath; renderCT = options.contentType; }, postrender: function postrender(a, options) { postrenderFP = options.filePath; postrenderCT = options.contentType; return a; } }; createDocV4("tag-example.docx", { modules: [module] }).render({}); expect(renderFP).to.equal("word/footnotes.xml"); expect(renderCT).to.equal("application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"); expect(postparseFP).to.equal("word/footnotes.xml"); expect(postparseCT).to.equal("application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"); expect(postrenderFP).to.equal("word/footnotes.xml"); expect(postrenderCT).to.equal("application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"); /* * The order of the filePaths here is important, this has been fixed in * version 3.37.8 : First headers are templated, than the document, * than the footers. */ expect(filePaths).to.deep.equal([ // Header appears 4 times because there are 4 tags in the header "word/header1.xml", "word/header1.xml", "word/header1.xml", "word/header1.xml", // Document appears 2 times because there are 2 tags in the header "word/document.xml", "word/document.xml", // Footer appears 3 times because there are 3 tags in the header "word/footer1.xml", "word/footer1.xml", "word/footer1.xml"]); expect(ct).to.deep.equal(["application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"]); expect(relsType).to.deep.equal([undefined, // match to header1.xml undefined, // match to header1.xml undefined, // match to header1.xml undefined, // match to header1.xml "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", // match to document.xml "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", // match to document.xml undefined, // match to footer1.xml undefined, // match to footer1.xml undefined // match to footer1.xml ]); }); }); describe("Module detachment", function () { it("should detach the module when the module does not support the document filetype", function () { var isModuleCalled = false; var isDetachedCalled = false; var module = { optionsTransformer: function optionsTransformer(options) { isModuleCalled = true; return options; }, on: function on(eventName) { if (eventName === "detached") { isDetachedCalled = true; } }, supportedFileTypes: ["pptx"] }; createDocV4("tag-example.docx", { modules: [module] }); expect(isDetachedCalled).to.equal(true); expect(isModuleCalled).to.equal(false); }); }); describe("Module Matcher API", function () { it("should call onMatch function", function () { function module1() { var myVal = ""; return { name: "module1", matchers: function matchers() { return [["l", "module-m1", { onMatch: function onMatch(part) { myVal = part.prefix + part.lIndex + "!!"; } }]]; }, render: function render(part) { if (part.module === "module-m1") { return { value: myVal }; } } }; } expect(this.renderV4({ name: "tag-example.docx", options: { modules: [module1()] }, data: { first_name: "John" }, expectedText: "l28!! John" })); }); it("should automatically choose module with longest value", function () { function module1() { return { name: "module1", matchers: function matchers() { return [["l", "module-m1"]]; }, render: function render(part) { if (part.module === "module-m1") { return { value: part.value }; } } }; } function module2() { return { name: "module2", matchers: function matchers() { return [[/last_(.*)/, "module-m2"]]; }, render: function render(part) { if (part.module === "module-m2") { return { value: part.value }; } } }; } function module3() { return { name: "module3", matchers: function matchers() { return [["last", "module-m3"]]; }, render: function render(part) { if (part.module === "module-m3") { return { value: part.value }; } } }; } expect(createDocV4("tag-example.docx", { modules: [module1(), module2(), module3()] }).render({ first_name: "John" }).getFullText()).to.equal("name John"); expect(createDocV4("tag-example.docx", { modules: [module3(), module2(), module1()] }).render({ first_name: "John" }).getFullText()).to.equal("name John"); }); }); describe("Fix doc pr corruption module", function () { it("should work on multiple instances in parallel", function () { var doc = createDocV4("loop-image-footer.docx", { modules: [fixDocPrCorruption] }); // This test will test the case where the fixDocPrCorruption is used on two different instances of the docxtemplater library createDocV4("tag-example.docx", { modules: [fixDocPrCorruption] }); return doc.renderAsync({ loop: [1, 2, 3, 4] }).then(function () { shouldBeSame({ doc: doc, expectedName: "expected-loop-images-footer.docx" }); }); }); }); describe("Proofstate module", function () { it("should work with angular parser with proofstate module", function () { shouldBeSame({ doc: createDocV4("angular-example.docx", { parser: expressionParser, modules: [proofStateModule] }).render({ person: { first_name: "Hipp", last_name: "Edgar", birth_year: 1955, age: 59 } }), expectedName: "expected-proofstate-removed.docx" }); }); }); describe("Module calls to on(eventName) to pass events", function () { it("should work with v4 synchronously", function () { var calls = []; var mod = { name: "TestModule", on: function on(eventName) { calls.push(eventName); } }; createDocV4("loop-image-footer.docx", { modules: [mod] }).render({ loop: [1, 2, 3, 4] }); expect(calls).to.deep.equal(["attached", "before-preparse", "after-preparse", "after-parse", "after-postparse", "syncing-zip", "synced-zip"]); }); }); describe("Module call order", function () { it("should work with v4 synchronously", function () { var calls = []; var mod = { name: "TestModule", set: function set() { calls.push("set"); return null; }, matchers: function matchers() { calls.push("matchers"); return []; }, render: function render() { calls.push("render"); return null; }, optionsTransformer: function optionsTransformer(options) { calls.push("optionsTransformer"); return options; }, preparse: function preparse() { calls.push("preparse"); return null; }, parse: function parse() { calls.push("parse"); return null; }, postparse: function postparse() { calls.push("postparse"); return null; }, getTraits: function getTraits() { calls.push("getTraits"); }, getFileType: function getFileType() { calls.push("getFileType"); }, nullGetter: function nullGetter() { calls.push("nullGetter"); }, postrender: function postrender() { calls.push("postrender"); return []; }, errorsTransformer: function errorsTransformer() { calls.push("errorsTransformer"); }, getRenderedMap: function getRenderedMap(obj) { calls.push("getRenderedMap"); return obj; }, on: function on() { calls.push("on"); }, resolve: function resolve() { calls.push("resolve"); } }; createDocV4("loop-image-footer.docx", { modules: [mod] }).render({ loop: [1, 2, 3, 4] }); expect(uniq(calls)).to.deep.equal(["on", "set", "getFileType", "optionsTransformer", "preparse", "matchers", "getTraits", "postparse", "errorsTransformer", "getRenderedMap", "render", "postrender"]); }); it("should work with v4 async", function () { var calls = [], parseValues = [], preparsedFilePaths = []; var mod = { name: "TestModule", set: function set() { calls.push("set"); return null; }, matchers: function matchers() { calls.push("matchers"); return []; }, render: function render() { calls.push("render"); return null; }, optionsTransformer: function optionsTransformer(options) { calls.push("optionsTransformer"); return options; }, preparse: function preparse(_, options) { preparsedFilePaths.push(options.filePath); calls.push("preparse"); return null; }, parse: function parse(part) { parseValues.push(part); calls.push("parse"); return null; }, postparse: function postparse() { calls.push("postparse"); return null; }, getTraits: function getTraits() { calls.push("getTraits"); }, getFileType: function getFileType() { calls.push("getFileType"); }, nullGetter: function nullGetter() { calls.push("nullGetter"); }, postrender: function postrender() { calls.push("postrender"); return []; }, errorsTransformer: function errorsTransformer() { calls.push("errorsTransformer"); }, getRenderedMap: function getRenderedMap(obj) { calls.push("getRenderedMap"); return obj; }, on: function on() { calls.push("on"); }, resolve: function resolve() { calls.push("resolve"); } }; return createDocV4("tag-example.docx", { modules: [mod] }).renderAsync({ loop: [1, 2, 3, 4] }).then(function () { expect(parseValues).to.deep.equal(["last_name", "first_name", "phone", "description", "last_name", "first_name", "last_name", "first_name", "phone"]); expect(parseValues.length).to.equal(9); expect(preparsedFilePaths).to.deep.equal(["word/settings.xml", "docProps/core.xml", "docProps/app.xml", "word/header1.xml", "word/document.xml", "word/footer1.xml", "word/footnotes.xml"]); expect(preparsedFilePaths.length).to.equal(7); expect(uniqTimes(calls)).to.deep.equal([ // Runs on("attached") (module is attached), after-preparse, ... ["on", 7], // Runs multiple times to set xmllexed, filePath, parsed ["set", 62], /* * Runs Twice, it should theoretically run once * #tofix-getFileType-twice . However for some modules, if * you run it just once, the modules break (this is in * particular if you use const doc = new Docxtemplater(); * doc.attachModule() which is now deprecated */ ["getFileType", 2], // Runs Once ["optionsTransformer", 1], // Runs for each templatedFile ["preparse", 7], // Runs for each tag ["matchers", 9], // Runs for each tag ["parse", 9], // Runs for each templatedFile ["getTraits", 7], // Runs for each templatedFile ["postparse", 7], // Runs for each templatedFile ["errorsTransformer", 7], // Runs Once ["getRenderedMap", 1], // Runs for each tag * data ["resolve", 9], // Runs for each tag * data ["nullGetter", 9], // Runs for each xml tag or placeholder tag ["render", 240], // Runs for each templatedFile ["postrender", 7]]); }); }); it("should work with v3", function () { var calls = []; var mod = { name: "TestModule", set: function set() { calls.push("set"); return null; }, matchers: function matchers() { calls.push("matchers"); return []; }, render: function render() { calls.push("render"); return null; }, optionsTransformer: function optionsTransformer(options) { calls.push("optionsTransformer"); return options; }, preparse: function preparse() { calls.push("preparse"); return null; }, parse: function parse() { calls.push("parse"); return null; }, postparse: function postparse() { calls.push("postparse"); return null; }, getTraits: function getTraits() { calls.push("getTraits"); }, getFileType: function getFileType() { calls.push("getFileType"); }, nullGetter: function nullGetter() { calls.push("nullGetter"); }, postrender: function postrender() { calls.push("postrender"); return []; }, errorsTransformer: function errorsTransformer() { calls.push("errorsTransformer"); }, getRenderedMap: function getRenderedMap(obj) { calls.push("getRenderedMap"); return obj; }, on: function on() { calls.push("on"); }, resolve: function resolve() { calls.push("on"); } }; this.render({ name: "loop-image-footer.docx", options: { modules: [mod] }, data: { loop: [1, 2, 3, 4] } }); expect(uniq(calls)).to.deep.equal(["on", "set", "getFileType", "optionsTransformer", "preparse", "matchers", "getTraits", "postparse", "errorsTransformer", "getRenderedMap", "render", "postrender"]); }); }); describe("Module priority", function () { it("should reorder modules #test-reorder-modules", function () { var doc = createDocV4("loop-image-footer.docx", { modules: [{ priority: 4, name: "M1", parse: function parse(parsed) { return parsed; } }, { priority: -1, name: "M2", parse: function parse(parsed) { return parsed; } }, { priority: 5, name: "M3", parse: function parse(parsed) { return parsed; } }, { priority: 5, name: "M4", parse: function parse(parsed) { return parsed; } }, { priority: 5, name: "M5", parse: function parse(parsed) { return parsed; } }] }); var orderedNames = doc.modules.map(function (_ref4) { var name = _ref4.name; return name; }); expect(orderedNames).to.deep.equal(["M3", // Priority 5 "M4", // Priority 5 "M5", // Priority 5 "M1", // Priority 4 // All default modules have default priority of 0 "LoopModule", "SpacePreserveModule", "ExpandPairTrait", "RawXmlModule", "Render", "Common", "AssertionModule", "M2" // Priority -1 ]); }); });