UNPKG

@rodewitsch/carbone

Version:

Fast, Simple and Powerful report generator. Injects JSON and produces PDF, DOCX, XLSX, ODT, PPTX, ODS, ...!

1,020 lines (991 loc) 72.4 kB
var fs = require('fs'); var os = require('os'); var path = require('path'); var file = require('./file'); var params = require('./params'); var helper = require('./helper'); var format = require('./format'); var builder = require('./builder'); var input = require('./input'); var preprocessor = require('./preprocessor'); var translator = require('./translator'); var converter = require('./converter'); var debug = require('debug')('carbone'); var dayjs = require('dayjs'); var locales = require('../formatters/_locale'); var xmljs = require('xml-js'); var carbone = { /** * This function is NOT asynchronous (It may create the template or temp directory synchronously) * @param {Object} options { * tempPath : system temp directory by default * templatePath : it will create the directory if it does not exists * renderPath : where rendered files are temporary saved. It will create the directory if it does not exists * lang : set default lang of carbone, can be overwrite in carbone.render options.lang * timezone : set default timezone of carbone, can be overwrite in carbone.render options.timezone * translations : overwrite carbone translations object * currencySource : currency of data, it depends on the locale if empty * currencyTarget : default target currency when the formatter convCurr is used without target * it depends on the locale if empty * currencyRates : rates, based on EUR { EUR : 1, USD : 1.14 } * } */ set: function (options) { for (var attr in options) { if (params[attr] !== undefined) { params[attr] = options[attr]; } else { throw Error('Undefined options :' + attr); } } if (options.templatePath !== undefined) { if (fs.existsSync(params.templatePath) === false) { fs.mkdirSync(params.templatePath, '0755'); } if (fs.existsSync(path.join(params.templatePath, 'lang')) === false) { fs.mkdirSync(path.join(params.templatePath, 'lang'), '0755'); } if (options.translations === undefined) { translator.loadTranslations(params.templatePath); } } if (options.tempPath !== undefined && fs.existsSync(params.tempPath) === false) { fs.mkdirSync(params.tempPath, '0755'); } if (fs.existsSync(params.renderPath) === false) { fs.mkdirSync(params.renderPath, '0755'); } if (options.factories !== undefined || options.startFactory !== undefined) { converter.init(); } dayjs.tz.setDefault(params.timezone); dayjs.locale(params.lang.toLowerCase()); }, /** * Reset parameters (for test purpose) */ reset: function () { // manage node 0.8 / 0.10 differences var _nodeVersion = process.versions.node.split('.'); var _tmpDir = (parseInt(_nodeVersion[0], 10) === 0 && parseInt(_nodeVersion[1], 10) < 10) ? os.tmpDir() : os.tmpdir(); params.tempPath = _tmpDir; params.templatePath = process.cwd(); params.renderPath = path.join(params.tempPath, 'carbone_render'); params.factories = 1; params.attempts = 2; params.startFactory = false; params.factoryMemoryFileSize = 1; params.factoryMemoryThreshold = 50; params.converterFactoryTimeout = 60000; params.uidPrefix = 'c'; params.pipeNamePrefix = '_carbone'; params.lang = 'en'; params.timezone = 'Europe/Paris'; params.translations = {}; params.currencySource = ''; params.currencyTarget = ''; params.currencyRates = { EUR: 1, USD: 1.14 }; }, /** * Add a template in Carbone datastore (template path) * @param {String} fileId Unique file name. All templates will be saved in the same folder (templatePath). It will overwrite if the template already exists. * @param {String|Buffer} data The content of the template * @param {Function} callback(err) called when done */ addTemplate: function (fileId, data, callback) { /* if(path.isAbsolute(fileId)===true){ //possible with Node v0.11 return callback('The file id should not be an absolute path: '+fileId); }*/ var _fullPath = path.join(params.templatePath, fileId); fs.writeFile(_fullPath, data, function (err) { callback(err); }); }, /** * add formatters * @param {Object} formatters {toInt: function(d, args, agrs, ...)} */ addFormatters: function (customFormatters) { for (var f in customFormatters) { input.formatters[f] = customFormatters[f]; } }, /** * Remove a template from the Carbone datastore (template path) * @param {String} fileId Unique file name. * @param {Function} callback(err) */ removeTemplate: function (fileId, callback) { var _fullPath = path.join(params.templatePath, fileId); fs.unlink(_fullPath, callback); }, /** * Return the list of possible conversion format * @param {String} documentType Must be 'document', 'web', 'graphics', 'spreadsheet', 'presentation' * @return {Array} List of format */ listConversionFormats: function (documentType) { var _res = []; if (format[documentType] === undefined) { throw Error('Unknown document type'); } var _doc = format[documentType]; for (var attr in _doc) { var _format = _doc[attr]; _format.id = attr; _res.push(_format); } return _res; }, /** * Render XML directly * * @param {String} xml The XML * @param {Object|Array} data The data * @param {Object} optionsRaw The options raw * @param {Function} callbackRaw The callback raw */ renderXML: function (xml, data, optionsRaw, callbackRaw) { input.parseOptions(optionsRaw, callbackRaw, function (options, callback) { // Clean XML tags inside Carbone markers and translate xml = preprocessor.preParseXML(xml, options); return builder.buildXML(xml, data, options, callback); }); }, /** * Renders a template with given datas and return result to the callback function * * @param {String} templatePath : file name of the template (or absolute path) * @param {Object|Array} data : Datas to be inserted in the template represented by the {d.****} * @param {Object} optionsRaw [optional] : { * 'complement' : {} data which is represented by the {c.****} * 'convertTo' : 'pdf' || { 'formatName', 'formatOptions'} Convert the document in the format specified * 'extension' : 'odt' || undefined Specify the template extension * 'variableStr' : '' pre-declared variables, * 'lang' : overwrite default lang. Ex. "fr" * 'timezone' : set timezone for date formatters (Europe/Paris) by default * 'translations' : overwrite all loaded translations {fr: {}, en: {}, es: {}} * 'enum' : { ORDER_STATUS : ['open', 'close', 'sent'] * 'currencySource' : currency of data, 'EUR' * 'currencyTarget' : default target currency when the formatter convCurr is used without target * 'currencyRates' : rates, based on EUR { EUR : 1, USD : 1.14 } * 'hardRefresh' : (default: false) if true, LibreOffice is used to render and refresh the content of the report at the end of Carbone process * 'renderPrefix' : If defined, it returns a path instead of a buffer, and it adds this prefix in the filename * The filename will contains also the report name URL Encoded * } * @param {Function} callbackRaw(err, bufferOrPath, reportName) : Function called after generation with the result */ renderCallback: function (templatePath, data, optionsRaw, callbackRaw) { if (!optionsRaw) optionsRaw = {}; input.parseOptions(optionsRaw, callbackRaw, function (options, callback) { // open the template (unzip if necessary) file.openTemplate(templatePath, function (err, template) { if (err) { return callback(err, null); } // Determine the template extension. var _extension = file.detectType(template); // It takes the user defined one, or use the file type. options.extension = optionsRaw.extension || _extension; if (options.extension === null) { return callback('Unknown input file type. It should be a docx, xlsx, pptx, odt, ods, odp, xhtml, html or an xml file'); } // check and clean convertTo object, options.convertTo contains a clean version of optionsRaw.convertTo var _error = input.parseConvertTo(options, optionsRaw.convertTo); if (_error) { return callback(_error); } template.reportName = options.reportName; template.extension = options.extension; preprocessor.execute(template, options, function (err, template) { if (err) { return callback(err, null); } // obtaining images from data for generation let images = getImagesFromData(data); try { if (template.extension == 'xlsx') { // delete sheets containing pivot tables var pivotFiles = removePivotFiles(template.files); // shift drawing elements drawingsShifting(template.files, data); // shift and expansion of the data area of functions template.files = scaleFunctionData(template.files, data); // expanding chart data areas scaleChartsData(template.files, data); // expanding table data areas scaleTablesData(template.files, data); // merge line shift shiftMergeRows(template.files, data); // line shift rowsShift(template.files, data); // print area shift printAreaShift(template.files, data); for (let fileIndex = 0; fileIndex < template.files.length; fileIndex++) { template.files[fileIndex].data = preprocessor.removeRowCounterInWorksheet(template.files[fileIndex].data); } } } catch (err) { return callback(err); } // parse all files of the template walkFiles(template, data, options, 0, function (err, report) { if (err) { return callback(err, null); } if (template.extension == 'xlsx') putImagesToSheets(report, images); if (template.extension == 'docx') putImagesToDocument(report, images); if (template.extension == 'docx') insertHTML(report, data); if (pivotFiles) report.files.push(...pivotFiles); // assemble all files and zip if necessary file.buildFile(report, function (err, result) { if (err) { return callback(err, null); } convert(result, report.reportName, options, function (err, bufferOrFile) { if (report.reportName === undefined && typeof bufferOrFile === 'string') { report.reportName = path.basename(bufferOrFile); } callback(err, bufferOrFile, report.reportName, (options.isDebugActive === true ? options.debugInfo : null)); }); }); }); }); }); }); }, /** * Renders a template with given datas and return Promise * @param {String} templatePath : file name of the template (or absolute path) * @param {Object|Array} data : Datas to be inserted in the template represented by the {d.****} * @param {Object} optionsRaw [optional] : { * 'complement' : {} data which is represented by the {c.****} * 'convertTo' : 'pdf' || { 'formatName', 'formatOptions'} Convert the document in the format specified * 'extension' : 'odt' || undefined Specify the template extension * 'variableStr' : '' pre-declared variables, * 'lang' : overwrite default lang. Ex. "fr" * 'translations' : overwrite all loaded translations {fr: {}, en: {}, es: {}} * 'enum' : { ORDER_STATUS : ['open', 'close', 'sent'] * 'currencySource' : currency of data, 'EUR' * 'currencyTarget' : default target currency when the formatter convCurr is used without target * 'currencyRates' : rates, based on EUR { EUR : 1, USD : 1.14 } * } */ renderPromise: function (templatePath, data, optionsRaw) { return new Promise((resolve, reject) => { const callback = (err, result, reportName) => { if (err) return reject(err); return resolve({ result, reportName }); }; if (optionsRaw) return this.renderCallback(templatePath, data, optionsRaw, callback); return this.render(templatePath, data, callback); }) }, render: function (templatePath, data, optionsRaw, callbackRaw) { if (typeof optionsRaw === 'function' || typeof callbackRaw === 'function') { return this.renderCallback(templatePath, data, optionsRaw, callbackRaw); } return this.renderPromise(templatePath, data, optionsRaw); }, /** * Decodes a rendered filename. * * When carbone.render is called with the options renderPrefix, the callback returns a path instead of a buffer * The filename is built like this (3 distinct parts), with only alphanumeric characters to be able to write it on the disk safely * * <prefix><22-random-chars><encodedReportName.extension> * * This function decodes the part `<encodedReportName.extension>` ` * * @param {String} pathOrFilename The path or filename * @param {Integer} prefixLength The prefix length used in options.renderPrefix * @return {Object} { * extension : 'pdf', * reportName : 'decoded filename' * } */ decodeRenderedFilename: function (pathOrFilename, prefixLength = 0) { var _filename = path.basename(pathOrFilename); var _extension = path.extname(_filename); var _onlyReportName = _filename.slice(prefixLength + helper.RANDOM_STRING_LENGTH, -_extension.length); return { reportName: helper.decodeSafeFilename(_onlyReportName), extension: _extension.slice(1) }; }, /** * Return the file extension * @param {String} filePath File path * @param {Function} callback */ getFileExtension: function (filePath, callback) { file.openTemplate(filePath, function (err, template) { if (err) { return callback(err); } var ext = file.detectType(template); if (ext === null) { return callback('Cannot detect file extension'); } return callback(null, ext); }); }, /** * Convert a file format to another * * @param {Buffer} data raw data returned by fs.readFile (no utf-8) * @param {Object} options Same as carbone.render * { * convertTo : pdf || {formatName, formatOptions} * extension : 'csv' // extension of input file returned by path.extname(filename). * // It helps LibreOffice to understand the source format (mandatory for CSV files) * } * @param {Function} callback (err, result) result is a buffer (file converted) */ convert: function (fileBuffer, optionsRaw, callbackRaw) { input.parseOptions(optionsRaw, callbackRaw, function (options, callback) { options.extension = optionsRaw.extension; if (options.extension === null) { return callback('Unknown input file type. options.extension should be equals to docx, xlsx, pptx, odt, ods, odp, xhtml, html or an xml'); } var _error = input.parseConvertTo(options, optionsRaw.convertTo); if (_error) { return callback(_error); } convert(fileBuffer, undefined, options, callback); }); }, formatters: input.formatters }; /** ***************************************************************************************************************/ /* Privates methods */ /** ***************************************************************************************************************/ /** * Merge line shift * @param {Array<Object>} files - an array of objects containing meta file information * @param {Object} data - data to substitute */ function shiftMergeRows(files, data) { let sheetsFilesIndexes = findSheetsFilesIndexes(files); sheetsFilesIndexes.forEach(sheetFileIndex => { let rows = (files[sheetFileIndex.sheetFileIndex].data.match(/<row.*?>.*?<\/row>/g) || []); rows = rows.map(row => ({ value: row, number: +row.match(/r="(.*?)"/)[1] })); let interpolations = rows.map(interpolation => ({ value: (interpolation.value.match(/\{d\.(.*?)\[i\].*\}/) || [])[1], number: interpolation.number })); if (sheetFileIndex.sheetRelsFileIndex == -1) return; let mergedCells = files[sheetFileIndex.sheetFileIndex].data.match(/<mergeCell .*?\/>/g); if (!mergedCells) return; if (!mergedCells.length) return; mergedCells.reverse().forEach(mergeCell => { let pos = mergeCell.match(/ref="(.*?)"/)[1]; let splittedPos = pos.split(':'); let newSplittedPos = splittedPos; let from = getNumberFromXlsxPosition(splittedPos[0]); let to = getNumberFromXlsxPosition(splittedPos[1]); let previousInterpolations = interpolations.filter(interpolation => interpolation.value && interpolation.number + 1 < from.row); let offset = 0; previousInterpolations.forEach(prevInterpolation => offset += data[prevInterpolation.value].length - 2); if (offset) { newSplittedPos[0] = newSplittedPos[0].replace(from.row, from.row + offset); newSplittedPos[1] = newSplittedPos[1].replace(to.row, to.row + offset); } let newPos = newSplittedPos.join(':'); files[sheetFileIndex.sheetFileIndex].data = files[sheetFileIndex.sheetFileIndex].data.replace(mergeCell, mergeCell.replace(pos, newPos)); }); }); } /** * Deleting and returning sheets containing pivot tables, and files having pivot tables mentioned * @param {Array<Object>} files - an array of objects containing meta file information * @return {Array<Object>} - deleted files */ function removePivotFiles(files) { let sheets = findSheetsFilesIndexes(files); let pivotFiles = []; let pivotSheetsIndexes = []; sheets.forEach(sheet => { if (sheet.sheetRelsFileIndex == -1) return; if (sheet.sheetRelsFileIndex && files[sheet.sheetRelsFileIndex].data.indexOf('pivot') != -1) { pivotSheetsIndexes.push(sheet.sheetFileIndex); } }); pivotSheetsIndexes.reverse().forEach(index => pivotFiles.push(...files.splice(index, 1))); let fileIndex = 0; while (fileIndex != files.length) { if (files[fileIndex].name.indexOf('pivot') != -1) { pivotFiles.push(...files.splice(fileIndex, 1)); fileIndex = 0; } fileIndex++; } return pivotFiles; } /** * Shift and expansion of the table data area * @param {Array<Object>} files - an array of objects containing meta file information * @param {Object} data - data to substitute */ function scaleTablesData(files, data) { let sheetsFilesIndexes = findSheetsFilesIndexes(files); sheetsFilesIndexes.forEach(sheetFileIndex => { let rows = (files[sheetFileIndex.sheetFileIndex].data.match(/<row.*?>.*?<\/row>/g) || []); rows = rows.map(row => ({ value: row, number: +row.match(/r="(.*?)"/)[1] })); let interpolations = rows.map(interpolation => ({ value: (interpolation.value.match(/\{d\.(.*?)\[i\].*\}/) || [])[1], number: interpolation.number })); if (sheetFileIndex.sheetRelsFileIndex == -1) return; let tables = files[sheetFileIndex.sheetRelsFileIndex].data.split('<').filter(item => item.indexOf('\/tables\/table') != -1).map(item => item.match(/tables\/(.*?)"/)[1]); if (!tables.length) return; tables.forEach(table => { let tableFileIndex = files.findIndex(file => file.name.indexOf(table) != -1); let tableFile = files[tableFileIndex].data; let tableDataAreas = tableFile.match(/ref="(.*?)"/g) || []; tableDataAreas.forEach(tableDataArea => { let pos = tableDataArea.match(/ref="(.*?)"/)[1]; let splittedPos = pos.split(':'); let newSplittedPos = splittedPos; let from = getNumberFromXlsxPosition(splittedPos[0]); let to = getNumberFromXlsxPosition(splittedPos[1]); let previousInterpolations = interpolations.filter(interpolation => interpolation.value && interpolation.number + 1 < from.row); let offset = 0; previousInterpolations.forEach(prevInterpolation => offset += data[prevInterpolation.value].length - 2); if (offset) { newSplittedPos[0] = newSplittedPos[0].replace(from.row, from.row + offset); newSplittedPos[1] = newSplittedPos[1].replace(to.row, to.row + offset); } let scaleNumber = 0; interpolations.filter(item => getNumsArrayToNumber(to.row, from.row).includes(item.number) && item.value).forEach(item => scaleNumber += (data[item.value] ? data[item.value].length - 2 : 0)); if (scaleNumber && from.row != to.row) { newSplittedPos[1] = newSplittedPos[1].replace(to.row + offset, to.row + offset + scaleNumber); } let newPos = newSplittedPos.join(':'); files[tableFileIndex].data = files[tableFileIndex].data.replace(pos, newPos); }) }); }); return files; } /** * Shift and expansion of the field of data formulas * @param {Array<Object>} files - an array of objects containing meta file information * @param {Object} data - data to substitute */ function scaleFunctionData(files, data) { // поиск индексов листов и их связей let sheetsFilesIndexes = findSheetsFilesIndexes(files); let calcChainFileIndex = files.findIndex(file => file.name.indexOf('calcChain.xml') != -1); sheetsFilesIndexes.forEach(sheetFileIndex => { let rows = (files[sheetFileIndex.sheetFileIndex].data.match(/<row.*?>.*?<\/row>/g) || []); rows = rows.map(row => ({ value: row, number: +row.match(/r="(.*?)"/)[1] })); let interpolations = rows.map(interpolation => ({ value: (interpolation.value.match(/\{d\.(.*?)\[i\].*\}/) || [])[1], number: interpolation.number })); let formulas = rows.reduce((acc, row, index) => { let matched = row.value.match(/<f.*?>.*?<\/f>/g); if (matched && matched.length) { matched.forEach(formula => { acc.push({ value: formula, cell: row.value.split('<c').find(cell => cell.indexOf(formula) != -1).match(/r="(.*?)"/)[1] }); }); } return acc; }, []); if (!formulas.length) return; formulas.reverse().forEach(formula => { let formulaDataAreas = formula.value.substring(formula.value.indexOf('>'), formula.value.indexOf('</')).match(/\w+\d+:\w+\d+/g); if (!formulaDataAreas) { let formulaCells = formula.value.substring(formula.value.indexOf('>'), formula.value.indexOf('</')).match(/\w+\d+/g); let currentPos = getNumberFromXlsxPosition(formulaCells[0]); let offset = 0; let previousInterpolations = interpolations.filter(interpolation => interpolation.value && interpolation.number + 1 < currentPos.row); previousInterpolations.forEach(prevInterpolation => offset += data[prevInterpolation.value].length - 2); let newRow = currentPos.row + offset; files[sheetFileIndex.sheetFileIndex].data = files[sheetFileIndex.sheetFileIndex].data.replace(formula.value, formula.value.replace(new RegExp(`${currentPos.row}`, 'g'), newRow)); let formulaCalcRelation = files[calcChainFileIndex].data.match(new RegExp(`<c r="${formula.cell}".*?i="${sheetFileIndex.sheetId}".*?\/>`))[0]; let formulaRelPosition = getNumberFromXlsxPosition(formula.cell); let previousCalcChainInterpolations = interpolations.filter(interpolation => interpolation.value && interpolation.number + 1 < formulaRelPosition.row); let calcChainInterpolationsOffset = 0; previousCalcChainInterpolations.forEach(prevInterpolation => calcChainInterpolationsOffset += data[prevInterpolation.value].length - 2); let newFormulaCalcRelation = formulaCalcRelation.replace(formula.cell, formula.cell.replace(formulaRelPosition.row, formulaRelPosition.row + calcChainInterpolationsOffset)); files[calcChainFileIndex].data = files[calcChainFileIndex].data.replace(formulaCalcRelation, newFormulaCalcRelation); } else { formulaDataAreas.forEach(pos => { let splittedPos = pos.split(':'); let newSplittedPos = splittedPos; let from = getNumberFromXlsxPosition(splittedPos[0]); let to = getNumberFromXlsxPosition(splittedPos[1]); let previousInterpolations = interpolations.filter(interpolation => interpolation.value && interpolation.number + 1 < from.row); let offset = 0; previousInterpolations.forEach(prevInterpolation => offset += data[prevInterpolation.value].length - 2); if (offset) { newSplittedPos[0] = newSplittedPos[0].replace(from.row, from.row + offset); newSplittedPos[1] = newSplittedPos[1].replace(to.row, to.row + offset); } let scaleNumber = data[interpolations.find(item => item.number == from.row).value].length - 2; if (scaleNumber && from.row != to.row) { newSplittedPos[1] = newSplittedPos[1].replace(to.row + offset, to.row + offset + scaleNumber); } let newPos = newSplittedPos.join(':'); if (files[calcChainFileIndex] && files[calcChainFileIndex].data) { let formulaCalcRelation = (files[calcChainFileIndex].data.match(new RegExp(`<c r="${formula.cell}".*?i="${sheetFileIndex.sheetId}".*?\/>`)) || [])[0]; if (formulaCalcRelation) { let formulaRelPosition = getNumberFromXlsxPosition(formula.cell); let previousCalcChainInterpolations = interpolations.filter(interpolation => interpolation.value && interpolation.number + 1 < formulaRelPosition.row); let calcChainInterpolationsOffset = 0; previousCalcChainInterpolations.forEach(prevInterpolation => calcChainInterpolationsOffset += data[prevInterpolation.value].length - 2); let newFormulaCalcRelation = formulaCalcRelation.replace(formula.cell, formula.cell.replace(formulaRelPosition.row, formulaRelPosition.row + calcChainInterpolationsOffset)); files[calcChainFileIndex].data = files[calcChainFileIndex].data.replace(formulaCalcRelation, newFormulaCalcRelation); } } files[sheetFileIndex.sheetFileIndex].data = files[sheetFileIndex.sheetFileIndex].data.replace(pos, newPos); }); } }) files[sheetFileIndex.sheetFileIndex].data = files[sheetFileIndex.sheetFileIndex].data.replace(/(<f.*?>.*?<\/f>)<v>.*?<\/v>/g, '$1'); }); return files; } /** * Shift and expansion of chart data area * @param {Array<Object>} files - an array of objects containing meta file information * @param {Object} data - data to substitute */ function scaleChartsData(files, data) { // поиск индексов листов и их связей let sheetsFilesIndexes = findSheetsFilesIndexes(files); sheetsFilesIndexes.forEach(sheetFileIndex => { let rows = (files[sheetFileIndex.sheetFileIndex].data.match(/<row.*?>.*?<\/row>/g) || []); rows = rows.map(row => ({ value: row, number: +row.match(/r="(.*?)"/)[1] })); if (sheetFileIndex.sheetRelsFileIndex == -1) return; let drawingFileName = (files[sheetFileIndex.sheetRelsFileIndex].data.match(/drawing\d*.xml/) || [])[0]; if (!drawingFileName) return; let drawingRelFileIndex = files.findIndex(file => file.name.indexOf(`${drawingFileName}.rel`) != -1); if (drawingRelFileIndex == -1) return; let chartsNames = files[drawingRelFileIndex].data.match(/charts\/chart\w*?\.xml/g); if (!chartsNames) return; let chartsFilesIndexes = chartsNames.reduce((indexes, chartName) => { let index = files.findIndex(file => file.name.indexOf(chartName) != -1); if (index != -1) return [...indexes, index]; return indexes; }, []); if (!chartsFilesIndexes.length) return; chartsFilesIndexes.forEach(chartFileIndex => { let chartFile = files[chartFileIndex].data; let chartDataAreas = chartFile.match(/<c:f>.*?<\/c:f>/g); chartDataAreas.forEach(dataArea => { let rawDataArea = dataArea.substring(5, dataArea.length - 6); let [from, to] = rawDataArea.split('!')[1].split(':'); let fromRowPosition = getNumberFromXlsxPosition(from.replace(/\$/g, '')).row; let newRawDataArea; let previousRowsFrom = rows.filter(row => row.value && row.number + 1 < fromRowPosition); let previousInterpolations = previousRowsFrom.reduce((acc, row) => { let interpolation = row.value.match(/{d\.(.*?)\[i\].*?}/); if (interpolation && interpolation[1]) acc.push(interpolation[1]); return acc; }, []); let offset = 0; previousInterpolations.forEach(interpolation => offset += data[interpolation].length - 2); let newFrom = from.replace(fromRowPosition, fromRowPosition + offset); let dataKey = (files[sheetFileIndex.sheetFileIndex].data.match(new RegExp(`r="${from.replace(/\$/g, '')}".*?>.*?{d\\.(.*?)\\[i\\].*?}.*?<\\/c>`)) || [])[1]; if (to) { let toRowPosition = getNumberFromXlsxPosition(to.replace(/\$/g, '')).row; const HAS_SUMMARY_ROW = !!files[sheetFileIndex.sheetFileIndex].data.match(new RegExp(`d.${dataKey}\\[i\\+1\\]\\..+:summary`)); if (data[dataKey]) { let newTo = to.replace(toRowPosition, toRowPosition + offset + (dataKey ? data[dataKey].length - 2 : 0) + (HAS_SUMMARY_ROW ? -1 : 0)); newRawDataArea = rawDataArea.replace(to, newTo); } else { let newTo = to.replace(toRowPosition, toRowPosition + offset); newRawDataArea = rawDataArea.replace(to, newTo); } } newRawDataArea = (newRawDataArea || rawDataArea).replace(from, newFrom); files[chartFileIndex].data = files[chartFileIndex].data.replace( rawDataArea, newRawDataArea ); }) }); }); return files; } /** * Getting number positions from A1 format * @param {string} position - position in format (A1, DE5 e.t.c); * @returns {Object} - number position */ function getNumberFromXlsxPosition(position) { const alpabet = { 'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6, 'G': 6, 'H': 8, 'I': 9, 'J': 10, 'K': 11, 'L': 12, 'M': 13, 'N': 14, 'O': 15, 'P': 16, 'Q': 17, 'R': 18, 'S': 19, 'T': 20, 'U': 21, 'V': 22, 'W': 23, 'X': 24, 'Y': 25, 'Z': 26 }; const [, letters, number] = position.match(/(\D+)(\d+)/); let splittedLetters = letters.split(''); let row = +number; let col = 0; if (splittedLetters.length > 1) { while (splittedLetters.length != 1) { col += 26 * alpabet[splittedLetters.shift()]; } } col += alpabet[splittedLetters[0]]; return { row, col }; } /** * Arrayed numbers from to * @param {number} numFrom - start number * @param {number} [numTo = 0] - end number * @returns {Array} */ function getNumsArrayToNumber(numFrom, numTo = 0) { let array = []; while (numFrom >= numTo) { array.push(numFrom--) }; return array; } /** * Line shift * @param {Array<Object>} files - an array of objects containing meta file information * @param {Object} data - data to substitute */ function rowsShift(files, data) { let sheetsFilesIndexes = findSheetsFilesIndexes(files); sheetsFilesIndexes.forEach(sheetFileIndex => { let rows = (files[sheetFileIndex.sheetFileIndex].data.match(/<row.*?>.*?<\/row>/g) || []); let interpolations = rows.map(interpolation => (interpolation.match(/\{d\.(.*?)\[i\].*\}/) || [])[1]); rows.reverse().forEach((row, index) => { let rowIndex = rows.length - index - 1; let previousInterpolations = interpolations.filter((interpolation, interpolationIndex) => interpolation && interpolationIndex < (rowIndex - (row.indexOf('i+1') == -1 ? 0 : 2))); if (!previousInterpolations.length) return; let position = +row.match(/r="(\d*)"/)[1]; let offset = 0; previousInterpolations.forEach(interpolation => offset += (data[interpolation] ? data[interpolation].length - 2 : 0)); let replacement = row.replace(/r="(.*?)\d*?"/g, `r="$1${position + offset}"`); files[sheetFileIndex.sheetFileIndex].data = files[sheetFileIndex.sheetFileIndex].data.replace(row, replacement); }) }); return files; } /** * Insert html to files * @param {Array<Object>} files - an array of objects containing meta file information * @param {Object} data - data to substitute */ function insertHTML(report) { let documentIndex = -1; const document = report.files.find((file, index) => { if (file.name === 'word/document.xml') { documentIndex = index return true; } return false; }); if (!document) return; const paragraphs = document.data.match(/<w:p.*?>.*?<\/w:p>/g); for (const paragraph of paragraphs) { if (!paragraph.includes(':html')) continue; const encodedHTMLContent = paragraph.match(/<w:p.*?>.*<w:t>(.*):html<\/w:t>.*<\/w:p>/)[1]; const HTMLContent = Buffer.from(encodedHTMLContent, 'base64').toString('utf8'); document.data = document.data.replace(paragraph, HTMLContent); } } /** * Shift printing area * @param {Array<Object>} files - an array of objects containing meta file information * @param {Object} data - data to substitute */ function printAreaShift(files, data) { let workBookIndex = -1; const [workBook] = files.filter((file, index) => { if (file.name === 'xl/workbook.xml') { workBookIndex = index; return true; } return false; }); if (!workBook) return; let workBookPrintArea = workBook.data.match(/<definedName name=\"_xlnm.Print_Area\".*?>(.*?)<\/definedName>/g); if (!workBookPrintArea) return; workBookPrintArea = workBookPrintArea.map(printAreaData => { const sheetName = printAreaData.match(/<.*?>(.*?)!.*?<\/definedName>/)[1]; const sheetInitialOffset = +printAreaData.match(new RegExp(`${sheetName}!\\$[A-Z]{1,2}\\$[0-9]+:\\$[A-Z]{1,2}\\$([0-9]+)`))[1]; const sheetInitialTagData = printAreaData.match(new RegExp(`(${sheetName}!\\$[A-Z]{1,2}\\$[0-9]+:\\$[A-Z]{1,2}\\$[0-9]+)`))[1]; const sheetId = workBook.data.match(new RegExp(`<sheet name=\"${sheetName.replace(/\'/g, '')}\" sheetId=\"(.*?)\".*?/>`))[1]; return { sheetId, sheetInitialOffset, sheetInitialTagData, printAreaData } }) let sheetsFilesIndexes = findSheetsFilesIndexes(files); sheetsFilesIndexes.forEach(sheetFileIndex => { const [printArea] = workBookPrintArea.filter(workBookPrintAreaItem => workBookPrintAreaItem.sheetId === sheetFileIndex.sheetId); if (!printArea) return; const rows = (files[sheetFileIndex.sheetFileIndex].data.match(/<row.*?>.*?<\/row>/g) || []); const interpolations = rows .map(interpolation => (interpolation.match(/\{d\.(.*?)\[i\].*\}/) || [])[1]) .filter((interpolation, index) => interpolation && index <= printArea.sheetInitialOffset); let offset = 0; interpolations.forEach(interpolation => offset += (data[interpolation] ? data[interpolation].length - 2 : 0)); const newPrintShift = printArea.sheetInitialTagData.slice(0, printArea.sheetInitialTagData.length - `${printArea.sheetInitialOffset}`.length) + (offset + printArea.sheetInitialOffset); const newTag = printArea.printAreaData.replace(printArea.sheetInitialTagData, newPrintShift) files[workBookIndex].data = files[workBookIndex].data.replace(printArea.printAreaData, newTag); }); return files; } /** * Shift driving elements * @param {Array<Object>} files - an array of objects containing meta file information * @param {Object} data - data to substitute */ function drawingsShifting(files, data) { // поиск индексов листов и их связей let sheetsFilesIndexes = findSheetsFilesIndexes(files); sheetsFilesIndexes.forEach(sheetFileIndex => { if (sheetFileIndex.sheetRelsFileIndex == -1) return; let drawingFileName = (files[sheetFileIndex.sheetRelsFileIndex].data.match(/drawings\/drawing.?\.xml/) || [])[0]; let drawingFileIndex = files.findIndex(file => file.name.indexOf(drawingFileName) != -1); if (drawingFileIndex == -1) return; let matchedPositions = files[drawingFileIndex].data.match(/<xdr:row>\d+<\/xdr:row>/g); matchedPositions.reverse().forEach(matchedPosition => { let position = +matchedPosition.substring(9, matchedPosition.lastIndexOf('</xdr:row>')); let rows = (files[sheetFileIndex.sheetFileIndex].data.match(/<row.*?>.*?<\/row>/g) || []); let interpolations = rows.map(interpolation => ({ interpolation: (interpolation.match(/\{d\.(.*?)\[i\].*\}/) || [])[1], position: (interpolation.match(/r="(\d*)"/) || [])[1] })); let previousInterpolations = interpolations.filter(elem => elem.interpolation && elem.position <= position); if (!previousInterpolations.length) return; let offset = 0; previousInterpolations.forEach(elem => { offset += data[elem.interpolation].length - 2; }); files[drawingFileIndex].data = files[drawingFileIndex].data.replace( matchedPosition, `<xdr:row>${offset + position}</xdr:row>` ); }) }); return files; } /** * Search for sheets and indexes of their bindings * @param {Array<Object>} files - an array of objects containing meta file information * @returns {Array<Object>} */ function findSheetsFilesIndexes(files) { let bookRels = files.find(file => file.name.indexOf('xl/_rels/workbook.xml.rels') != -1).data; let workBook = files.find(file => file.name == 'xl/workbook.xml').data; let workBookSheets = workBook.match(/<sheet .*?\/>/g); return files.reduce((acc, file, index) => { if (file.name.indexOf('xl/worksheets/sheet') != -1) { let sheetNumber = file.name.substring(19, file.name.lastIndexOf('.')), sheetRelsRid = bookRels.split('<').filter(item => item.indexOf(`sheet${sheetNumber}`) != -1)[0].match(/Id="(.*?)"/)[1], workBookSheet = workBookSheets.find(item => item.indexOf(sheetRelsRid) != -1), sheetId = workBookSheet.match(/sheetId="(\w+)"/)[1], sheetName = workBookSheet.match(/name="(.+?)"/)[1], sheetRelsFileIndex = findSheetRelFileIndex(files, sheetNumber); acc.push({ sheetNumber, sheetFileIndex: index, sheetId, sheetName, sheetRelsRid, sheetRelsFileIndex }); } return acc; }, []); } /** * @param {Array<Object>} files - an array of objects containing meta file information * @param {number} sheetNumber * @returns {Number} */ function findSheetRelFileIndex(files, sheetNumber) { let fileIndex = files.findIndex(file => file.name == `xl/worksheets/_rels/sheet${sheetNumber}.xml.rels`); if (fileIndex === -1) { fileIndex = files.findIndex(file => file.name == `xl/_rels/workbook.xml.rels`); } return fileIndex; } /** * Add file buffer to image list * @param {Object} data - data to substitute * @returns {Array<{fileName: string, data: string}>} */ function getImagesFromData(data) { let acc = []; for (let key in data) { if (!data[key]) continue; if (Array.isArray(data[key])) { for (let i = 0; i < data[key].length; i++) { for (let nestedKey in data[key][i]) { if (data[key][i][nestedKey] && data[key][i][nestedKey].indexOf && data[key][i][nestedKey].indexOf('base64') != -1 || data[key][i][nestedKey] && (Buffer.isBuffer(data[key][i][nestedKey]) || data[key][i][nestedKey].type == 'Buffer' || data[key][i][nestedKey].BYTES_PER_ELEMENT != undefined)) { if (data[key][i][nestedKey].type == 'Buffer' || data[key][i][nestedKey].BYTES_PER_ELEMENT != undefined) data[key][i][nestedKey] = Buffer.from(data[key][i][nestedKey]); acc.push({ fileName: `${key}[${i}].${nestedKey}`, data: Buffer.from(data[key][i][nestedKey].toString('utf8').replace(/data:image\/(jpeg|png);base64,/, ''), 'base64') }); data[key][i][nestedKey] = `${key}[${i}].${nestedKey}`; } } } } else { if (data[key].indexOf && data[key].indexOf('base64') != -1) { acc.push({ fileName: `${key}`, data: Buffer.from(data[key].replace(/data:image\/(jpeg|png);base64,/, ''), 'base64') }); data[key] = `${key}`; } } } return acc; } /** * Insert images into document * @param {Object} report - report data * @param {Array} images - images */ function putImagesToDocument(report, images = []) { if (!images.length) return; images = images.map(image => { let encodedImageName = Buffer.from(image.fileName).toString('base64'); report.files.push({ data: image.data, isMarked: false, name: `word/media/${encodedImageName}.png`, parent: "" }); return { ...image, encodedImageName }; }) report.files[0].data = report.files[0].data.replace( '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">', '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="png" ContentType="image/png"/><Default Extension="jpeg" ContentType="image/jpeg"/>' ); let documentRelsIndex = report.files.findIndex(file => file.name.indexOf('document.xml.rels') != -1); let documentIndex = report.files.findIndex(file => file.name.indexOf('word/document.xml') != -1); let maxDocumentRelsId = Math.max(0, ...(report.files[documentRelsIndex].data.match(/Id=".*?"/g) || []).map(elem => +elem.substring(elem.indexOf('rId') + 3, elem.length - 1))); images.forEach((image, index) => { let imageRid = maxDocumentRelsId + index + 1; report.files[documentRelsIndex].data = report.files[documentRelsIndex].data.replace( '</Relationships>', `<Relationship Id="rId${imageRid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${image.encodedImageName}.png"/></Relationships>` ); let imageSizeFormatter = (report.files[documentIndex].data.match(new RegExp(`<w:t>${Buffer.from(image.encodedImageName, 'base64').toString('utf8').replace(/(\[|\]|\.)/g, '\\$1')}:(\\d+\\*\\d+|\\d+)<\\/w:t>`)) || [])[1]; let imageWidth, imageHeight; if (imageSizeFormatter) { ([imageWidth, imageHeight = imageWidth] = imageSizeFormatter.split('*')); imageWidth *= 12700; imageHeight *= 12700; } else { imageWidth = 2095500; imageHeight = 2095500; } report.files[documentIndex].data = report.files[documentIndex].data.replace( new RegExp(`<w:t>${Buffer.from(image.encodedImageName, 'base64').toString('utf8').replace(/(\[|\]|\.)/g, '\\$1')}(:\\d+\\*\\d+|:\\d+|)<\\/w:t>`), `<w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${imageWidth}" cy="${imageHeight}"/><wp:effectExtent l="0" t="0" r="0" b="0"/><wp:docPr id="1" name="Рисунок 1"/><wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="1" name="Аннотация 2019-04-04 111910.jpg"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="rId${imageRid}"><a:extLst><a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}"><a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0"/></a:ext></a:extLst></a:blip><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${imageWidth}" cy="${imageHeight}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing>` ); }) } /** * Insert images into sheets * @param {Object} report - report data * @param {Array} images - images */ function putImagesToSheets(report, images = []) { if (!images.length) return; images = images.map(image => { let encodedImageName = Buffer.from(image.fileName).toString('base64'); report.files.push({ data: image.data, isMarked: false, name: `xl/media/${encodedImageName}.png`, parent: "" }); return { ...image, encodedImageName }; }) report.files[0].data = report.files[0].data.replace( '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">', '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="png" ContentType="image/png"/><Default Extension="jpeg" ContentType="image/jpeg"/>' ); let sheets = []; report.files.forEach((file, index) => { if (file.name.indexOf('xl/worksheets/sheet') != -1) sheets.push({ file, sheetNumber: file.name.substring(file.name.indexOf('/sheet') + 6, file.name.lastIndexOf('.')), index }); }) sheets.forEach((sheet) => { let sheetHasImages = images.reduce((acc, image) => { if (sheet.file.data.indexOf(image.fileName) != -1) acc++; return acc }, 0); if (!sheetHasImages) return; let sheetDrawingRelsIndex = 0; let drawingId = 0; let sheetRelsIndex = report.files.findIndex(file => file.name.indexOf(`sheet${sheet.sheetNumber}.xml.rels`) != -1); // нет sheetRels у листа if (sheetRelsIndex == -1) { // ищем все drawings let allDrawings = report.files.filter(file => file.name.indexOf('drawings/drawing') != - 1); // берем максимальное название if (allDrawings.length) { drawingId = Math.max(0, ...allDrawings.map(file => +file.name.substring(file.name.indexOf('xl/drawings/drawing') + 19, file.name.lastIndexOf('.')))); drawingId++; } else { drawingId = 1; } // создаем новый файл с макс названием +1 report.files.push(createBlankDrawingRelsFile(drawingId)); sheetDrawingRelsIndex = report.files.length - 1; // добавляем sheetRels с указателем на drawings report.files.push(createBlankSheetRelsFile(sheet.sheetNumber)); sheetRelsIndex = report.files.length - 1; let maxSheetRelsId = Math.max(0, ...(report.files[sheetRelsIndex].data.match(/Id=".*?"/g) || []).map(elem => +elem.substring(elem.indexOf('rId') + 3, elem.length - 1))); report.files[sheetRelsIndex].data = report.files[sheetRelsIndex].data.replace( '</Relationships>', `<Relationship Id="rId${maxSheetRelsId + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" Target="../drawings/drawing${drawingId}.xml"/></Relationships>` ); // создание drawingRels let sheetDrawingIndex = 0; report.files.push(createBlankDrawingFile(drawingId)); sheetDrawingIndex = report.files.length - 1; // добавление ссылки на drawing в content-type report.files[0].data = report.files[0].data.replace( '</Types>', `<Override PartName="/xl/drawings/drawing${drawingId}.xml" ContentType="application/vnd.openxmlformats-officedocument.drawing+xml"/></Types>` ); // добавление ссылки на drawing в sheet report.files[sheet.index].data = report.files[sheet.index].data.replace( '</worksheet>', `<drawing r:id="rId${maxSheetRelsId + 1}"/></worksheet>` ); let maxSheetDrawingRelsId = Math.max(0, ...(report.files[sheetDrawingRelsIndex].data.match(/Id=".*?"/) || []).map(elem => +elem.substring(elem.indexOf('rId') + 3, elem.length - 1))); const sheetJson = JSON.parse(xmljs.xml2json(sheet.file.data, { compact: true })).worksheet; images.forEach((image, index) => { let imageParams = getImagePosition(sheetJson, image.fileName); let imageRid = maxSheetDrawingRelsId + index + 1; report.files[sheetDrawingRelsIndex].data = report.files[sheetDrawingRelsIndex].data.replace( '</Relationships>', `<Relationship Id="rId${imageRid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/${image.encodedImageName}.png"/></Relationships>` ); report.files[sheetDrawingIndex].data = report.files[sheetDrawingIndex].data.replace( '</xdr:wsDr>', `<xdr:oneCellAnchor><xdr:from><xdr:col>${imageParams.coords.col}</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>${imageParams.coords.row}</xdr:row><xdr:rowOff>1</xdr:rowOff></xdr:from><xdr:ext cx="${imageParams.size.col}" cy="${imageParams.size.row}"/><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="${imageRid}" name="Изображение