docxtemplater/es6/docxtemplater.js
2021-09-04 10:15:02 +05:00

424 lines
11 KiB
JavaScript

"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;