"use strict"; const DocUtils = require("./doc-utils"); DocUtils.traits = require("./traits"); DocUtils.moduleWrapper = require("./module-wrapper"); const { throwMultiError, throwResolveBeforeCompile, throwRenderInvalidTemplate, } = require("./errors"); const collectContentTypes = require("./collect-content-types"); const ctXML = "[Content_Types].xml"; const commonModule = require("./modules/common"); const Lexer = require("./lexer"); const { defaults, str2xml, xml2str, moduleWrapper, utf8ToWord, concatArrays, unique, } = DocUtils; const { XTInternalError, throwFileTypeNotIdentified, throwFileTypeNotHandled, throwApiVersionError, } = require("./errors"); const currentModuleApiVersion = [3, 24, 0]; const Docxtemplater = class Docxtemplater { constructor(zip, { modules = [], ...options } = {}) { if (!Array.isArray(modules)) { throw new Error( "The modules argument of docxtemplater's constructor must be an array" ); } this.compiled = {}; this.modules = [commonModule()]; this.setOptions(options); modules.forEach((module) => { this.attachModule(module); }); if (zip) { if (!zip.files || typeof zip.file !== "function") { throw new Error( "The first argument of docxtemplater's constructor must be a valid zip file (jszip v2 or pizzip v3)" ); } this.loadZip(zip); // remove the unsupported modules this.modules = this.modules.filter((module) => { if (module.supportedFileTypes) { if (!Array.isArray(module.supportedFileTypes)) { throw new Error( "The supportedFileTypes field of the module must be an array" ); } const isSupportedModule = module.supportedFileTypes.indexOf(this.fileType) !== -1; if (!isSupportedModule) { module.on("detached"); } return isSupportedModule; } return true; }); this.compile(); this.v4Constructor = true; } } getModuleApiVersion() { return currentModuleApiVersion.join("."); } verifyApiVersion(neededVersion) { neededVersion = neededVersion.split(".").map(function (i) { return parseInt(i, 10); }); if (neededVersion.length !== 3) { throwApiVersionError("neededVersion is not a valid version", { neededVersion, explanation: "the neededVersion must be an array of length 3", }); } if (neededVersion[0] !== currentModuleApiVersion[0]) { throwApiVersionError( "The major api version do not match, you probably have to update docxtemplater with npm install --save docxtemplater", { neededVersion, currentModuleApiVersion, explanation: `moduleAPIVersionMismatch : needed=${neededVersion.join( "." )}, current=${currentModuleApiVersion.join(".")}`, } ); } if (neededVersion[1] > currentModuleApiVersion[1]) { throwApiVersionError( "The minor api version is not uptodate, you probably have to update docxtemplater with npm install --save docxtemplater", { neededVersion, currentModuleApiVersion, explanation: `moduleAPIVersionMismatch : needed=${neededVersion.join( "." )}, current=${currentModuleApiVersion.join(".")}`, } ); } if ( neededVersion[1] === currentModuleApiVersion[1] && neededVersion[2] > currentModuleApiVersion[2] ) { throwApiVersionError( "The patch api version is not uptodate, you probably have to update docxtemplater with npm install --save docxtemplater", { neededVersion, currentModuleApiVersion, explanation: `moduleAPIVersionMismatch : needed=${neededVersion.join( "." )}, current=${currentModuleApiVersion.join(".")}`, } ); } return true; } setModules(obj) { this.modules.forEach((module) => { module.set(obj); }); } sendEvent(eventName) { this.modules.forEach((module) => { module.on(eventName); }); } attachModule(module, options = {}) { if (this.v4Constructor) { throw new Error( "attachModule() should not be called manually when using the v4 constructor" ); } if (module.requiredAPIVersion) { this.verifyApiVersion(module.requiredAPIVersion); } if (module.attached === true) { throw new Error( `Cannot attach a module that was already attached : "${module.name}". Maybe you are instantiating the module at the root level, and using it for multiple instances of Docxtemplater` ); } module.attached = true; const { prefix } = options; if (prefix) { module.prefix = prefix; } const wrappedModule = moduleWrapper(module); this.modules.push(wrappedModule); wrappedModule.on("attached"); return this; } setOptions(options) { if (this.v4Constructor) { throw new Error( "setOptions() should not be called manually when using the v4 constructor" ); } if (!options) { throw new Error( "setOptions should be called with an object as first parameter" ); } if (options.delimiters) { options.delimiters.start = utf8ToWord(options.delimiters.start); options.delimiters.end = utf8ToWord(options.delimiters.end); } this.options = {}; Object.keys(defaults).forEach((key) => { const defaultValue = defaults[key]; this.options[key] = options[key] != null ? options[key] : defaultValue; this[key] = this.options[key]; }); if (this.zip) { this.updateFileTypeConfig(); } return this; } loadZip(zip) { if (zip.loadAsync) { throw new XTInternalError( "Docxtemplater doesn't handle JSZip version >=3, please use pizzip" ); } this.zip = zip; this.updateFileTypeConfig(); this.modules = concatArrays([ this.fileTypeConfig.baseModules.map(function (moduleFunction) { return moduleFunction(); }), this.modules, ]); return this; } compileFile(fileName) { this.compiled[fileName].parse(); } precompileFile(fileName) { const currentFile = this.createTemplateClass(fileName); currentFile.preparse(); this.compiled[fileName] = currentFile; } resolveData(data) { let errors = []; if (!Object.keys(this.compiled).length) { throwResolveBeforeCompile(); } return Promise.resolve(data) .then((data) => { return Promise.all( Object.keys(this.compiled).map((from) => { const currentFile = this.compiled[from]; return currentFile.resolveTags(data).catch(function (errs) { errors = errors.concat(errs); }); }) ); }) .then((resolved) => { if (errors.length !== 0) { throwMultiError(errors); } return concatArrays(resolved); }); } compile() { if (Object.keys(this.compiled).length) { return this; } this.options = this.modules.reduce((options, module) => { return module.optionsTransformer(options, this); }, this.options); this.options.xmlFileNames = unique(this.options.xmlFileNames); this.xmlDocuments = this.options.xmlFileNames.reduce( (xmlDocuments, fileName) => { const content = this.zip.files[fileName].asText(); xmlDocuments[fileName] = str2xml(content); return xmlDocuments; }, {} ); this.setModules({ zip: this.zip, xmlDocuments: this.xmlDocuments, }); this.getTemplatedFiles(); this.setModules({ compiled: this.compiled }); // Loop inside all templatedFiles (ie xml files with content). // Sometimes they don't exist (footer.xml for example) this.templatedFiles.forEach((fileName) => { if (this.zip.files[fileName] != null) { this.precompileFile(fileName); } }); this.templatedFiles.forEach((fileName) => { if (this.zip.files[fileName] != null) { this.compileFile(fileName); } }); verifyErrors(this); return this; } updateFileTypeConfig() { let fileType; if (this.zip.files.mimetype) { fileType = "odt"; } const contentTypes = this.zip.files[ctXML]; this.targets = []; const contentTypeXml = contentTypes ? str2xml(contentTypes.asText()) : null; const overrides = contentTypeXml ? contentTypeXml.getElementsByTagName("Override") : null; const defaults = contentTypeXml ? contentTypeXml.getElementsByTagName("Default") : null; if (contentTypeXml) { this.filesContentTypes = collectContentTypes( overrides, defaults, this.zip ); this.invertedContentTypes = DocUtils.invertMap(this.filesContentTypes); this.setModules({ contentTypes: this.contentTypes, invertedContentTypes: this.invertedContentTypes, }); } this.modules.forEach((module) => { fileType = module.getFileType({ zip: this.zip, contentTypes, contentTypeXml, overrides, defaults, doc: this, }) || fileType; }); if (fileType === "odt") { throwFileTypeNotHandled(fileType); } if (!fileType) { throwFileTypeNotIdentified(); } this.fileType = fileType; this.fileTypeConfig = this.options.fileTypeConfig || this.fileTypeConfig || Docxtemplater.FileTypeConfig[this.fileType]; return this; } render() { this.compile(); if (this.errors.length > 0) { throwRenderInvalidTemplate(); } this.setModules({ data: this.data, Lexer, }); this.mapper = this.modules.reduce(function (value, module) { return module.getRenderedMap(value); }, {}); this.fileTypeConfig.tagsXmlLexedArray = unique( this.fileTypeConfig.tagsXmlLexedArray ); this.fileTypeConfig.tagsXmlTextArray = unique( this.fileTypeConfig.tagsXmlTextArray ); Object.keys(this.mapper).forEach((to) => { const { from, data } = this.mapper[to]; const currentFile = this.compiled[from]; currentFile.setTags(data); currentFile.render(to); this.zip.file(to, currentFile.content, { createFolders: true }); }); verifyErrors(this); this.sendEvent("syncing-zip"); this.syncZip(); return this; } syncZip() { Object.keys(this.xmlDocuments).forEach((fileName) => { this.zip.remove(fileName); const content = xml2str(this.xmlDocuments[fileName]); return this.zip.file(fileName, content, { createFolders: true }); }); } setData(data) { this.data = data; return this; } getZip() { return this.zip; } createTemplateClass(path) { const content = this.zip.files[path].asText(); return this.createTemplateClassFromContent(content, path); } createTemplateClassFromContent(content, filePath) { const xmltOptions = { filePath, contentType: this.filesContentTypes[filePath], }; Object.keys(defaults) .concat(["filesContentTypes", "fileTypeConfig", "modules"]) .forEach((key) => { xmltOptions[key] = this[key]; }); return new Docxtemplater.XmlTemplater(content, xmltOptions); } getFullText(path) { return this.createTemplateClass( path || this.fileTypeConfig.textPath(this) ).getFullText(); } getTemplatedFiles() { this.templatedFiles = this.fileTypeConfig.getTemplatedFiles(this.zip); this.targets.forEach((target) => { this.templatedFiles.push(target); }); return this.templatedFiles; } }; function verifyErrors(doc) { const compiled = doc.compiled; let allErrors = []; Object.keys(compiled).forEach((name) => { const templatePart = compiled[name]; allErrors = concatArrays([allErrors, templatePart.allErrors]); }); doc.errors = allErrors; if (allErrors.length !== 0) { throwMultiError(allErrors); } } Docxtemplater.DocUtils = DocUtils; Docxtemplater.Errors = require("./errors"); Docxtemplater.XmlTemplater = require("./xml-templater"); Docxtemplater.FileTypeConfig = require("./file-type-config"); Docxtemplater.XmlMatcher = require("./xml-matcher"); module.exports = Docxtemplater;