@jsreport/jsreport-xlsx
Advanced tools
Comparing version 4.1.1 to 4.2.0
@@ -0,2 +1,4 @@ | ||
const { decode } = require('html-entities') | ||
const { col2num } = require('xlsx-coordinates') | ||
const pixelWidth = require('string-pixel-width') | ||
@@ -70,2 +72,63 @@ const CELL_REG_REGEXP = /^(?:('?(?:\[([A-Za-z0-9_. ]+\.xlsx)\])?([A-Za-z0-9_. ]+)'?)!)?(\$?[A-Z]+)(\$?\d+)$/ | ||
function getPixelWidthOfValue (value, fontSize) { | ||
const fontSizeInPx = fontSize * (96 / 72) | ||
const size = pixelWidth(value, { font: 'Arial', size: fontSizeInPx }) | ||
return parseFloat(size.toFixed(2)) | ||
} | ||
function getFontSizeFromStyle (_styleId, styleInfo, cache) { | ||
let styleId | ||
if (_styleId != null && _styleId !== '') { | ||
styleId = parseInt(_styleId, 10) | ||
} else { | ||
styleId = 0 | ||
} | ||
const cacheKey = styleId.toString() | ||
if (cache != null && cache.has(cacheKey)) { | ||
return cache.get(cacheKey) | ||
} | ||
const { cellXfsEls, cellStyleXfsEls, fontEls } = styleInfo | ||
const selectedXfEl = cellXfsEls[styleId] | ||
let fontId = selectedXfEl.getAttribute('fontId') | ||
const applyFont = selectedXfEl.getAttribute('applyFont') | ||
const xfId = selectedXfEl.getAttribute('xfId') | ||
if ( | ||
applyFont == null || | ||
applyFont === '' || | ||
applyFont === '0' | ||
) { | ||
const selectedStyleXfEl = cellStyleXfsEls[xfId] | ||
const nestedFontId = selectedStyleXfEl.getAttribute('fontId') | ||
const nestedApplyFont = selectedStyleXfEl.getAttribute('applyFont') | ||
if ( | ||
nestedApplyFont == null || | ||
nestedApplyFont === '' || | ||
nestedApplyFont === '1' | ||
) { | ||
fontId = nestedFontId | ||
} | ||
} | ||
const fontEl = fontEls[fontId] | ||
const sizeEl = fontEl.getElementsByTagName('sz')[0] | ||
// size stored in xlsx is in pt | ||
const fontSize = parseFloat(sizeEl.getAttribute('val')) | ||
if (cache != null) { | ||
cache.set(cacheKey, fontSize) | ||
} | ||
return fontSize | ||
} | ||
function evaluateCellRefsFromExpression (valueExpr, replacer) { | ||
@@ -124,3 +187,3 @@ const cellRefs = [] | ||
function getNewFormula (helperType, originalFormula, parsedOriginCellRef, meta) { | ||
function getNewFormula (originalFormula, parsedOriginCellRef, meta) { | ||
let newFormula | ||
@@ -251,2 +314,5 @@ const result = {} | ||
formula: newValue, | ||
// just placeholder for the key, this is going to be fill at runtime | ||
// with the updated formula | ||
newFormula: null, | ||
parsedOriginCellRef, | ||
@@ -297,5 +363,44 @@ originCellIsFromLoop, | ||
const xmlEscapeMap = { | ||
'>': '>', | ||
'<': '<', | ||
"'": ''', | ||
'"': '"', | ||
'&': '&' | ||
} | ||
// we dont'se the encode function of html-entities because want to have the chance to | ||
// escape just some characters | ||
function encodeXML (str, mode = 'all') { | ||
let pattern | ||
switch (mode) { | ||
case 'all': | ||
pattern = /([&"<>'])/g | ||
break | ||
case 'basic': | ||
pattern = /([&<>])/g | ||
break | ||
default: | ||
throw new Error('Invalid mode for encodeXML') | ||
} | ||
const output = str.replace(pattern, (_, item) => { | ||
return xmlEscapeMap[item] | ||
}) | ||
return output | ||
} | ||
function decodeXML (str) { | ||
return decode(str, { level: 'xml' }) | ||
} | ||
module.exports.parseCellRef = parseCellRef | ||
module.exports.getPixelWidthOfValue = getPixelWidthOfValue | ||
module.exports.getFontSizeFromStyle = getFontSizeFromStyle | ||
module.exports.evaluateCellRefsFromExpression = evaluateCellRefsFromExpression | ||
module.exports.getNewFormula = getNewFormula | ||
module.exports.generateNewCellRefFromRow = generateNewCellRefFromRow | ||
module.exports.encodeXML = encodeXML | ||
module.exports.decodeXML = decodeXML |
@@ -1,9 +0,9 @@ | ||
const { v1: uuidv1 } = require('uuid') | ||
const path = require('path') | ||
const fs = require('fs') | ||
const mkdirp = require('mkdirp') | ||
const { nanoid } = require('nanoid') | ||
const zlib = require('zlib') | ||
module.exports.write = (tmp, data) => { | ||
const file = path.join(tmp, uuidv1() + '.xml') | ||
const file = path.join(tmp, `${nanoid()}.xml'`) | ||
@@ -10,0 +10,0 @@ mkdirp.sync(tmp) |
const { DOMParser } = require('@xmldom/xmldom') | ||
const { col2num } = require('xlsx-coordinates') | ||
const recursiveStringReplaceAsync = require('../../recursiveStringReplaceAsync') | ||
const stringReplaceAsync = require('../../stringReplaceAsync') | ||
const { nodeListToArray, isWorksheetFile, serializeXml, getSheetInfo } = require('../../utils') | ||
const { nodeListToArray, isWorksheetFile, getSheetInfo } = require('../../utils') | ||
@@ -11,3 +9,2 @@ module.exports = async (files) => { | ||
const workbookRelsDoc = files.find((file) => file.path === 'xl/_rels/workbook.xml.rels').doc | ||
const calcChainDoc = files.find((f) => f.path === 'xl/calcChain.xml')?.doc | ||
const workbookSheetsEls = nodeListToArray(workbookDoc.getElementsByTagName('sheet')) | ||
@@ -23,86 +20,2 @@ const workbookRelsEls = nodeListToArray(workbookRelsDoc.getElementsByTagName('Relationship')) | ||
const updatedCalcChainCountMap = new Map() | ||
// update the calcChain.xml to the new updated cells | ||
sheetFile.data = await recursiveStringReplaceAsync( | ||
sheetFile.data.toString(), | ||
'<calcChainCellUpdated>', | ||
'</calcChainCellUpdated>', | ||
'g', | ||
async (val, content, hasNestedMatch) => { | ||
if (hasNestedMatch) { | ||
return val | ||
} | ||
const doc = new DOMParser().parseFromString(val) | ||
const calcChainCellRefUpdatedEl = doc.documentElement.firstChild | ||
const calcChainEls = nodeListToArray(calcChainDoc.getElementsByTagName('c')) | ||
const matches = calcChainEls.filter((c) => ( | ||
c.getAttribute('i') === sheetInfo.id && | ||
c.getAttribute('oldR') === calcChainCellRefUpdatedEl.getAttribute('oldR') | ||
)) | ||
const calcChainCellRefEl = matches[matches.length - 1] | ||
if (calcChainCellRefEl != null) { | ||
const oldRef = calcChainCellRefEl.getAttribute('oldR') | ||
const newRef = calcChainCellRefUpdatedEl.getAttribute('r') | ||
const existingCount = updatedCalcChainCountMap.get(oldRef) || 0 | ||
if (oldRef !== newRef) { | ||
if (existingCount === 0) { | ||
calcChainCellRefEl.setAttribute('r', newRef) | ||
} else { | ||
const newCalcChainCellRefEl = calcChainCellRefEl.cloneNode(true) | ||
newCalcChainCellRefEl.setAttribute('r', newRef) | ||
calcChainCellRefEl.parentNode.insertBefore(newCalcChainCellRefEl, calcChainCellRefEl.nextSibling) | ||
} | ||
} | ||
updatedCalcChainCountMap.set(oldRef, existingCount + 1) | ||
} | ||
return '' | ||
} | ||
) | ||
if (calcChainDoc != null) { | ||
const newCalcChainEls = nodeListToArray(calcChainDoc.getElementsByTagName('c')).filter((c) => c.getAttribute('i') === sheetInfo.id) | ||
// we clean the oldR attribute that we used for the conditions | ||
for (const calcChainEl of newCalcChainEls) { | ||
calcChainEl.removeAttribute('oldR') | ||
} | ||
} | ||
let dimensionUpdatedRef | ||
// search if we should update the dimension | ||
sheetFile.data = await stringReplaceAsync( | ||
sheetFile.data.toString(), | ||
/<dimensionUpdated [^>]*\/>/g, | ||
async (val) => { | ||
const dimensionUpdatedEl = new DOMParser().parseFromString(val).documentElement | ||
dimensionUpdatedRef = dimensionUpdatedEl.getAttribute('ref') | ||
return '' | ||
} | ||
) | ||
// update the dimension with latest cellRef values | ||
if (dimensionUpdatedRef != null) { | ||
sheetFile.data = await stringReplaceAsync( | ||
sheetFile.data.toString(), | ||
/<dimension [^>]*\/>/g, | ||
async (val) => { | ||
const dimensionEl = new DOMParser().parseFromString(val).documentElement | ||
dimensionEl.setAttribute('ref', dimensionUpdatedRef) | ||
return serializeXml(dimensionEl) | ||
} | ||
) | ||
} | ||
// check if we need to updates tables | ||
@@ -143,120 +56,3 @@ sheetFile.data = await recursiveStringReplaceAsync( | ||
) | ||
const autofitCols = {} | ||
// check if we need to update the cols with autofit information | ||
sheetFile.data = await recursiveStringReplaceAsync( | ||
sheetFile.data.toString(), | ||
'<autofitUpdated>', | ||
'</autofitUpdated>', | ||
'g', | ||
async (val, content, hasNestedMatch) => { | ||
if (hasNestedMatch) { | ||
return val | ||
} | ||
const doc = new DOMParser().parseFromString(val) | ||
const autofitUpdatedEl = doc.documentElement | ||
const colEls = nodeListToArray(autofitUpdatedEl.getElementsByTagName('col')) | ||
for (const colEl of colEls) { | ||
const letter = colEl.getAttribute('ref') | ||
const size = parseFloat(colEl.getAttribute('size')) | ||
autofitCols[letter] = size | ||
} | ||
return '' | ||
} | ||
) | ||
if (Object.keys(autofitCols).length > 0) { | ||
sheetFile.data = await recursiveStringReplaceAsync( | ||
sheetFile.data.toString(), | ||
'<cols>', | ||
'</cols>', | ||
'g', | ||
async (val, content, hasNestedMatch) => { | ||
if (hasNestedMatch) { | ||
return val | ||
} | ||
const doc = new DOMParser().parseFromString(val) | ||
const colsEl = doc.documentElement | ||
const existingColEls = nodeListToArray(colsEl.getElementsByTagName('col')) | ||
// cleaning | ||
for (let idx = 0; idx < colsEl.childNodes.length; idx++) { | ||
const el = colsEl.childNodes[idx] | ||
colsEl.removeChild(el) | ||
} | ||
for (const [colLetter, colPxSize] of Object.entries(autofitCols)) { | ||
const colSizeInNumberCharactersMDW = (colPxSize / 6.5) + 2 // 2 is for padding | ||
const colNumber = col2num(colLetter) + 1 | ||
const existingColEl = existingColEls.find((el) => ( | ||
el.getAttribute('min') === colNumber.toString() && | ||
el.getAttribute('max') === colNumber.toString() | ||
)) | ||
if (existingColEl != null) { | ||
existingColEl.setAttribute('width', colSizeInNumberCharactersMDW) | ||
existingColEl.setAttribute('customWidth', '1') | ||
} else { | ||
const newCol = doc.createElement('col') | ||
newCol.setAttribute('min', colNumber.toString()) | ||
newCol.setAttribute('max', colNumber.toString()) | ||
newCol.setAttribute('width', colSizeInNumberCharactersMDW) | ||
newCol.setAttribute('customWidth', '1') | ||
colsEl.appendChild(newCol) | ||
} | ||
} | ||
return serializeXml(colsEl) | ||
} | ||
) | ||
} | ||
const allLazyFormulaEls = {} | ||
// lazy formulas | ||
sheetFile.data = await recursiveStringReplaceAsync( | ||
sheetFile.data.toString(), | ||
'<lazyFormulas>', | ||
'</lazyFormulas>', | ||
'g', | ||
async (val, content, hasNestedMatch) => { | ||
if (hasNestedMatch) { | ||
return val | ||
} | ||
const doc = new DOMParser().parseFromString(val) | ||
const lazyFormulasEl = doc.documentElement | ||
const itemEls = nodeListToArray(lazyFormulasEl.getElementsByTagName('item')) | ||
for (const itemEl of itemEls) { | ||
allLazyFormulaEls[itemEl.getAttribute('id')] = itemEl.textContent | ||
} | ||
return '' | ||
} | ||
) | ||
if (Object.keys(allLazyFormulaEls).length > 0) { | ||
sheetFile.data = await stringReplaceAsync( | ||
sheetFile.data.toString(), | ||
/\$lazyFormulaRef[\d]+/g, | ||
async (lazyFormulaId) => { | ||
const newFormula = allLazyFormulaEls[lazyFormulaId] | ||
if (newFormula == null) { | ||
throw new Error(`Could not find lazyFormula internal data with id "${lazyFormulaId}"`) | ||
} | ||
return newFormula | ||
} | ||
) | ||
} | ||
} | ||
} |
const path = require('path') | ||
const { num2col } = require('xlsx-coordinates') | ||
const { nodeListToArray, isWorksheetFile, isWorksheetRelsFile, getSheetInfo } = require('../../utils') | ||
const { parseCellRef, evaluateCellRefsFromExpression } = require('../../cellUtils') | ||
const { nodeListToArray, isWorksheetFile, isWorksheetRelsFile, getSheetInfo, getStyleFile, getStyleInfo } = require('../../utils') | ||
const { parseCellRef, getPixelWidthOfValue, getFontSizeFromStyle, evaluateCellRefsFromExpression } = require('../../cellUtils') | ||
const startLoopRegexp = /{{#each ([^{}]{0,500})}}/ | ||
module.exports = (files) => { | ||
const workbookPath = 'xl/workbook.xml' | ||
const workbookDoc = files.find((file) => file.path === workbookPath)?.doc | ||
module.exports = (files, meta) => { | ||
// console.time('pre:loop process setup') | ||
const workbookDoc = files.find((file) => file.path === 'xl/workbook.xml')?.doc | ||
const workbookRelsDoc = files.find((file) => file.path === 'xl/_rels/workbook.xml.rels')?.doc | ||
const sharedStringsDoc = files.find((f) => f.path === 'xl/sharedStrings.xml')?.doc | ||
const calcChainDoc = files.find((f) => f.path === 'xl/calcChain.xml')?.doc | ||
const styleInfo = getStyleInfo(getStyleFile(files)?.doc) | ||
const workbookCalcPrEl = workbookDoc.getElementsByTagName('calcPr')[0] | ||
let stylesInfo = {} | ||
let workbookSheetsEls = [] | ||
let workbookRelsEls = [] | ||
let sharedStringsEls = [] | ||
let calcChainEls = [] | ||
const calcChainMap = new Map() | ||
@@ -28,15 +28,2 @@ if (workbookDoc) { | ||
workbookRelsEls = nodeListToArray(workbookRelsDoc.getElementsByTagName('Relationship')) | ||
const styleRel = workbookRelsEls.find((el) => el.getAttribute('Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles') | ||
if (styleRel != null) { | ||
const stylePath = path.posix.join(path.posix.dirname(workbookPath), styleRel.getAttribute('Target')) | ||
const stylesDoc = files.find((file) => file.path === stylePath)?.doc | ||
if (stylesDoc != null) { | ||
stylesInfo = { | ||
doc: stylesDoc | ||
} | ||
} | ||
} | ||
} | ||
@@ -49,3 +36,3 @@ | ||
if (calcChainDoc != null) { | ||
calcChainEls = nodeListToArray(calcChainDoc.getElementsByTagName('c')) | ||
const calcChainCellEls = nodeListToArray(calcChainDoc.getElementsByTagName('c')) | ||
@@ -55,4 +42,4 @@ // we store the existing cell ref into other attribute | ||
// is going to be updated | ||
for (const calcChainEl of calcChainEls) { | ||
calcChainEl.setAttribute('oldR', calcChainEl.getAttribute('r')) | ||
for (const calcChainEl of calcChainCellEls) { | ||
calcChainMap.set(`${calcChainEl.getAttribute('i')}-${calcChainEl.getAttribute('r')}`, calcChainEl) | ||
} | ||
@@ -72,3 +59,3 @@ } | ||
const sheetDataEl = sheetDoc.getElementsByTagName('sheetData')[0] | ||
const colsEl = sheetDoc.getElementsByTagName('cols')[0] | ||
let colsEl = sheetDoc.getElementsByTagName('cols')[0] | ||
@@ -94,20 +81,35 @@ if (sheetDataEl == null) { | ||
// wrap the <sheetData> into wrapper so we can store data during helper calls | ||
const rootBlockStartEl = processOpeningTag(sheetDoc, sheetDataEl, `{{#xlsxSData type='root'${isAutofitConfigured ? ` autofit="${autoFitColLettersStr}"` : ''}}}`) | ||
const sheetDataCallProps = {} | ||
let rootEdgeEl = sheetDataEl | ||
if (isAutofitConfigured) { | ||
sheetDataCallProps.autofit = autoFitColLettersStr | ||
while (rootEdgeEl.nextSibling != null) { | ||
const nextSibling = rootEdgeEl.nextSibling | ||
if (colsEl == null) { | ||
colsEl = sheetDoc.createElement('cols') | ||
sheetDataEl.parentNode.insertBefore(colsEl, sheetDataEl) | ||
} | ||
if (meta.autofitConfigured !== true) { | ||
meta.autofitConfigured = true | ||
} | ||
} | ||
const sheetDataBlockStartEl = processOpeningTag(sheetDoc, sheetDataEl, getDataHelperCall('sd', sheetDataCallProps)) | ||
let sheetDataEdgeEl = sheetDataEl | ||
while (sheetDataEdgeEl.nextSibling != null) { | ||
const nextSibling = sheetDataEdgeEl.nextSibling | ||
if (nextSibling.nodeName === 'mergeCells') { | ||
rootEdgeEl = nextSibling | ||
sheetDataEdgeEl = nextSibling | ||
break | ||
} else { | ||
rootEdgeEl = nextSibling | ||
sheetDataEdgeEl = nextSibling | ||
} | ||
} | ||
rootEdgeEl = rootEdgeEl.nodeName === 'mergeCells' ? rootEdgeEl : sheetDataEl | ||
sheetDataEdgeEl = sheetDataEdgeEl.nodeName === 'mergeCells' ? sheetDataEdgeEl : sheetDataEl | ||
processClosingTag(sheetDoc, rootEdgeEl, '{{/xlsxSData}}') | ||
processClosingTag(sheetDoc, sheetDataEdgeEl, '{{/_D}}') | ||
@@ -122,5 +124,3 @@ const mergeCellsEl = sheetDoc.getElementsByTagName('mergeCells')[0] | ||
// the ref by the handlebars | ||
const newDimensionEl = sheetDoc.createElement('dimensionUpdated') | ||
newDimensionEl.setAttribute('ref', `{{xlsxSData type="dimensionRef" originalCellRefRange="${dimensionEl.getAttribute('ref')}" }}`) | ||
sheetDataEl.appendChild(newDimensionEl) | ||
dimensionEl.setAttribute('ref', `{{_D t='dimension' o='${dimensionEl.getAttribute('ref')}'}}`) | ||
} | ||
@@ -148,3 +148,3 @@ | ||
newTableUpdatedEl.setAttribute('ref', `{{xlsxSData type='newCellRef' originalCellRefRange='${tableDoc.documentElement.getAttribute('ref')}'}}`) | ||
newTableUpdatedEl.setAttribute('ref', `{{_D t='newCellRef' originalCellRefRange='${tableDoc.documentElement.getAttribute('ref')}'}}`) | ||
@@ -155,3 +155,3 @@ const autoFilterEl = tableDoc.getElementsByTagName('autoFilter')[0] | ||
const newAutoFilterRef = sheetDoc.createElement('autoFilterRef') | ||
newAutoFilterRef.setAttribute('ref', `{{xlsxSData type='newCellRef' originalCellRefRange='${autoFilterEl.getAttribute('ref')}'}}`) | ||
newAutoFilterRef.setAttribute('ref', `{{_D t='newCellRef' originalCellRefRange='${autoFilterEl.getAttribute('ref')}'}}`) | ||
newTableUpdatedEl.appendChild(newAutoFilterRef) | ||
@@ -192,12 +192,2 @@ } | ||
} | ||
const newAutofitEl = sheetDoc.createElement('autofitUpdated') | ||
newAutofitEl.textContent = '{{xlsxSData type="autofit"}}' | ||
sheetDataEl.appendChild(newAutofitEl) | ||
if (colsEl == null) { | ||
const newColsEl = sheetDoc.createElement('cols') | ||
newColsEl.textContent = 'placeholder for autofit' | ||
sheetDataEl.parentNode.insertBefore(newColsEl, sheetDataEl) | ||
} | ||
} | ||
@@ -210,2 +200,3 @@ | ||
const cellsElsByRefMap = new Map() | ||
const colMaxSizeMap = new Map() | ||
@@ -217,5 +208,4 @@ const lastRowIdx = rowsEls.length - 1 | ||
const isLastRow = rowIdx === lastRowIdx | ||
const standardCellElsToHandle = [] | ||
const staticCellElsToHandle = [] | ||
const contentDetectCellElsToHandle = [] | ||
const calcCellElsToHandle = [] | ||
@@ -229,28 +219,47 @@ if (originalRowNumber == null || originalRowNumber === '') { | ||
// wrap the <row> into wrapper so we can store data during helper calls | ||
processOpeningTag(sheetDoc, rowEl, `{{#xlsxSData type='row' originalRowNumber=${originalRowNumber}}}`) | ||
processClosingTag(sheetDoc, rowEl, '{{/xlsxSData}}') | ||
processOpeningTag(sheetDoc, rowEl, `{{#_R ${originalRowNumber}}}`) | ||
processClosingTag(sheetDoc, rowEl, '{{/_R}}') | ||
// update the row number to be based on helper call | ||
rowEl.setAttribute('r', "{{xlsxSData type='rowNumber'}}") | ||
rowEl.setAttribute('r', '{{@r}}') | ||
const cellsEls = nodeListToArray(rowEl.getElementsByTagName('c')) | ||
if (cellsEls.length === 0) { | ||
// there can be cases when the row has no cells but it has merge cell defined, if yes | ||
// then queue it to process it later | ||
const foundMergeCellEls = mergeCellEls.filter((mergeCellEl) => { | ||
const ref = mergeCellEl.getAttribute('ref') | ||
const mergeStartCellRef = ref.split(':')[0] | ||
return parseCellRef(mergeStartCellRef).rowNumber === originalRowNumber | ||
}) | ||
for (const mergeCellEl of foundMergeCellEls) { | ||
mergeCellElsToHandle.push({ ref: mergeCellEl.getAttribute('ref'), rowEl }) | ||
} | ||
} | ||
for (const cellEl of cellsEls) { | ||
const cellRef = cellEl.getAttribute('r') | ||
const parsedCellRef = parseCellRef(cellRef) | ||
cellsElsByRefMap.set(cellRef, cellEl) | ||
const cellMeta = { | ||
letter: parsedCellRef.letter | ||
} | ||
cellEl.setAttribute('r', `{{xlsxSData type='cellRef' originalCellRef='${cellRef}'}}`) | ||
cellsElsByRefMap.set(cellRef, [cellEl, cellMeta]) | ||
let cellCallType = '_C' | ||
// search if we need to update some calc cell | ||
const calcCellEl = findCellElInCalcChain(sheetInfo.id, cellRef, calcChainEls) | ||
const calcCellEl = calcChainMap.get(`${sheetInfo.id}-${cellRef}`) | ||
if (calcCellEl != null) { | ||
calcCellElsToHandle.push({ | ||
calcCellEl, | ||
cellRef, | ||
cellEl | ||
}) | ||
cellCallType = '_c' | ||
cellMeta.calcChainUpdate = true | ||
} | ||
// using alias for helper to optimize the size of generated xml | ||
cellEl.setAttribute('r', `{{${cellCallType} '${cellMeta.letter}'}}`) | ||
// check if the cell starts a merge cell, if yes | ||
@@ -277,5 +286,5 @@ // then queue it to process it later | ||
if (info.value.includes('{{') && info.value.includes('}}')) { | ||
contentDetectCellElsToHandle.push(cellEl) | ||
contentDetectCellElsToHandle.push(cellRef) | ||
} else if (isAutofitConfigured) { | ||
standardCellElsToHandle.push(cellEl) | ||
staticCellElsToHandle.push(cellRef) | ||
} | ||
@@ -344,3 +353,2 @@ | ||
const formulaInfo = { | ||
cellRef, | ||
formula: info.value, | ||
@@ -411,7 +419,7 @@ formulaEl: info.valueEl | ||
const parsedLoopEnd = parseCellRef(currentRowLoopDetected.end.cellRef) | ||
return `{{#xlsxSData ${valueInsideEachCall} type='loop' hierarchyId='${currentRowLoopDetected.hierarchyId}' start=${originalRowNumber} columnStart=${parsedLoopStart.columnNumber} columnEnd=${parsedLoopEnd.columnNumber} }}` | ||
return `{{#_D ${valueInsideEachCall} t='loop' hierarchyId='${currentRowLoopDetected.hierarchyId}' start=${originalRowNumber} columnStart=${parsedLoopStart.columnNumber} columnEnd=${parsedLoopEnd.columnNumber} }}` | ||
})) | ||
// we want to put the loop wrapper around the row wrapper | ||
currentRowLoopDetected.blockEndEl = processClosingTag(sheetDoc, rowEl.nextSibling, '{{/xlsxSData}}') | ||
currentRowLoopDetected.blockEndEl = processClosingTag(sheetDoc, rowEl.nextSibling, '{{/_D}}') | ||
} | ||
@@ -464,20 +472,24 @@ | ||
const parsedLoopEnd = parseCellRef(currentBlockLoopDetected.end.cellRef) | ||
return `{{#xlsxSData ${valueInsideEachCall} type='loop' hierarchyId='${currentBlockLoopDetected.hierarchyId}' start=${currentBlockLoopDetected.start.originalRowNumber} columnStart=${parsedLoopStart.columnNumber} end=${currentBlockLoopDetected.end.originalRowNumber} columnEnd=${parsedLoopEnd.columnNumber} }}` | ||
return `{{#_D ${valueInsideEachCall} t='loop' hierarchyId='${currentBlockLoopDetected.hierarchyId}' start=${currentBlockLoopDetected.start.originalRowNumber} columnStart=${parsedLoopStart.columnNumber} end=${currentBlockLoopDetected.end.originalRowNumber} columnEnd=${parsedLoopEnd.columnNumber} }}` | ||
})) | ||
// we want to put the loop wrapper around the end row wrapper | ||
currentBlockLoopDetected.blockEndEl = processClosingTag(sheetDoc, endingRowEl.nextSibling, '{{/xlsxSData}}') | ||
currentBlockLoopDetected.blockEndEl = processClosingTag(sheetDoc, endingRowEl.nextSibling, '{{/_D}}') | ||
} | ||
for (const cellEl of standardCellElsToHandle) { | ||
for (const cellRef of staticCellElsToHandle) { | ||
const [cellEl, cellMeta] = cellsElsByRefMap.get(cellRef) | ||
const cellInfo = getCellInfo(cellEl, sharedStringsEls, sheetFilepath) | ||
const fontSize = findCellFontSize(cellEl, stylesInfo) | ||
const fontSize = getFontSizeFromStyle(cellEl.getAttribute('s'), styleInfo) | ||
// wrap the cell <v> into wrapper so we can check the value size for | ||
// auto-size logic | ||
processOpeningTag(sheetDoc, cellEl.firstChild, `{{#xlsxSData type='cellValue' value="${cellInfo.value}" fontSize=${fontSize}}}`) | ||
processClosingTag(sheetDoc, cellEl.firstChild.nextSibling, '{{/xlsxSData}}') | ||
const currentMaxSize = colMaxSizeMap.get(cellMeta.letter) | ||
const currentSize = getPixelWidthOfValue(cellInfo.value, fontSize) | ||
if (currentMaxSize == null || currentSize > currentMaxSize) { | ||
colMaxSizeMap.set(cellMeta.letter, currentSize) | ||
} | ||
} | ||
for (const cellEl of contentDetectCellElsToHandle) { | ||
for (const cellRef of contentDetectCellElsToHandle) { | ||
const [cellEl, cellMeta] = cellsElsByRefMap.get(cellRef) | ||
const cellInfo = getCellInfo(cellEl, sharedStringsEls, sheetFilepath) | ||
@@ -498,11 +510,11 @@ | ||
const newContentEl = sheetDoc.createElement('xlsxRemove') | ||
const cellValueWrapperEl = sheetDoc.createElement('xlsxRemove') | ||
const cellValueWrapperEndEl = sheetDoc.createElement('xlsxRemove') | ||
const rawEl = sheetDoc.createElement('xlsxRemove') | ||
const handlebarsRegexp = /{{{?(#[\w-]+ )?([\w-]+[^\n\r}]*)}?}}/g | ||
const matches = Array.from(newTextValue.matchAll(handlebarsRegexp)) | ||
const isSingleMatch = matches.length === 1 && matches[0][0] === newTextValue && matches[0][1] == null | ||
const fontSize = findCellFontSize(cellEl, stylesInfo) | ||
cellEl.setAttribute('r', cellMeta.letter) | ||
cellEl.setAttribute('__CT_t__', '_T') | ||
cellEl.setAttribute('__CT_m__', isSingleMatch ? '0' : '1') | ||
cellEl.setAttribute('__CT_cCU__', cellMeta.calcChainUpdate ? '1' : '0') | ||
if (isSingleMatch) { | ||
@@ -512,44 +524,12 @@ const match = matches[0] | ||
const expressionValue = match[2] | ||
const value = expressionValue.includes(' ') ? `(${expressionValue})` : expressionValue | ||
cellValueWrapperEl.textContent = `{{#xlsxSData type='cellValue' value=${expressionValue.includes(' ') ? `(${expressionValue})` : expressionValue}${shouldEscape ? ' escape=true' : ''}` | ||
} else { | ||
cellValueWrapperEl.textContent = "{{#xlsxSData type='cellValue'" | ||
cellEl.setAttribute('__CT_v__', value) | ||
cellEl.setAttribute('__CT_ve__', shouldEscape ? '1' : '0') | ||
} | ||
cellValueWrapperEl.textContent += ` fontSize=${fontSize}}}` | ||
if (!isSingleMatch) { | ||
rawEl.textContent = `{{#xlsxSData type='cellValueRaw' }}${newTextValue}{{/xlsxSData}}` | ||
} | ||
cellEl.setAttribute('t', "{{xlsxSData type='cellValueType' }}") | ||
newContentEl.textContent = "{{xlsxSData type='cellContent' }}" | ||
cellValueWrapperEndEl.textContent = '{{/xlsxSData}}' | ||
cellEl.replaceChild(newContentEl, cellInfo.contentEl) | ||
cellEl.parentNode.insertBefore(cellValueWrapperEl, cellEl) | ||
if (!isSingleMatch) { | ||
cellEl.parentNode.insertBefore(rawEl, cellValueWrapperEl.nextSibling) | ||
} | ||
cellEl.parentNode.insertBefore(cellValueWrapperEndEl, cellEl.nextSibling) | ||
// when multi-expression put the content with handlebars as the only content of the cell, | ||
// for the rest of cases put a space to avoid the cell to be serialized as self closing tag | ||
cellEl.textContent = isSingleMatch ? ' ' : newTextValue | ||
} | ||
for (const { calcCellEl, cellRef, cellEl } of calcCellElsToHandle) { | ||
// we add the referenced cell in the calcChain in the cell | ||
// to be able to update the ref by the handlebars | ||
const newCalcCellEl = calcCellEl.cloneNode(true) | ||
newCalcCellEl.setAttribute('r', `{{xlsxSData type='cellRef' originalCellRef='${cellRef}' shadow=true}}`) | ||
newCalcCellEl.setAttribute('oldR', cellRef) | ||
const wrapperElement = sheetDoc.createElement('calcChainCellUpdated') | ||
wrapperElement.appendChild(newCalcCellEl) | ||
// on the contrary with the merge cells case, the calcChainCellUpdated is inserted | ||
// in the cell, so there is no need for a wrapper that only renders it | ||
// for the first item in loop | ||
cellEl.insertBefore(wrapperElement, cellEl.firstChild) | ||
} | ||
} | ||
@@ -587,6 +567,6 @@ | ||
processOpeningTag(sheetDoc, outOfLoopEl.firstChild, rowHandlebarsWrapperText) | ||
processClosingTag(sheetDoc, outOfLoopEl.lastChild, '{{/xlsxSData}}') | ||
processClosingTag(sheetDoc, outOfLoopEl.lastChild, '{{/_R}}') | ||
processOpeningTag(sheetDoc, outOfLoopEl.firstChild, `{{#xlsxSData type='outOfLoop' item='${outLoopItemIndex}' }}`) | ||
processClosingTag(sheetDoc, outOfLoopEl.lastChild, '{{/xlsxSData}}') | ||
processOpeningTag(sheetDoc, outOfLoopEl.firstChild, `{{#_D t='outOfLoop' item='${outLoopItemIndex}' }}`) | ||
processClosingTag(sheetDoc, outOfLoopEl.lastChild, '{{/_D}}') | ||
@@ -607,3 +587,3 @@ const loopEdgeEl = loopDetected.blockStartEl | ||
const contentEl = sheetDoc.createElement('xlsxRemove') | ||
contentEl.textContent = `{{xlsxSData type='outOfLoopPlaceholder' item='${outLoopItemIndex}' }}` | ||
contentEl.textContent = `{{_D t='outOfLoopPlaceholder' item='${outLoopItemIndex}' }}` | ||
@@ -644,3 +624,3 @@ outOfLoopPlaceholderEl.appendChild(contentEl) | ||
newMergeCellCallEl.textContent = `{{xlsxSData type='mergeCell' originalCellRefRange='${ref}'}}` | ||
newMergeCellCallEl.textContent = `{{_D t='m' o='${ref}'}}` | ||
@@ -680,6 +660,6 @@ const mergeStartCellRef = ref.split(':')[0] | ||
mergeCellsEl.setAttribute('count', '{{@mergeCellsCount}}') | ||
mergeCellsEl.setAttribute('count', "{{_D t='mergeCellsCount'}}") | ||
processOpeningTag(sheetDoc, mergeCellsEl, "{{#xlsxSData type='mergeCells'}}") | ||
processClosingTag(sheetDoc, mergeCellsEl, '{{/xlsxSData}}') | ||
processOpeningTag(sheetDoc, mergeCellsEl, "{{#_D t='mergeCells'}}") | ||
processClosingTag(sheetDoc, mergeCellsEl, '{{/_D}}') | ||
@@ -690,8 +670,8 @@ for (const mergeCellEl of mergeCellEls) { | ||
processOpeningTag(sheetDoc, mergeCellEl, `{{#xlsxSData type='mergeCellItem' originalCellRefRange='${originalCellRefRange}'}}`) | ||
processClosingTag(sheetDoc, mergeCellEl, '{{/xlsxSData}}') | ||
processOpeningTag(sheetDoc, mergeCellEl, `{{#_D t='mI' o='${originalCellRefRange}'}}`) | ||
processClosingTag(sheetDoc, mergeCellEl, '{{/_D}}') | ||
} | ||
processOpeningTag(sheetDoc, mergeCellsEl.firstChild, "{{#xlsxSData type='mergeCellsItems'}}") | ||
processClosingTag(sheetDoc, mergeCellsEl.lastChild, '{{/xlsxSData}}') | ||
processOpeningTag(sheetDoc, mergeCellsEl.firstChild, "{{#_D t='mergeCellsItems'}}") | ||
processClosingTag(sheetDoc, mergeCellsEl.lastChild, '{{/_D}}') | ||
} | ||
@@ -701,3 +681,3 @@ | ||
for (const { cellRef, formula, formulaEl, sharedFormula, cellRefsInFormula } of formulaCellElsToHandle) { | ||
for (const { formula, formulaEl, sharedFormula, cellRefsInFormula } of formulaCellElsToHandle) { | ||
if (sharedFormula?.type === 'reference') { | ||
@@ -707,6 +687,6 @@ continue | ||
formulaEl.textContent = `{{xlsxSData type='formula' originalCellRef='${cellRef}' originalFormula='${jsSingleQuoteEscape(formula)}'` | ||
formulaEl.textContent = `{{_D t='f' o='${jsSingleQuoteEscape(formula)}'` | ||
if (sharedFormula?.type === 'source') { | ||
formulaEl.setAttribute('ref', `{{xlsxSData type='formulaSharedRefRange' originalSharedRefRange='${jsSingleQuoteEscape(sharedFormula.sourceRef)}'}}`) | ||
formulaEl.setAttribute('ref', `{{_D t='fs' o='${jsSingleQuoteEscape(sharedFormula.sourceRef)}'}}`) | ||
} | ||
@@ -724,3 +704,3 @@ | ||
const normalizedCellRef = cellRefInfo.localRef | ||
const targetCellEl = cellsElsByRefMap.get(normalizedCellRef) | ||
const targetCellEl = cellsElsByRefMap.get(normalizedCellRef)?.[0] | ||
@@ -751,3 +731,3 @@ if (targetCellEl != null) { | ||
if (formulaNotExistingCellRefs.size > 0) { | ||
rootBlockStartEl.textContent = rootBlockStartEl.textContent.replace('}}', ` nonExistingCellRefs='${[...formulaNotExistingCellRefs].join(',')}'}}`) | ||
sheetDataCallProps.nonExistingCellRefs = [...formulaNotExistingCellRefs].join(',') | ||
} | ||
@@ -769,4 +749,26 @@ | ||
const newLazyFormulasCallEl = sheetDoc.createElement('xlsxRemove') | ||
newLazyFormulasCallEl.textContent = '{{xlsxSData type="lazyFormulas"}}' | ||
newLazyFormulasCallEl.textContent = "{{_D t='lazyFormulas'}}" | ||
sheetDataEl.appendChild(newLazyFormulasCallEl) | ||
if (isAutofitConfigured) { | ||
const colsProps = {} | ||
const baseCols = [] | ||
for (const [cellLetter, maxSize] of colMaxSizeMap) { | ||
baseCols.push(`${cellLetter}:${maxSize}`) | ||
} | ||
if (baseCols.length > 0) { | ||
sheetDataCallProps.autofitBCols = baseCols.join(',') | ||
} | ||
processOpeningTag(sheetDoc, colsEl, getDataHelperCall('autofit', colsProps)) | ||
processClosingTag(sheetDoc, colsEl, '{{/_D}}') | ||
} | ||
sheetDataBlockStartEl.textContent = getDataHelperCall('sd', sheetDataCallProps) | ||
processOpeningTag(sheetDoc, sheetDoc.documentElement.firstChild, getDataHelperCall('ws', { sheetId: sheetInfo.id })) | ||
processClosingTag(sheetDoc, sheetDoc.documentElement.lastChild, '{{/_D}}') | ||
} | ||
@@ -783,7 +785,37 @@ | ||
if (tEl.textContent.includes('{{') && tEl.textContent.includes('}}')) { | ||
tEl.textContent = `{{{{xlsxSData type='raw'}}}}${tEl.textContent}{{{{/xlsxSData}}}}` | ||
tEl.textContent = `{{{{_D t='raw'}}}}${tEl.textContent}{{{{/_D}}}}` | ||
} | ||
} | ||
// place handlebars call that handle updating the calcChain | ||
if (calcChainDoc != null) { | ||
processOpeningTag(calcChainDoc, calcChainDoc.documentElement.firstChild, "{{#_D t='calcChain'}}") | ||
processClosingTag(calcChainDoc, calcChainDoc.documentElement.lastChild, '{{/_D}}') | ||
} | ||
} | ||
function getDataHelperCall (type, props, isBlock = true) { | ||
let callStr = `{{${isBlock ? '#' : ''}_D t='${type}'` | ||
const targetProps = props || {} | ||
const keys = Object.keys(targetProps) | ||
for (const key of keys) { | ||
const value = targetProps[key] | ||
if (value == null) { | ||
continue | ||
} | ||
if (typeof value === 'number' || typeof value === 'boolean') { | ||
callStr += ` ${key}=${value}` | ||
} else { | ||
callStr += ` ${key}='${value}'` | ||
} | ||
} | ||
callStr += '}}' | ||
return callStr | ||
} | ||
function getOutOfLoopElsSortedByHierarchy (outOfLoopElsToHandle) { | ||
@@ -930,3 +962,3 @@ const hierarchy = new Map() | ||
currentEl[target].nodeName === 'xlsxRemove' && | ||
currentEl[target].textContent.startsWith(target === 'previousSibling' ? '{{#xlsxSData' : '{{/xlsxSData') | ||
currentEl[target].textContent.startsWith(target === 'previousSibling' ? '{{#_D' : '{{/_D') | ||
) | ||
@@ -968,3 +1000,3 @@ | ||
// if it is xlsxRemove but it is not end point then we are ready | ||
!currentEl[target].textContent.startsWith(type === 'previous' ? '{{#xlsxSData' : '{{/xlsxSData') | ||
!currentEl[target].textContent.startsWith(type === 'previous' ? '{{#_D' : '{{/_D') | ||
) | ||
@@ -1131,60 +1163,2 @@ } | ||
function findCellFontSize (cellEl, styleInfo) { | ||
let styleId = cellEl.getAttribute('s') | ||
if (styleId != null && styleId !== '') { | ||
styleId = parseInt(styleId, 10) | ||
} else { | ||
styleId = 0 | ||
} | ||
const cellXfsEls = styleInfo.cellXfsEls ?? nodeListToArray(styleInfo.doc.getElementsByTagName('cellXfs')[0]?.getElementsByTagName('xf')) | ||
const cellStyleXfsEls = styleInfo.cellStyleXfsEls ?? nodeListToArray(styleInfo.doc.getElementsByTagName('cellStyleXfs')[0]?.getElementsByTagName('xf')) | ||
const fontEls = styleInfo.fontEls ?? nodeListToArray(styleInfo.doc.getElementsByTagName('fonts')[0]?.getElementsByTagName('font')) | ||
const selectedXfEl = cellXfsEls[styleId] | ||
let fontId = selectedXfEl.getAttribute('fontId') | ||
const applyFont = selectedXfEl.getAttribute('applyFont') | ||
const xfId = selectedXfEl.getAttribute('xfId') | ||
if ( | ||
applyFont == null || | ||
applyFont === '' || | ||
applyFont === '0' | ||
) { | ||
const selectedStyleXfEl = cellStyleXfsEls[xfId] | ||
const nestedFontId = selectedStyleXfEl.getAttribute('fontId') | ||
const nestedApplyFont = selectedStyleXfEl.getAttribute('applyFont') | ||
if ( | ||
nestedApplyFont == null || | ||
nestedApplyFont === '' || | ||
nestedApplyFont === '1' | ||
) { | ||
fontId = nestedFontId | ||
} | ||
} | ||
const fontEl = fontEls[fontId] | ||
const sizeEl = fontEl.getElementsByTagName('sz')[0] | ||
// size stored in xlsx is in pt | ||
return parseFloat(sizeEl.getAttribute('val')) | ||
} | ||
function findCellElInCalcChain (sheetId, cellRef, calcChainEls) { | ||
const foundIndex = calcChainEls.findIndex((el) => { | ||
return el.getAttribute('r') === cellRef && el.getAttribute('i') === sheetId | ||
}) | ||
if (foundIndex === -1) { | ||
return | ||
} | ||
const cellEl = calcChainEls[foundIndex] | ||
return cellEl | ||
} | ||
function findAutofitConfigured (sheetFilepath, sheetDoc, sheetRelsDoc, files) { | ||
@@ -1191,0 +1165,0 @@ const result = [] |
@@ -155,3 +155,3 @@ const path = require('path') | ||
if (ptValueEl.textContent.includes('{{') && ptValueEl.textContent.includes('}}')) { | ||
ptValueEl.textContent = `{{{{xlsxSData type='raw'}}}}${ptValueEl.textContent}{{{{/xlsxSData}}}}` | ||
ptValueEl.textContent = `{{{{_D t='raw'}}}}${ptValueEl.textContent}{{{{/_D}}}}` | ||
} | ||
@@ -158,0 +158,0 @@ } |
const { DOMParser, XMLSerializer } = require('@xmldom/xmldom') | ||
const { decode } = require('html-entities') | ||
const { customAlphabet } = require('nanoid') | ||
const { decompress, saveXmlsToOfficeFile } = require('@jsreport/office') | ||
const generateRandomId = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 4) | ||
const preprocess = require('./preprocess/preprocess') | ||
const postprocess = require('./postprocess/postprocess') | ||
const { contentIsXML, isWorksheetFile } = require('../utils') | ||
const { contentIsXML, isWorksheetFile, getStyleFile } = require('../utils') | ||
const { decodeXML } = require('../cellUtils') | ||
const decodeXML = (str) => decode(str, { level: 'xml' }) | ||
module.exports = (reporter) => async (inputs, req) => { | ||
@@ -34,28 +34,92 @@ const { xlsxTemplateContent, options, outputPath } = inputs | ||
const filesToRender = files.filter(f => contentIsXML(f.data)) | ||
const [filesToRender, styleFile] = ensureOrderOfFiles(files.filter(f => contentIsXML(f.data))) | ||
const contentToRender = filesToRender.map(f => { | ||
const xmlStr = new XMLSerializer().serializeToString(f.doc, undefined, (node) => { | ||
const normalizeAttributeAndTextNode = (node) => { | ||
if (node.nodeType === 2 && node.nodeValue && node.nodeValue.includes('{{')) { | ||
// we need to decode the xml entities for the attributes for handlebars to work ok | ||
if (node.nodeType === 2 && node.nodeValue && node.nodeValue.includes('{{')) { | ||
const str = new XMLSerializer().serializeToString(node) | ||
return decodeXML(str) | ||
} else if ( | ||
// we need to decode the xml entities in text nodes for handlebars to work ok with partials | ||
node.nodeType === 3 && node.nodeValue && | ||
(node.nodeValue.includes('{{>') || node.nodeValue.includes('{{#>')) | ||
) { | ||
const str = new XMLSerializer().serializeToString(node) | ||
const str = new XMLSerializer().serializeToString(node) | ||
return decodeXML(str) | ||
} else if ( | ||
// we need to decode the xml entities in text nodes for handlebars to work ok with partials | ||
node.nodeType === 3 && node.nodeValue && | ||
(node.nodeValue.includes('{{>') || node.nodeValue.includes('{{#>')) | ||
) { | ||
const str = new XMLSerializer().serializeToString(node) | ||
return str.replace(/{{#?>/g, (m) => { | ||
return decodeXML(m) | ||
}) | ||
return str.replace(/{{#?>/g, (m) => { | ||
return decodeXML(m) | ||
}) | ||
} | ||
return node | ||
} | ||
let contentToRender = filesToRender.map(f => { | ||
let xmlStr = new XMLSerializer().serializeToString(f.doc, undefined, (node) => { | ||
if (node.nodeType === 1 && node.localName === 'c' && node.hasAttribute('__CT_t__')) { | ||
let callType = node.getAttribute('__CT_t__') | ||
const hasMultipleExpressionCall = node.getAttribute('__CT_m__') === '1' | ||
const calcChainUpdate = node.getAttribute('__CT_cCU__') === '1' | ||
const expressionValue = node.getAttribute('__CT_v__') | ||
const escapeValue = node.getAttribute('__CT_ve__') === '1' | ||
if (calcChainUpdate) { | ||
callType = callType.toLowerCase() | ||
} | ||
node.removeAttribute('__CT_t__') | ||
node.removeAttribute('__CT_m__') | ||
node.removeAttribute('__CT_cCU__') | ||
node.removeAttribute('__CT_v__') | ||
node.removeAttribute('__CT_ve__') | ||
// this will take care of removing xmlns, xmlns:prefix attributes that we don't want here | ||
const str = new XMLSerializer().serializeToString( | ||
node, | ||
undefined, | ||
normalizeAttributeAndTextNode | ||
).replace(/ xmlns(:[a-z0-9]+)?="[^"]*"/g, '') | ||
const isSelfClosing = node.childNodes.length === 0 | ||
let attrs | ||
let content | ||
if (isSelfClosing) { | ||
const closeTagStartIdx = str.lastIndexOf('/>') | ||
attrs = str.slice(2, closeTagStartIdx) | ||
content = '' | ||
} else { | ||
const openTagEndIdx = str.indexOf('>') | ||
const closeTagStartIdx = str.lastIndexOf('</') | ||
attrs = str.slice(2, openTagEndIdx) | ||
content = str.slice(openTagEndIdx + 1, closeTagStartIdx) | ||
} | ||
if (hasMultipleExpressionCall) { | ||
return `{{#${callType}${attrs}}}${content}{{/${callType}}}` | ||
} | ||
// for the handlebars call we want to avoid the extra character for escape param, | ||
// (size optimization) since the common case is that the handlebars call is | ||
// escaped {{}} expression, so instead we expect in the helper receive if the | ||
// value should be raw or not (the inverse of escape) | ||
return `{{${callType} ${expressionValue}${escapeValue ? '' : ' 1'}${attrs}}}` | ||
} | ||
return node | ||
return normalizeAttributeAndTextNode(node) | ||
}) | ||
return xmlStr.replace(/<xlsxRemove>/g, '').replace(/<\/xlsxRemove>/g, '') | ||
xmlStr = xmlStr.replace(/<xlsxRemove>/g, '').replace(/<\/xlsxRemove>/g, '') | ||
if (meta.autofitConfigured && styleFile?.path === f.path) { | ||
xmlStr = `{{#_D t='style'}}${xmlStr}{{/_D}}` | ||
} | ||
return xmlStr | ||
}).join('$$$xlsxFile$$$') | ||
contentToRender = `{{#xlsxContext type="global" evalId="${generateRandomId()}"}}${contentToRender}{{/xlsxContext}}` | ||
reporter.logger.debug('Starting child request to render xlsx dynamic parts for generation step', req) | ||
@@ -123,1 +187,45 @@ | ||
} | ||
function ensureOrderOfFiles (files) { | ||
// we want to ensure a specific order of files for the render processing, | ||
// 1. ensure style file comes as the first | ||
// 2. ensure calcChain.xml comes after sheet files (we just put it at the end of everything) | ||
// this is required in child render for our handlebars logic to correctly update the calcChain | ||
const calcChainIdx = files.findIndex((file) => file.path === 'xl/calcChain.xml') | ||
const filesSorted = [] | ||
const skipIndexesSet = new Set() | ||
const styleFile = getStyleFile(files) | ||
let styleIdx = -1 | ||
if (styleFile != null) { | ||
styleIdx = files.findIndex((file) => file.path === styleFile.path) | ||
} | ||
for (const idx of [styleIdx, calcChainIdx]) { | ||
if (idx === -1) { | ||
continue | ||
} | ||
skipIndexesSet.add(idx) | ||
} | ||
if (styleIdx !== -1) { | ||
filesSorted.push(files[styleIdx]) | ||
} | ||
for (let fileIdx = 0; fileIdx < files.length; fileIdx++) { | ||
if (skipIndexesSet.has(fileIdx)) { | ||
continue | ||
} | ||
filesSorted.push(files[fileIdx]) | ||
} | ||
if (calcChainIdx !== -1) { | ||
filesSorted.push(files[calcChainIdx]) | ||
} | ||
return [filesSorted, styleFile] | ||
} |
@@ -211,2 +211,44 @@ const path = require('path') | ||
function getStyleFile (files) { | ||
const workbookRelsDoc = files.find((file) => file.path === 'xl/_rels/workbook.xml.rels')?.doc | ||
if (workbookRelsDoc == null) { | ||
return | ||
} | ||
const workbookRelsEls = nodeListToArray(workbookRelsDoc.getElementsByTagName('Relationship')) | ||
const styleRel = workbookRelsEls.find((el) => el.getAttribute('Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles') | ||
if (styleRel == null) { | ||
return | ||
} | ||
const stylePath = path.posix.join(path.posix.dirname('xl/workbook.xml'), styleRel.getAttribute('Target')) | ||
const stylesFile = files.find((file) => file.path === stylePath) | ||
if (stylesFile == null) { | ||
return | ||
} | ||
return stylesFile | ||
} | ||
function getStyleInfo (styleDoc) { | ||
if (styleDoc == null) { | ||
return | ||
} | ||
const cellXfsEls = nodeListToArray(styleDoc.getElementsByTagName('cellXfs')[0]?.getElementsByTagName('xf')) | ||
const cellStyleXfsEls = nodeListToArray(styleDoc.getElementsByTagName('cellStyleXfs')[0]?.getElementsByTagName('xf')) | ||
const fontEls = nodeListToArray(styleDoc.getElementsByTagName('fonts')[0]?.getElementsByTagName('font')) | ||
return { | ||
doc: styleDoc, | ||
cellXfsEls, | ||
cellStyleXfsEls, | ||
fontEls | ||
} | ||
} | ||
function isWorksheetFile (filePath) { | ||
@@ -249,2 +291,4 @@ return path.posix.dirname(filePath) === 'xl/worksheets' && filePath.endsWith('.xml') | ||
module.exports.getSheetInfo = getSheetInfo | ||
module.exports.getStyleFile = getStyleFile | ||
module.exports.getStyleInfo = getStyleInfo | ||
module.exports.findOrCreateChildNode = findOrCreateChildNode | ||
@@ -251,0 +295,0 @@ module.exports.findChildNode = findChildNode |
@@ -16,2 +16,17 @@ const fs = require('fs').promises | ||
reporter.options.sandbox.modules.push({ | ||
alias: 'xlsxProcessStyle', | ||
path: path.join(__dirname, './processStyle.js') | ||
}) | ||
reporter.options.sandbox.modules.push({ | ||
alias: 'xlsxProcessCalcChain', | ||
path: path.join(__dirname, './processCalcChain.js') | ||
}) | ||
reporter.options.sandbox.modules.push({ | ||
alias: 'xlsxProcessAutofitCols', | ||
path: path.join(__dirname, './processAutofitCols.js') | ||
}) | ||
reporter.options.sandbox.modules.push({ | ||
alias: 'lodash', | ||
@@ -26,7 +41,2 @@ path: require.resolve('lodash') | ||
reporter.options.sandbox.modules.push({ | ||
alias: 'string-pixel-width', | ||
path: require.resolve('string-pixel-width') | ||
}) | ||
if (reporter.options.sandbox.allowedModules !== '*') { | ||
@@ -33,0 +43,0 @@ reporter.options.sandbox.allowedModules.push('path') |
{ | ||
"name": "@jsreport/jsreport-xlsx", | ||
"version": "4.1.1", | ||
"version": "4.2.0", | ||
"description": "jsreport recipe rendering excels directly from open xml", | ||
@@ -40,2 +40,3 @@ "keywords": [ | ||
"moment": "2.29.4", | ||
"nanoid": "3.2.0", | ||
"node.extend.without.arrays": "1.1.6", | ||
@@ -45,3 +46,2 @@ "semaphore-async-await": "1.5.1", | ||
"string-replace-async": "2.0.0", | ||
"uuid": "8.3.2", | ||
"xlsx-coordinates": "1.0.1", | ||
@@ -52,4 +52,4 @@ "xml2js-preserve-spaces": "0.0.1" | ||
"@jsreport/jsreport-assets": "4.1.1", | ||
"@jsreport/jsreport-components": "4.0.1", | ||
"@jsreport/jsreport-core": "4.3.0", | ||
"@jsreport/jsreport-components": "4.0.2", | ||
"@jsreport/jsreport-core": "4.4.1", | ||
"@jsreport/jsreport-data": "4.1.0", | ||
@@ -62,2 +62,3 @@ "@jsreport/jsreport-handlebars": "4.0.1", | ||
"mocha": "10.1.0", | ||
"sax": "1.4.1", | ||
"should": "13.2.3", | ||
@@ -64,0 +65,0 @@ "standard": "16.0.4", |
@@ -10,2 +10,9 @@ # @jsreport/jsreport-xlsx | ||
### 4.2.0 | ||
- keep existing dimension when appropriate | ||
- fix escaping issue in formulas | ||
- optimization for autofit cols and cell type auto detection helpers | ||
- performance improvement for templates with lots of cells in calcChain (using lot of formulas) | ||
### 4.1.1 | ||
@@ -12,0 +19,0 @@ |
@@ -11,2 +11,16 @@ /* eslint no-unused-vars: 0 */ | ||
const enabledFor = options?.data?.meta?.autofit?.enabledFor ?? [] | ||
const allColsEnabled = enabledFor[0] === true | ||
if (!allColsEnabled) { | ||
const cols = options?.data?.meta?.autofit?.cols ?? {} | ||
for (const colLetter of Object.keys(cols)) { | ||
// remove cells that are not enabled for autofit | ||
if (!enabledFor.includes(colLetter)) { | ||
delete cols[colLetter] | ||
} | ||
} | ||
} | ||
return '' | ||
@@ -37,3 +51,3 @@ } | ||
) { | ||
throw new Error('docxChart helper when options parameter is set, it should be an object') | ||
throw new Error('xlsxChart helper when options parameter is set, it should be an object') | ||
} | ||
@@ -44,65 +58,73 @@ | ||
function xlsxSData (data, options) { | ||
function xlsxContext (options) { | ||
const Handlebars = require('handlebars') | ||
const optionsToUse = options == null ? data : options | ||
const type = optionsToUse.hash.type | ||
let data | ||
if (type == null) { | ||
throw new Error('xlsxSData helper type arg is required') | ||
if (options.hash.type === 'global') { | ||
data = Handlebars.createFrame(options.data) | ||
data.evalId = options.hash.evalId | ||
data.calcChainUpdatesMap = new Map() | ||
data.styleInfo = null | ||
data.styleFontSizeMap = new Map() | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'raw' | ||
) { | ||
return optionsToUse.fn() | ||
const context = {} | ||
if (data) { | ||
context.data = data | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'root' | ||
) { | ||
const newData = Handlebars.createFrame({}) | ||
let nonExistingCellRefs = optionsToUse.hash.nonExistingCellRefs != null ? optionsToUse.hash.nonExistingCellRefs.split(',') : [] | ||
const autofit = optionsToUse.hash.autofit != null ? optionsToUse.hash.autofit.split(',') : [] | ||
const trackedCells = {} | ||
const result = options.fn(this, context) | ||
if (nonExistingCellRefs.length > 0) { | ||
nonExistingCellRefs = nonExistingCellRefs.map((cellRef) => { | ||
const parts = cellRef.split('|') | ||
const result = { | ||
ref: parts[0] | ||
} | ||
return result | ||
} | ||
if (parts.length === 2) { | ||
result.inLoop = true | ||
result.loopHierarchyId = parts[1] | ||
} | ||
const __xlsxD = (function () { | ||
function ws (options) { | ||
const Handlebars = require('handlebars') | ||
const newData = Handlebars.createFrame(options.data) | ||
const sheetId = options.hash.sheetId | ||
return result | ||
}) | ||
assertOk(sheetId != null, 'sheetId arg is required') | ||
for (const cellRefEntry of nonExistingCellRefs) { | ||
trackedCells[cellRefEntry.ref] = { | ||
first: cellRefEntry.ref, | ||
last: cellRefEntry.ref, | ||
count: 0 | ||
newData.sheetId = sheetId | ||
const tasks = new Map() | ||
newData.tasks = { | ||
wait (key) { | ||
return tasks.get(key)?.promise | ||
}, | ||
add (key) { | ||
let taskExecution = tasks.get(key) | ||
if (taskExecution != null) { | ||
return [taskExecution.resolve, taskExecution.reject] | ||
} | ||
if (cellRefEntry.inLoop) { | ||
trackedCells[cellRefEntry.ref].inLoop = cellRefEntry.inLoop | ||
trackedCells[cellRefEntry.ref].loopHierarchyId = cellRefEntry.loopHierarchyId | ||
} else { | ||
trackedCells[cellRefEntry.ref].inLoop = false | ||
} | ||
taskExecution = {} | ||
taskExecution.promise = new Promise((resolve, reject) => { | ||
taskExecution.resolve = resolve | ||
taskExecution.reject = reject | ||
}) | ||
tasks.set(key, taskExecution) | ||
return [taskExecution.resolve, taskExecution.reject] | ||
} | ||
} | ||
// init tasks | ||
newData.tasks.add('sd') | ||
newData.tasks.add('lazyFormulas') | ||
newData.meta = { | ||
calcChainCellRefsSet: null, | ||
autofit: { | ||
cols: {}, | ||
enabledFor: autofit | ||
enabledFor: null | ||
}, | ||
mergeCells: [], | ||
trackedCells, | ||
trackedCells: null, | ||
updatedOriginalCells: {}, | ||
@@ -117,99 +139,123 @@ lazyFormulas: {}, | ||
return optionsToUse.fn(this, { data: newData }) | ||
return options.fn(this, { data: newData }) | ||
} | ||
const getLoopItemById = (byTarget, loopItems) => { | ||
if (byTarget == null) { | ||
throw new Error('getLoopItemById byTarget arg is invalid') | ||
} | ||
function sd (options) { | ||
const [resolveTask, rejectTask] = options.data.tasks.add('sd') | ||
if (!Array.isArray(loopItems)) { | ||
throw new Error('getLoopItemById loopItems arg is invalid') | ||
} | ||
try { | ||
const Handlebars = require('handlebars') | ||
const { idName, idValue } = byTarget | ||
options.data.meta.calcChainCellRefsSet = new Set(options.hash.calcChainCellRefs != null ? options.hash.calcChainCellRefs.split(',') : []) | ||
if (idName == null || typeof idName !== 'string') { | ||
throw new Error('getLoopItemById byTarget.idName arg is invalid') | ||
} | ||
let nonExistingCellRefs = options.hash.nonExistingCellRefs != null ? options.hash.nonExistingCellRefs.split(',') : [] | ||
const autofitEnabledFor = options.hash.autofit != null ? options.hash.autofit.split(',') : [] | ||
if (idName !== 'hierarchyId' && idName !== 'id') { | ||
throw new Error('getLoopItemById byTarget.idName should be either "hierarchyId" or "id"') | ||
} | ||
const autofitBaseCols = options.hash.autofitBCols != null | ||
? options.hash.autofitBCols.split(',').map((entry) => { | ||
const [colLetter, size] = entry.split(':') | ||
return [colLetter, parseFloat(size)] | ||
}) | ||
: [] | ||
if (idValue == null || typeof idValue !== 'string') { | ||
throw new Error('getLoopItemById byTarget.idValue arg is invalid') | ||
} | ||
for (const [colLetter, size] of autofitBaseCols) { | ||
options.data.meta.autofit.cols[colLetter] = { | ||
size | ||
} | ||
} | ||
const idParts = idValue.split('#') | ||
let ctx = { children: loopItems } | ||
let targetIdValue = '' | ||
let parent | ||
const trackedCells = {} | ||
while (idParts.length > 0) { | ||
const idx = idParts.shift() | ||
if (nonExistingCellRefs.length > 0) { | ||
nonExistingCellRefs = nonExistingCellRefs.map((cellRef) => { | ||
const parts = cellRef.split('|') | ||
const result = { | ||
ref: parts[0] | ||
} | ||
targetIdValue = targetIdValue !== '' ? `${targetIdValue}#${idx}` : `${idx}` | ||
if (parts.length === 2) { | ||
result.inLoop = true | ||
result.loopHierarchyId = parts[1] | ||
} | ||
const matches = ctx.children.filter((c) => c[idName] === targetIdValue) | ||
const result = matches[matches.length - 1] | ||
return result | ||
}) | ||
if (result == null) { | ||
break | ||
for (const cellRefEntry of nonExistingCellRefs) { | ||
trackedCells[cellRefEntry.ref] = { | ||
first: cellRefEntry.ref, | ||
last: cellRefEntry.ref, | ||
count: 0 | ||
} | ||
if (cellRefEntry.inLoop) { | ||
trackedCells[cellRefEntry.ref].inLoop = cellRefEntry.inLoop | ||
trackedCells[cellRefEntry.ref].loopHierarchyId = cellRefEntry.loopHierarchyId | ||
} else { | ||
trackedCells[cellRefEntry.ref].inLoop = false | ||
} | ||
} | ||
} | ||
ctx = result | ||
options.data.meta.autofit.enabledFor = autofitEnabledFor | ||
options.data.meta.trackedCells = trackedCells | ||
if (idParts.length === 0) { | ||
parent = ctx | ||
} | ||
const result = options.fn(this, { data: options.data }) | ||
resolveTask() | ||
return result | ||
} catch (e) { | ||
rejectTask(e) | ||
throw e | ||
} | ||
return parent | ||
} | ||
const getParentLoopItemByHierarchy = (childLoopItem, loopItems) => { | ||
if (childLoopItem == null) { | ||
throw new Error('getParentLoopItemByHierarchy childLoopItem arg is invalid') | ||
} | ||
// this helper is async on purpose so it can wait for the sd helper to finish. | ||
// this is needed because the dimension tag appears before the sheetData tag and | ||
// we want to keep to logic for updating this node in handlebars | ||
async function dimension (options) { | ||
const originalCellRefRange = options.hash.o | ||
if (!Array.isArray(loopItems)) { | ||
throw new Error('getParentLoopItemByHierarchy loopItems arg is invalid') | ||
} | ||
assertOk(originalCellRefRange != null, 'originalCellRefRange arg is required') | ||
const parentHierarchyId = childLoopItem.hierarchyId.split('#').slice(0, -1).join('#') | ||
await options.data.tasks.wait('sd') | ||
if (parentHierarchyId === '') { | ||
return | ||
const refsParts = originalCellRefRange.split(':') | ||
if (refsParts.length === 1) { | ||
return refsParts[0] | ||
} | ||
return getLoopItemById({ idName: 'hierarchyId', idValue: parentHierarchyId }, loopItems) | ||
} | ||
const { parseCellRef } = require('cellUtils') | ||
const lastCellRef = options.data.meta.lastCellRef | ||
const parsedEndCellRef = parseCellRef(refsParts[1]) | ||
const parsedLastCellRef = parseCellRef(lastCellRef) | ||
if ( | ||
arguments.length === 2 && | ||
type === 'loop' | ||
) { | ||
const start = optionsToUse.hash.start | ||
const columnStart = optionsToUse.hash.columnStart | ||
const end = optionsToUse.hash.end | ||
const columnEnd = optionsToUse.hash.columnEnd | ||
const hierarchyId = optionsToUse.hash.hierarchyId | ||
const newData = Handlebars.createFrame(optionsToUse.data) | ||
if (start == null) { | ||
throw new Error('xlsxSData type="loop" helper start arg is required') | ||
// if existing dimension is bigger than the last cell with content | ||
// then we keep the existing dimension | ||
if ( | ||
parsedEndCellRef.rowNumber > parsedLastCellRef.rowNumber || | ||
( | ||
parsedEndCellRef.rowNumber === parsedLastCellRef.rowNumber && | ||
parsedEndCellRef.columnNumber > parsedLastCellRef.columnNumber | ||
) | ||
) { | ||
return originalCellRefRange | ||
} | ||
if (columnStart == null) { | ||
throw new Error('xlsxSData type="loop" helper columnStart arg is required') | ||
} | ||
return `${refsParts[0]}:${parsedEndCellRef.letter}${parsedLastCellRef.rowNumber}` | ||
} | ||
if (columnEnd == null) { | ||
throw new Error('xlsxSData type="loop" helper columnEnd arg is required') | ||
} | ||
function loop (data, options) { | ||
const Handlebars = require('handlebars') | ||
const start = options.hash.start | ||
const columnStart = options.hash.columnStart | ||
const end = options.hash.end | ||
const columnEnd = options.hash.columnEnd | ||
const hierarchyId = options.hash.hierarchyId | ||
const newData = Handlebars.createFrame(options.data) | ||
if (hierarchyId == null) { | ||
throw new Error('xlsxSData type="loop" helper hierarchyId arg is required') | ||
} | ||
assertOk(start != null, 'start arg is required') | ||
assertOk(columnStart != null, 'columnStart arg is required') | ||
assertOk(columnEnd != null, 'columnEnd arg is required') | ||
assertOk(hierarchyId != null, 'hierarchyId arg is required') | ||
@@ -236,3 +282,3 @@ let targetData = data | ||
length: targetData.length, | ||
parentLoopIndex: optionsToUse.data.index, | ||
parentLoopIndex: options.data.index, | ||
children: [], | ||
@@ -259,3 +305,3 @@ completed: false | ||
const result = Handlebars.helpers.each(targetData, { ...optionsToUse, data: newData }) | ||
const result = Handlebars.helpers.each(targetData, { ...options, data: newData }) | ||
@@ -267,14 +313,10 @@ loopItem.completed = true | ||
if ( | ||
arguments.length === 1 && | ||
type === 'outOfLoop' | ||
) { | ||
const item = optionsToUse.hash.item | ||
function outOfLoop (options) { | ||
const Handlebars = require('handlebars') | ||
const item = options.hash.item | ||
if (item == null) { | ||
throw new Error('xlsxSData type="outOfLoop" helper item arg is required') | ||
} | ||
assertOk(item != null, 'item arg is required') | ||
optionsToUse.data.outOfLoopTemplates[item] = (currentLoopId, currentIdx) => { | ||
const newData = Handlebars.createFrame(optionsToUse.data) | ||
options.data.outOfLoopTemplates[item] = (currentLoopId, currentIdx) => { | ||
const newData = Handlebars.createFrame(options.data) | ||
@@ -287,3 +329,3 @@ newData.currentLoopId = currentLoopId | ||
return optionsToUse.fn(this, { data: newData }) | ||
return options.fn(this, { data: newData }) | ||
} | ||
@@ -294,25 +336,17 @@ | ||
if ( | ||
arguments.length === 1 && | ||
type === 'outOfLoopPlaceholder' | ||
) { | ||
const item = optionsToUse.hash.item | ||
function outOfLoopPlaceholder (options) { | ||
const Handlebars = require('handlebars') | ||
const item = options.hash.item | ||
if (item == null) { | ||
throw new Error('xlsxSData type="outOfLoopPlaceholder" helper item arg is required') | ||
} | ||
assertOk(item != null, 'item arg is required') | ||
const outOfLoopTemplate = optionsToUse.data.outOfLoopTemplates[item] | ||
const outOfLoopTemplate = options.data.outOfLoopTemplates[item] | ||
if (outOfLoopTemplate == null) { | ||
throw new Error('xlsxSData type="outOfLoopPlaceholder" helper invalid usage, outOfLoopItem was not found') | ||
} | ||
assertOk(outOfLoopTemplate != null, 'outOfLoopItem was not found') | ||
const currentLoopId = optionsToUse.data.currentLoopId | ||
const currentLoopId = options.data.currentLoopId | ||
if (currentLoopId == null) { | ||
throw new Error('xlsxSData type="outOfLoopPlaceholder" helper invalid usage, currentLoopId not found') | ||
} | ||
assertOk(currentLoopId != null, 'currentLoopId not found') | ||
const currentIdx = optionsToUse.data.index | ||
const currentIdx = options.data.index | ||
@@ -324,164 +358,10 @@ const output = outOfLoopTemplate(currentLoopId, currentIdx) | ||
const { parseCellRef, evaluateCellRefsFromExpression, getNewFormula, generateNewCellRefFromRow } = require('cellUtils') | ||
function r (options) { | ||
const Handlebars = require('handlebars') | ||
const originalRowNumber = options.hash.o | ||
const getCurrentLoopItem = (loopId, loopItems) => { | ||
if (!Array.isArray(loopItems)) { | ||
throw new Error('getCurrentLoopItem loopItems arg is invalid') | ||
} | ||
assertOk(originalRowNumber != null, 'originalRowNumber arg is required') | ||
if (loopId == null) { | ||
return | ||
} | ||
const newData = Handlebars.createFrame(options.data) | ||
return getLoopItemById({ idName: 'id', idValue: loopId }, loopItems) | ||
} | ||
const getPreviousLoopItems = (loopId, evaluatedLoopsIds, loopItems) => { | ||
if (!Array.isArray(evaluatedLoopsIds)) { | ||
throw new Error('getPreviousLoopItems evaluatedLoopsIds arg is invalid') | ||
} | ||
if (!Array.isArray(loopItems)) { | ||
throw new Error('getPreviousLoopItems loopItems arg is invalid') | ||
} | ||
const lastEvaluatedLoopId = evaluatedLoopsIds[evaluatedLoopsIds.length - 1] | ||
const loopItemsToGet = loopId != null && loopId === lastEvaluatedLoopId ? evaluatedLoopsIds.slice(0, -1) : evaluatedLoopsIds | ||
const result = [] | ||
for (const lId of loopItemsToGet) { | ||
const loopItem = getLoopItemById({ idName: 'id', idValue: lId }, loopItems) | ||
if (loopItem == null) { | ||
throw new Error(`Can not find loop item by id "${lId}"`) | ||
} | ||
if (!loopItem.completed) { | ||
continue | ||
} | ||
result.push(loopItem) | ||
} | ||
return result | ||
} | ||
const getCurrentAndPreviousLoopItemsByTarget = (byTarget, loopItems) => { | ||
if (byTarget == null) { | ||
throw new Error('getCurrentAndPreviousLoopItemsByTarget byTarget arg is invalid') | ||
} | ||
if (byTarget.rowNumber == null) { | ||
throw new Error('getCurrentAndPreviousLoopItemsByTarget byTarget.rowNumber arg is required') | ||
} | ||
if (byTarget.columnNumber == null) { | ||
throw new Error('getCurrentAndPreviousLoopItemsByTarget byTarget.columnNumber arg is required') | ||
} | ||
if (!Array.isArray(loopItems)) { | ||
throw new Error('getCurrentAndPreviousLoopItemsByTarget loopItems arg is invalid') | ||
} | ||
const { rowNumber, columnNumber } = byTarget | ||
const matchedLoopItems = loopItems.filter((item) => { | ||
if (!item.completed) { | ||
throw new Error('getCurrentAndPreviousLoopItemsByTarget invalid usage, it should be called only after all loop items are completed evaluated') | ||
} | ||
return item.start <= rowNumber | ||
}) | ||
let current | ||
const previousAll = [...matchedLoopItems] | ||
const targetLoopItem = previousAll[previousAll.length - 1] | ||
const previous = previousAll.slice(0, -1) | ||
if (targetLoopItem != null) { | ||
let isInside = false | ||
const limit = targetLoopItem.type === 'block' ? targetLoopItem.end : targetLoopItem.start | ||
if (rowNumber === limit) { | ||
// for row loops we assume the row is inside when the row just matches the limit | ||
// (even if technically on the out of loop right case we should check columnEnd, | ||
// we don't do that because in that case the cell will anyway keep on its original place) | ||
isInside = targetLoopItem.type === 'block' ? targetLoopItem.columnEnd > columnNumber : true | ||
} else { | ||
isInside = limit > rowNumber | ||
} | ||
if (!isInside) { | ||
previous.push(targetLoopItem) | ||
} else { | ||
current = targetLoopItem | ||
} | ||
} | ||
return { | ||
current, | ||
previousAll, | ||
previous | ||
} | ||
} | ||
const getLoopItemTemplateLength = (loopItem) => { | ||
if (loopItem == null) { | ||
throw new Error('getLoopItemTemplateLength loopItem arg is invalid') | ||
} | ||
let templateLength = 1 | ||
if (loopItem.type === 'block') { | ||
templateLength = (loopItem.end - loopItem.start) + 1 | ||
} | ||
return templateLength | ||
} | ||
const getParentsLoopItems = (loopId, loopItems) => { | ||
if (loopId == null) { | ||
throw new Error('getParentsLoopItems loopId arg is invalid') | ||
} | ||
if (!Array.isArray(loopItems)) { | ||
throw new Error('getParentsLoopItems loopItems arg is invalid') | ||
} | ||
const results = [] | ||
const parentIdParts = loopId.split('#').slice(0, -1) | ||
if (parentIdParts.length === 0) { | ||
return results | ||
} | ||
let parentId = '' | ||
for (let index = 0; index < parentIdParts.length; index++) { | ||
parentId += parentId === '' ? parentIdParts[index] : `#${parentIdParts[index]}` | ||
const result = getLoopItemById({ idName: 'id', idValue: parentId }, loopItems) | ||
if (!result) { | ||
throw new Error(`Can not find loop item by id "${parentId}"`) | ||
} | ||
results.push(result) | ||
} | ||
return results | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'row' | ||
) { | ||
const originalRowNumber = optionsToUse.hash.originalRowNumber | ||
if (originalRowNumber == null) { | ||
throw new Error('xlsxSData type="row" helper originalRowNumber arg is required') | ||
} | ||
const newData = Handlebars.createFrame(optionsToUse.data) | ||
const currentLoopItem = getCurrentLoopItem(newData.currentLoopId, newData.loopItems) | ||
@@ -527,7 +407,5 @@ // this gets the previous loops (loops defined before a cell) and also on the case of nested loops | ||
if (currentLoopItem) { | ||
const loopIndex = optionsToUse.data.index | ||
const loopIndex = options.data.index | ||
if (loopIndex == null) { | ||
throw new Error('xlsxSData type="row" helper expected loop index to be defined') | ||
} | ||
assertOk(loopIndex != null, 'expected loop index to be defined') | ||
@@ -551,3 +429,4 @@ const parents = getParentsLoopItems(currentLoopItem.id, newData.loopItems) | ||
newData.rowNumber = originalRowNumber + increment | ||
newData.originalRowNumber = originalRowNumber | ||
newData.r = originalRowNumber + increment | ||
// only a value that represents the increment of previous loops defined before the cell | ||
@@ -559,5 +438,6 @@ newData.previousLoopIncrement = previousRootLoopIncrement | ||
newData.columnLetter = null | ||
newData.originalCellRef = null | ||
newData.currentCellRef = null | ||
const result = optionsToUse.fn(this, { data: newData }) | ||
const result = options.fn(this, { data: newData }) | ||
@@ -567,38 +447,25 @@ return result | ||
if ( | ||
arguments.length === 1 && | ||
type === 'rowNumber' | ||
) { | ||
return optionsToUse.data.rowNumber | ||
} | ||
function c (info, options) { | ||
const Handlebars = require('handlebars') | ||
const originalRowNumber = options.data.originalRowNumber | ||
const rowNumber = options.data.r | ||
const trackedCells = options.data.meta.trackedCells | ||
const isMultipleExpression = typeof options.fn === 'function' | ||
const { type, originalCellLetter, calcChainUpdate } = info | ||
if ( | ||
arguments.length === 1 && | ||
type === 'cellRef' | ||
) { | ||
const rowNumber = optionsToUse.data.rowNumber | ||
const trackedCells = optionsToUse.data.meta.trackedCells | ||
const originalCellRef = optionsToUse.hash.originalCellRef | ||
const isShadowCall = optionsToUse.hash.shadow === true | ||
const generateCellTag = type === 'autodetect' | ||
if (rowNumber == null) { | ||
throw new Error(`xlsxSData type="${type}" invalid usage, rowNumber needs to exists on internal data`) | ||
} | ||
assertOk(originalRowNumber != null, 'originalRowNumber needs to exists on internal data') | ||
assertOk(rowNumber != null, 'rowNumber needs to exists on internal data') | ||
assertOk(trackedCells != null, 'trackedCells needs to exists on internal data') | ||
if (trackedCells == null) { | ||
throw new Error(`xlsxSData type="${type}" invalid usage, trackedCells needs to exists on internal data`) | ||
} | ||
const { parseCellRef } = require('cellUtils') | ||
const originalCellRef = `${originalCellLetter}${originalRowNumber}` | ||
const updatedCellRef = `${originalCellLetter}${rowNumber}` | ||
if (originalCellRef == null) { | ||
throw new Error('xlsxSData type="cellRef" helper originalCellRef arg is required') | ||
} | ||
const parsedOriginalCellRef = parseCellRef(originalCellRef) | ||
const updatedCellRef = `${parsedOriginalCellRef.letter}${rowNumber}` | ||
// keeping the lastCellRef updated | ||
if (optionsToUse.data.meta.lastCellRef == null) { | ||
optionsToUse.data.meta.lastCellRef = updatedCellRef | ||
if (options.data.meta.lastCellRef == null) { | ||
options.data.meta.lastCellRef = updatedCellRef | ||
} else { | ||
const parsedLastCellRef = parseCellRef(optionsToUse.data.meta.lastCellRef) | ||
const parsedLastCellRef = parseCellRef(options.data.meta.lastCellRef) | ||
const parsedUpdatedCellRef = parseCellRef(updatedCellRef) | ||
@@ -611,3 +478,3 @@ | ||
) { | ||
optionsToUse.data.meta.lastCellRef = updatedCellRef | ||
options.data.meta.lastCellRef = updatedCellRef | ||
} | ||
@@ -619,6 +486,6 @@ } | ||
// if we are in loop then don't add item to updatedOriginalCells | ||
if (optionsToUse.data.currentLoopId != null) { | ||
if (options.data.currentLoopId != null) { | ||
shouldUpdateOriginalCell = false | ||
} else { | ||
shouldUpdateOriginalCell = originalCellRef !== updatedCellRef && optionsToUse.data.meta.updatedOriginalCells[originalCellRef] == null | ||
shouldUpdateOriginalCell = originalCellRef !== updatedCellRef && options.data.meta.updatedOriginalCells[originalCellRef] == null | ||
} | ||
@@ -628,171 +495,626 @@ | ||
// keeping a registry of the original cells that were updated | ||
optionsToUse.data.meta.updatedOriginalCells[originalCellRef] = updatedCellRef | ||
options.data.meta.updatedOriginalCells[originalCellRef] = updatedCellRef | ||
} | ||
if (!isShadowCall) { | ||
trackedCells[originalCellRef] = trackedCells[originalCellRef] || { first: null, last: null, count: 0 } | ||
trackedCells[originalCellRef] = trackedCells[originalCellRef] || { first: null, last: null, count: 0 } | ||
if (trackedCells[originalCellRef].inLoop == null) { | ||
trackedCells[originalCellRef].inLoop = optionsToUse.data.currentLoopId != null | ||
if (trackedCells[originalCellRef].inLoop == null) { | ||
trackedCells[originalCellRef].inLoop = options.data.currentLoopId != null | ||
} | ||
if (trackedCells[originalCellRef].first == null) { | ||
trackedCells[originalCellRef].first = updatedCellRef | ||
} | ||
trackedCells[originalCellRef].last = updatedCellRef | ||
trackedCells[originalCellRef].count += 1 | ||
if (calcChainUpdate) { | ||
const sheetId = options.data.sheetId | ||
const cellRefKey = `${sheetId}-${originalCellRef}` | ||
let calcChainUpdatesForCellRef = options.data.calcChainUpdatesMap.get(cellRefKey) | ||
if (calcChainUpdatesForCellRef == null) { | ||
calcChainUpdatesForCellRef = [] | ||
options.data.calcChainUpdatesMap.set(cellRefKey, calcChainUpdatesForCellRef) | ||
} | ||
if (trackedCells[originalCellRef].first == null) { | ||
trackedCells[originalCellRef].first = updatedCellRef | ||
calcChainUpdatesForCellRef.push(updatedCellRef) | ||
} | ||
options.data.columnLetter = originalCellLetter | ||
options.data.originalCellRef = originalCellRef | ||
options.data.currentCellRef = updatedCellRef | ||
if (!generateCellTag) { | ||
return updatedCellRef | ||
} | ||
let cellValue = isMultipleExpression ? options.fn(this, { data: options.data }) : info.value | ||
if (!isMultipleExpression && info.escape && typeof cellValue === 'string') { | ||
cellValue = Handlebars.escapeExpression(cellValue) | ||
} | ||
const enabledForCol = options.data.meta.autofit.enabledFor[0] === true ? true : options.data.meta.autofit.enabledFor.includes(originalCellLetter) | ||
if (enabledForCol) { | ||
const { getPixelWidthOfValue, getFontSizeFromStyle } = require('cellUtils') | ||
const fontSize = getFontSizeFromStyle(options.hash.s, options.data.styleInfo, options.data.styleFontSizeMap) | ||
const colInfo = options.data.meta.autofit.cols[originalCellLetter] | ||
const size = getPixelWidthOfValue(cellValue, fontSize) | ||
if (colInfo == null) { | ||
options.data.meta.autofit.cols[originalCellLetter] = { | ||
size | ||
} | ||
} else if (size > colInfo.size) { | ||
options.data.meta.autofit.cols[originalCellLetter] = { | ||
size | ||
} | ||
} | ||
} | ||
trackedCells[originalCellRef].last = updatedCellRef | ||
trackedCells[originalCellRef].count += 1 | ||
let cellType | ||
const serializedAttrs = [] | ||
const attrsKeys = Object.keys(options.hash) | ||
if (!attrsKeys.includes('t')) { | ||
attrsKeys.push('t') | ||
} | ||
optionsToUse.data.columnLetter = parsedOriginalCellRef.letter | ||
optionsToUse.data.currentCellRef = updatedCellRef | ||
for (let idx = 0; idx < attrsKeys.length; idx++) { | ||
const key = attrsKeys[idx] | ||
let value | ||
return updatedCellRef | ||
if (key === 'r') { | ||
// ensure updated cellRef is part of cell xml | ||
value = updatedCellRef | ||
} else if (key === 't') { | ||
// ensure we put the type according to cell content | ||
if (cellValue == null) { | ||
cellType = 'inlineStr' | ||
} else if ( | ||
typeof cellValue === 'boolean' || | ||
( | ||
cellValue != null && | ||
typeof cellValue === 'object' && | ||
Object.prototype.toString.call(cellValue) === '[object Boolean]' | ||
) | ||
) { | ||
cellType = 'b' | ||
} else if ( | ||
typeof cellValue === 'number' || | ||
( | ||
cellValue != null && | ||
typeof cellValue === 'object' && | ||
Object.prototype.toString.call(cellValue) === '[object Number]' | ||
) | ||
) { | ||
cellType = 'n' | ||
} else { | ||
cellType = 'inlineStr' | ||
} | ||
value = cellType | ||
} else { | ||
// keep rest of attrs | ||
value = options.hash[key] | ||
} | ||
serializedAttrs.push(`${key}="${value}"`) | ||
} | ||
let cellContent | ||
if (cellType === 'inlineStr') { | ||
cellContent = `<is><t>${cellValue == null ? '' : cellValue}</t></is>` | ||
} else if (cellType === 'b') { | ||
cellContent = `<v>${cellValue ? '1' : '0'}</v>` | ||
} else if (cellType === 'n') { | ||
cellContent = `<v>${cellValue}</v>` | ||
} | ||
assertOk(cellContent != null, `cell type "${cellType}" not supported`) | ||
return new Handlebars.SafeString(`<c ${serializedAttrs.join(' ')}>${cellContent}</c>`) | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'cellValue' | ||
) { | ||
const newData = Handlebars.createFrame(optionsToUse.data) | ||
function mergeOrFormulaCell (type, options) { | ||
const Handlebars = require('handlebars') | ||
const rowNumber = options.data.r | ||
newData.currentCellValueInfo = {} | ||
assertOk(rowNumber != null, 'rowNumber needs to exists on internal data') | ||
if (Object.prototype.hasOwnProperty.call(optionsToUse.hash, 'value')) { | ||
newData.currentCellValueInfo.value = optionsToUse.hash.value | ||
let output = '' | ||
let toEscape = false | ||
if (type === 'mergeCell') { | ||
const originalCellRefRange = options.hash.o | ||
// escape should be there when the original handlebars expression was intended | ||
// to be escaped, we preserve that intend here and escape it, we need to do this | ||
// because handlebars does not escape automatically the helper parameter hash, | ||
// which we use as an implementation detail of our auto detect cell type logic | ||
if (Object.prototype.hasOwnProperty.call(optionsToUse.hash, 'escape')) { | ||
toEscape = optionsToUse.hash.escape === true && typeof newData.currentCellValueInfo.value === 'string' | ||
assertOk(originalCellRefRange != null, 'originalCellRefRange arg is required') | ||
const { evaluateCellRefsFromExpression, generateNewCellRefFromRow } = require('cellUtils') | ||
const { newValue } = evaluateCellRefsFromExpression(originalCellRefRange, (cellRefInfo) => { | ||
const isRange = cellRefInfo.type === 'rangeStart' || cellRefInfo.type === 'rangeEnd' | ||
assertOk(isRange, `cell ref expected to be a range. value: "${originalCellRefRange}`) | ||
const increment = cellRefInfo.type === 'rangeEnd' ? cellRefInfo.parsedRangeEnd.rowNumber - cellRefInfo.parsedRangeStart.rowNumber : 0 | ||
const newCellRef = generateNewCellRefFromRow(cellRefInfo.parsed, rowNumber + increment) | ||
return newCellRef | ||
}) | ||
const mergeCell = { | ||
original: originalCellRefRange, | ||
value: newValue | ||
} | ||
if (toEscape) { | ||
newData.currentCellValueInfo.value = Handlebars.escapeExpression(newData.currentCellValueInfo.value) | ||
options.data.meta.mergeCells.push(mergeCell) | ||
} else { | ||
const { parseCellRef, getNewFormula, decodeXML, encodeXML } = require('cellUtils') | ||
const currentCellRef = options.data.currentCellRef | ||
const trackedCells = options.data.meta.trackedCells | ||
const lazyFormulas = options.data.meta.lazyFormulas | ||
const originalCellRef = options.data.originalCellRef | ||
// formulas can contain characters that are encoded as part of the xlsx processing, | ||
// we ensure here that we decode the xml entities | ||
const originalFormula = options.hash.o != null ? decodeXML(options.hash.o) : null | ||
const previousLoopIncrement = options.data.previousLoopIncrement | ||
const currentLoopIncrement = options.data.currentLoopIncrement | ||
assertOk(currentCellRef != null, 'currentCellRef needs to exists on internal data') | ||
assertOk(trackedCells != null, 'trackedCells needs to exists on internal data') | ||
assertOk(lazyFormulas != null, 'lazyFormulas needs to exists on internal data') | ||
assertOk(originalCellRef != null, 'originalCellRef needs to exists on internal data') | ||
assertOk(originalFormula != null, 'originalFormula arg is required') | ||
assertOk(currentLoopIncrement != null, 'currentLoopIncrement needs to exists on internal data') | ||
const parsedOriginCellRef = parseCellRef(originalCellRef) | ||
const originCellIsFromLoop = options.data.currentLoopId != null | ||
const { lazyCellRefs = {}, formula: newFormula } = getNewFormula(originalFormula, parsedOriginCellRef, { | ||
type: 'normal', | ||
originCellIsFromLoop, | ||
previousLoopIncrement, | ||
currentLoopIncrement, | ||
trackedCells, | ||
includeLoopIncrementResolver: (cellRefIsFromLoop, cellRefInfo) => { | ||
return ( | ||
cellRefIsFromLoop && | ||
trackedCells[cellRefInfo.localRef] != null && | ||
trackedCells[cellRefInfo.localRef].loopHierarchyId === getCurrentLoopItem(options.data.currentLoopId, options.data.loopItems)?.hierarchyId | ||
) | ||
}, | ||
lazyFormulas, | ||
currentCellRef | ||
}) | ||
// ensure we encode just some basic xml entities, formula values does not need to | ||
// have the full xml entities escaped | ||
if (Object.keys(lazyCellRefs).length > 0) { | ||
return options.data.tasks.wait('lazyFormulas').then(() => { | ||
const finalFormula = lazyFormulas.data[newFormula].newFormula | ||
return encodeXML(finalFormula, 'basic') | ||
}) | ||
} | ||
output = encodeXML(newFormula, 'basic') | ||
} | ||
const result = optionsToUse.fn(this, { data: newData }) | ||
const enabledForCol = newData.meta.autofit.enabledFor[0] === true ? true : newData.meta.autofit.enabledFor.includes(newData.columnLetter) | ||
return new Handlebars.SafeString(output) | ||
} | ||
if (enabledForCol) { | ||
const pixelWidth = require('string-pixel-width') | ||
const fontSize = optionsToUse.hash.fontSize | ||
const fontSizeInPx = fontSize * (96 / 72) | ||
const currentValue = newData.currentCellValueInfo.value | ||
const maxInfo = newData.meta.autofit.cols[newData.columnLetter] | ||
function mergeCells (options) { | ||
const Handlebars = require('handlebars') | ||
const newData = Handlebars.createFrame(options.data) | ||
const size = pixelWidth(currentValue, { font: 'Arial', size: fontSizeInPx }) | ||
const [resolveTask, rejectTask] = options.data.tasks.add('mergeCells') | ||
if (maxInfo == null) { | ||
newData.meta.autofit.cols[newData.columnLetter] = { | ||
value: currentValue, | ||
size | ||
newData.mergeCellsCount = 0 | ||
newData.mergeCellsTemplatesMap = new Map() | ||
try { | ||
const result = options.fn(this, { data: newData }) | ||
resolveTask() | ||
return result | ||
} catch (e) { | ||
rejectTask(e) | ||
throw e | ||
} | ||
} | ||
async function mergeCellsCount (options) { | ||
await options.data.tasks.wait('mergeCells') | ||
return options.data.mergeCellsCount | ||
} | ||
function mergeCellsItems (options) { | ||
const Handlebars = require('handlebars') | ||
const targetItems = options.data.meta.mergeCells | ||
// run the body to fulfill the merge cells templates | ||
options.fn(this) | ||
const mergeCellsTemplatesMap = options.data.mergeCellsTemplatesMap | ||
let original = [] | ||
const generated = [] | ||
const originalOrderScore = new Map( | ||
Array.from(mergeCellsTemplatesMap.keys()).map((key, idx) => [key, idx]) | ||
) | ||
for (const targetItem of targetItems) { | ||
const template = mergeCellsTemplatesMap.get(targetItem.original) | ||
const output = template({ newRef: targetItem.value }) | ||
if (targetItem.original === targetItem.value) { | ||
original.push({ score: originalOrderScore.get(targetItem.original), output }) | ||
} else { | ||
generated.push(output) | ||
} | ||
} | ||
// preserve the original order from xlsx file, so we avoid noise in diffs when | ||
// comparing xlsx output | ||
original.sort((a, b) => a.score - b.score) | ||
original = original.map((item) => item.output) | ||
options.data.mergeCellsCount = original.length + generated.length | ||
const output = `${original.join('\n')}${generated.join('\n')}` | ||
return new Handlebars.SafeString(output) | ||
} | ||
function mI (options) { | ||
const originalCellRefRange = options.hash.o | ||
assertOk(originalCellRefRange != null, 'originalCellRefRange arg is required') | ||
options.data.mergeCellsTemplatesMap.set(originalCellRefRange, options.fn) | ||
return '' | ||
} | ||
function formulaShared (options) { | ||
const rowNumber = options.data.r | ||
assertOk(rowNumber != null, 'rowNumber needs to exists on internal data') | ||
const originalSharedRefRange = options.hash.o | ||
assertOk(originalSharedRefRange != null, 'originalSharedRefRange arg is required') | ||
const { evaluateCellRefsFromExpression, generateNewCellRefFromRow } = require('cellUtils') | ||
const { newValue } = evaluateCellRefsFromExpression(originalSharedRefRange, (cellRefInfo) => { | ||
const increment = cellRefInfo.type === 'rangeEnd' ? cellRefInfo.parsedRangeEnd.rowNumber - cellRefInfo.parsedRangeStart.rowNumber : 0 | ||
const newCellRef = generateNewCellRefFromRow(cellRefInfo.parsed, rowNumber + increment) | ||
return newCellRef | ||
}) | ||
return newValue | ||
} | ||
// TODO: this should be refactored at some point to be more generic | ||
// and support nested loops, maybe the logic will be similar to mergeCell or formula helpers | ||
// when this is done we can remove all the methods that are only used here "getNewCellRef", "getCurrentAndPreviousLoopItemsByTarget" | ||
function newCellRef (options) { | ||
const Handlebars = require('handlebars') | ||
const updatedOriginalCells = options.data.meta.updatedOriginalCells | ||
const loopItems = options.data.loopItems | ||
let targetItems = [] | ||
const updated = [] | ||
const type = 'newCellRef' | ||
if (type === 'newCellRef') { | ||
targetItems = [{ value: options.hash.originalCellRefRange }] | ||
} | ||
for (const targetItem of targetItems) { | ||
const regexp = /(\$?[A-Z]+\$?\d+:)?(\$?[A-Z]+\$?\d+)/g | ||
const newValue = targetItem.value.replace(regexp, (...args) => { | ||
const [match, _startingCellRef, endingCellRef] = args | ||
const isRange = _startingCellRef != null | ||
let newCellRef | ||
const ctx = { | ||
updatedOriginalCells, | ||
loopItems | ||
} | ||
} else if (size > maxInfo.size) { | ||
newData.meta.autofit.cols[newData.columnLetter] = { | ||
value: currentValue, | ||
size | ||
if (isRange) { | ||
const startingCellRef = _startingCellRef.slice(0, -1) | ||
const newStartingCellRef = getNewCellRef(type === 'formulas' ? [targetItem.cellRef, startingCellRef] : startingCellRef, targetItem.loopMeta, 'rangeStart', ctx) | ||
const newEndingCellRef = getNewCellRef(type === 'formulas' ? [targetItem.cellRef, endingCellRef] : endingCellRef, targetItem.loopMeta, 'rangeEnd', ctx) | ||
return `${newStartingCellRef}:${newEndingCellRef}` | ||
} else { | ||
newCellRef = getNewCellRef(type === 'formulas' ? [targetItem.cellRef, endingCellRef] : endingCellRef, targetItem.loopMeta, 'standalone', ctx) | ||
} | ||
return newCellRef | ||
}) | ||
if (type === 'newCellRef') { | ||
updated.push(newValue) | ||
} | ||
} | ||
return result | ||
return new Handlebars.SafeString(updated.join('\n')) | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'cellValueRaw' | ||
) { | ||
const newData = Handlebars.createFrame(optionsToUse.data) | ||
const result = optionsToUse.fn(this, { data: newData }) | ||
async function autofit (options) { | ||
const Handlebars = require('handlebars') | ||
const processAutofitCols = require('xlsxProcessAutofitCols') | ||
if ( | ||
optionsToUse?.data?.currentCellValueInfo != null && | ||
!Object.prototype.hasOwnProperty.call(optionsToUse.data.currentCellValueInfo, 'value') | ||
) { | ||
optionsToUse.data.currentCellValueInfo.value = result | ||
} | ||
const existingColsXml = options.fn(this) | ||
return '' | ||
await options.data.tasks.wait('sd') | ||
return new Handlebars.SafeString(processAutofitCols(options.data.meta.autofit, existingColsXml)) | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'cellValueType' | ||
) { | ||
const cellValue = optionsToUse.data.currentCellValueInfo.value | ||
let cellType | ||
function lazyFormulas (options) { | ||
const [resolveTask, rejectTask] = options.data.tasks.add('lazyFormulas') | ||
if (cellValue == null) { | ||
cellType = 'inlineStr' | ||
} else if ( | ||
typeof cellValue === 'boolean' || | ||
( | ||
cellValue != null && | ||
typeof cellValue === 'object' && | ||
Object.prototype.toString.call(cellValue) === '[object Boolean]' | ||
) | ||
) { | ||
cellType = 'b' | ||
} else if ( | ||
typeof cellValue === 'number' || | ||
( | ||
cellValue != null && | ||
typeof cellValue === 'object' && | ||
Object.prototype.toString.call(cellValue) === '[object Number]' | ||
) | ||
) { | ||
cellType = 'n' | ||
} else { | ||
cellType = 'inlineStr' | ||
try { | ||
const trackedCells = options.data.meta.trackedCells | ||
const lazyFormulas = options.data.meta.lazyFormulas | ||
assertOk(trackedCells != null, 'trackedCells needs to exists on internal data') | ||
assertOk(lazyFormulas != null, 'lazyFormulas needs to exists on internal data') | ||
if (lazyFormulas.count == null || lazyFormulas.count === 0) { | ||
resolveTask() | ||
return '' | ||
} | ||
const lazyFormulaIds = Object.keys(lazyFormulas.data) | ||
const { getNewFormula } = require('cellUtils') | ||
for (const lazyFormulaId of lazyFormulaIds) { | ||
const lazyFormulaInfo = lazyFormulas.data[lazyFormulaId] | ||
const { | ||
formula, | ||
parsedOriginCellRef, | ||
originCellIsFromLoop, | ||
previousLoopIncrement, | ||
currentLoopIncrement, | ||
cellRefs | ||
} = lazyFormulaInfo | ||
const { formula: newFormula } = getNewFormula(formula, parsedOriginCellRef, { | ||
type: 'lazy', | ||
originCellIsFromLoop, | ||
previousLoopIncrement, | ||
currentLoopIncrement, | ||
trackedCells, | ||
lazyCellRefs: cellRefs | ||
}) | ||
// ensure we encode just some basic xml entities, formula values does not need to | ||
// have the full xml entities escaped | ||
lazyFormulaInfo.newFormula = newFormula | ||
} | ||
resolveTask() | ||
return '' | ||
} catch (e) { | ||
rejectTask(e) | ||
throw e | ||
} | ||
} | ||
optionsToUse.data.currentCellValueInfo.type = cellType | ||
function style (options) { | ||
const Handlebars = require('handlebars') | ||
const { parseStyle, getStyleInfo } = require('xlsxProcessStyle') | ||
return cellType | ||
const styleXml = options.fn(this) | ||
const styleDoc = parseStyle(styleXml) | ||
const styleInfo = getStyleInfo(styleDoc) | ||
if (styleInfo != null) { | ||
options.data.styleInfo = styleInfo | ||
} | ||
return new Handlebars.SafeString(styleXml) | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'cellContent' | ||
) { | ||
const cellType = optionsToUse.data.currentCellValueInfo.type | ||
const cellValue = optionsToUse.data.currentCellValueInfo.value | ||
let result | ||
function calcChain (options) { | ||
const Handlebars = require('handlebars') | ||
const processCalcChain = require('xlsxProcessCalcChain') | ||
if (cellType === 'inlineStr') { | ||
result = `<is><t>${cellValue == null ? '' : cellValue}</t></is>` | ||
} else if (cellType === 'b') { | ||
result = `<v>${cellValue ? '1' : '0'}</v>` | ||
} else if (cellType === 'n') { | ||
result = `<v>${cellValue}</v>` | ||
const existingCalcChainXml = options.fn(this) | ||
return new Handlebars.SafeString(processCalcChain(options.data.calcChainUpdatesMap, existingCalcChainXml)) | ||
} | ||
function raw (options) { | ||
return options.fn() | ||
} | ||
function getLoopItemById (byTarget, loopItems) { | ||
assertOk(byTarget != null, 'getLoopItemById byTarget arg is required') | ||
assertOk(Array.isArray(loopItems), 'getLoopItemById loopItems arg is invalid') | ||
const { idName, idValue } = byTarget | ||
assertOk(idName != null, 'getLoopItemById byTarget.idName arg is required') | ||
assertOk(typeof idName === 'string', 'getLoopItemById byTarget.idName arg is invalid') | ||
assertOk(idName === 'hierarchyId' || idName === 'id', 'getLoopItemById byTarget.idName should be either "hierarchyId" or "id"') | ||
assertOk(idValue != null, 'getLoopItemById byTarget.idValue arg is required') | ||
assertOk(typeof idValue === 'string', 'getLoopItemById byTarget.idValue arg is invalid') | ||
const idParts = idValue.split('#') | ||
let ctx = { children: loopItems } | ||
let targetIdValue = '' | ||
let parent | ||
while (idParts.length > 0) { | ||
const idx = idParts.shift() | ||
targetIdValue = targetIdValue !== '' ? `${targetIdValue}#${idx}` : `${idx}` | ||
const matches = ctx.children.filter((c) => c[idName] === targetIdValue) | ||
const result = matches[matches.length - 1] | ||
if (result == null) { | ||
break | ||
} | ||
ctx = result | ||
if (idParts.length === 0) { | ||
parent = ctx | ||
} | ||
} | ||
if (result == null) { | ||
throw new Error(`xlsxSData type="cellContent" helper does not support cell type "${cellType}"`) | ||
return parent | ||
} | ||
function getParentLoopItemByHierarchy (childLoopItem, loopItems) { | ||
assertOk(childLoopItem != null, 'getParentLoopItemByHierarchy childLoopItem arg is required') | ||
assertOk(Array.isArray(loopItems), 'getParentLoopItemByHierarchy loopItems arg is invalid') | ||
const parentHierarchyId = childLoopItem.hierarchyId.split('#').slice(0, -1).join('#') | ||
if (parentHierarchyId === '') { | ||
return | ||
} | ||
return new Handlebars.SafeString(result) | ||
return getLoopItemById({ idName: 'hierarchyId', idValue: parentHierarchyId }, loopItems) | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'autofit' | ||
) { | ||
function getCurrentLoopItem (loopId, loopItems) { | ||
assertOk(Array.isArray(loopItems), 'getCurrentLoopItem loopItems arg is invalid') | ||
if (loopId == null) { | ||
return | ||
} | ||
return getLoopItemById({ idName: 'id', idValue: loopId }, loopItems) | ||
} | ||
function getPreviousLoopItems (loopId, evaluatedLoopsIds, loopItems) { | ||
assertOk(Array.isArray(evaluatedLoopsIds), 'getPreviousLoopItems evaluatedLoopsIds arg is invalid') | ||
assertOk(Array.isArray(loopItems), 'getPreviousLoopItems loopItems arg is invalid') | ||
const lastEvaluatedLoopId = evaluatedLoopsIds[evaluatedLoopsIds.length - 1] | ||
const loopItemsToGet = loopId != null && loopId === lastEvaluatedLoopId ? evaluatedLoopsIds.slice(0, -1) : evaluatedLoopsIds | ||
const result = [] | ||
const autofitInfo = optionsToUse.data.meta.autofit | ||
for (const [colLetter, colInfo] of Object.entries(autofitInfo.cols)) { | ||
result.push(`<col ref="${colLetter}" size="${colInfo.size}" />`) | ||
for (const lId of loopItemsToGet) { | ||
const loopItem = getLoopItemById({ idName: 'id', idValue: lId }, loopItems) | ||
assertOk(loopItem != null, `Can not find loop item by id "${lId}"`) | ||
if (!loopItem.completed) { | ||
continue | ||
} | ||
result.push(loopItem) | ||
} | ||
return new Handlebars.SafeString(result.join('\n')) | ||
return result | ||
} | ||
const getNewCellRef = (cellRefInput, originLoopMeta, mode = 'standalone', context) => { | ||
function getCurrentAndPreviousLoopItemsByTarget (byTarget, loopItems) { | ||
assertOk(byTarget != null, 'getCurrentAndPreviousLoopItemsByTarget byTarget arg is invalid') | ||
assertOk(byTarget.rowNumber != null, 'getCurrentAndPreviousLoopItemsByTarget byTarget.rowNumber arg is required') | ||
assertOk(byTarget.columnNumber != null, 'getCurrentAndPreviousLoopItemsByTarget byTarget.columnNumber arg is required') | ||
assertOk(Array.isArray(loopItems), 'getCurrentAndPreviousLoopItemsByTarget loopItems arg is invalid') | ||
const { rowNumber, columnNumber } = byTarget | ||
const matchedLoopItems = loopItems.filter((item) => { | ||
assertOk(item.completed, 'getCurrentAndPreviousLoopItemsByTarget invalid usage, it should be called only after all loop items are completed evaluated') | ||
return item.start <= rowNumber | ||
}) | ||
let current | ||
const previousAll = [...matchedLoopItems] | ||
const targetLoopItem = previousAll[previousAll.length - 1] | ||
const previous = previousAll.slice(0, -1) | ||
if (targetLoopItem != null) { | ||
let isInside = false | ||
const limit = targetLoopItem.type === 'block' ? targetLoopItem.end : targetLoopItem.start | ||
if (rowNumber === limit) { | ||
// for row loops we assume the row is inside when the row just matches the limit | ||
// (even if technically on the out of loop right case we should check columnEnd, | ||
// we don't do that because in that case the cell will anyway keep on its original place) | ||
isInside = targetLoopItem.type === 'block' ? targetLoopItem.columnEnd > columnNumber : true | ||
} else { | ||
isInside = limit > rowNumber | ||
} | ||
if (!isInside) { | ||
previous.push(targetLoopItem) | ||
} else { | ||
current = targetLoopItem | ||
} | ||
} | ||
return { | ||
current, | ||
previousAll, | ||
previous | ||
} | ||
} | ||
function getLoopItemTemplateLength (loopItem) { | ||
assertOk(loopItem != null, 'getLoopItemTemplateLength loopItem arg is invalid') | ||
let templateLength = 1 | ||
if (loopItem.type === 'block') { | ||
templateLength = (loopItem.end - loopItem.start) + 1 | ||
} | ||
return templateLength | ||
} | ||
function getParentsLoopItems (loopId, loopItems) { | ||
assertOk(loopId != null, 'getParentsLoopItems loopId arg is invalid') | ||
assertOk(Array.isArray(loopItems), 'getParentsLoopItems loopItems arg is invalid') | ||
const results = [] | ||
const parentIdParts = loopId.split('#').slice(0, -1) | ||
if (parentIdParts.length === 0) { | ||
return results | ||
} | ||
let parentId = '' | ||
for (let index = 0; index < parentIdParts.length; index++) { | ||
parentId += parentId === '' ? parentIdParts[index] : `#${parentIdParts[index]}` | ||
const result = getLoopItemById({ idName: 'id', idValue: parentId }, loopItems) | ||
assertOk(result != null, `Can not find loop item by id "${parentId}"`) | ||
results.push(result) | ||
} | ||
return results | ||
} | ||
function getNewCellRef (cellRefInput, originLoopMeta, mode = 'standalone', context) { | ||
const type = 'newCellRef' | ||
const { updatedOriginalCells, loopItems } = context | ||
@@ -809,2 +1131,3 @@ let cellRef | ||
const { parseCellRef } = require('cellUtils') | ||
const parsedCellRef = parseCellRef(cellRef) | ||
@@ -820,5 +1143,3 @@ const parsedOriginCellRef = originCellRef != null ? parseCellRef(originCellRef) : undefined | ||
const { | ||
current: currentLoopItemForTarget, | ||
previousAll: previousAllLoopItemsForTarget, | ||
previous: previousLoopItemsForTarget | ||
current: currentLoopItemForTarget, previousAll: previousAllLoopItemsForTarget, previous: previousLoopItemsForTarget | ||
} = getCurrentAndPreviousLoopItemsByTarget({ | ||
@@ -864,12 +1185,10 @@ rowNumber: parsedCellRef.rowNumber, | ||
if ( | ||
currentLoopItemForTarget != null && | ||
if (currentLoopItemForTarget != null && | ||
( | ||
(type === 'newCellRef' && mode === 'rangeEnd') || | ||
(type === 'formulas' && | ||
originCellRef != null && | ||
!originIsLoopItem && | ||
mode === 'rangeEnd') | ||
) | ||
) { | ||
originCellRef != null && | ||
!originIsLoopItem && | ||
mode === 'rangeEnd') | ||
)) { | ||
includeAll = true | ||
@@ -903,297 +1222,179 @@ } | ||
if ( | ||
arguments.length === 1 && | ||
(type === 'mergeCell' || type === 'formula') | ||
) { | ||
const rowNumber = optionsToUse.data.rowNumber | ||
if (rowNumber == null) { | ||
throw new Error(`xlsxSData type="${type}" invalid usage, rowNumber needs to exists on internal data`) | ||
function assertOk (valid, message) { | ||
if (!valid) { | ||
throw new Error(message) | ||
} | ||
} | ||
let output = '' | ||
const helpers = { | ||
ws, | ||
sd, | ||
dimension, | ||
loop, | ||
outOfLoop, | ||
outOfLoopPlaceholder, | ||
r, | ||
c: function (info, options) { | ||
delete options.hash.t | ||
if (type === 'mergeCell') { | ||
const originalCellRefRange = optionsToUse.hash.originalCellRefRange | ||
if (originalCellRefRange == null) { | ||
throw new Error(`xlsxSData type="${type}" helper originalCellRefRange arg is required`) | ||
// restoring original t attribute, needed when we render xml of cell | ||
if (options.hash.__originalT__ != null) { | ||
options.hash.t = options.hash.__originalT__ | ||
delete options.hash.__originalT__ | ||
} | ||
const { newValue } = evaluateCellRefsFromExpression(originalCellRefRange, (cellRefInfo) => { | ||
const isRange = cellRefInfo.type === 'rangeStart' || cellRefInfo.type === 'rangeEnd' | ||
return c.call(this, info, options) | ||
}, | ||
m: function (options) { | ||
return mergeOrFormulaCell.call(this, 'mergeCell', options) | ||
}, | ||
f: function (options) { | ||
return mergeOrFormulaCell.call(this, 'formula', options) | ||
}, | ||
fs: formulaShared, | ||
mergeCells, | ||
mergeCellsCount, | ||
mergeCellsItems, | ||
mI, | ||
newCellRef, | ||
autofit, | ||
lazyFormulas, | ||
style, | ||
calcChain, | ||
raw | ||
} | ||
if (!isRange) { | ||
throw new Error(`xlsxSData type="mergeCell" helper only support range for cell refs. value: "${originalCellRefRange}"`) | ||
} | ||
return { | ||
resolveHelper: (helperName, argumentsLength, context, values, options) => { | ||
const targetHelper = helpers[helperName] | ||
const validCall = targetHelper != null ? argumentsLength === targetHelper.length : false | ||
const increment = cellRefInfo.type === 'rangeEnd' ? cellRefInfo.parsedRangeEnd.rowNumber - cellRefInfo.parsedRangeStart.rowNumber : 0 | ||
const newCellRef = generateNewCellRefFromRow(cellRefInfo.parsed, rowNumber + increment) | ||
return newCellRef | ||
}) | ||
const mergeCell = { | ||
original: originalCellRefRange, | ||
value: newValue | ||
if (!validCall) { | ||
throw new Error(`Invalid usage of _D helper${helperName != null ? ` (t: ${helperName})` : ''}`) | ||
} | ||
optionsToUse.data.meta.mergeCells.push(mergeCell) | ||
} else { | ||
const currentCellRef = optionsToUse.data.currentCellRef | ||
const trackedCells = optionsToUse.data.meta.trackedCells | ||
const lazyFormulas = optionsToUse.data.meta.lazyFormulas | ||
const originalCellRef = optionsToUse.hash.originalCellRef | ||
const originalFormula = optionsToUse.hash.originalFormula | ||
const previousLoopIncrement = optionsToUse.data.previousLoopIncrement | ||
const currentLoopIncrement = optionsToUse.data.currentLoopIncrement | ||
try { | ||
if (values.length > 0) { | ||
return targetHelper.call(context, ...values, options) | ||
} | ||
if (currentCellRef == null) { | ||
throw new Error(`xlsxSData type="${type}" invalid usage, currentCellRef needs to exists on internal data`) | ||
return targetHelper.call(context, options) | ||
} catch (error) { | ||
error.message = `_D t="${helperName}" helper, ${error.message}` | ||
throw error | ||
} | ||
}, | ||
assertDataArg: assertOk | ||
} | ||
})() | ||
if (trackedCells == null) { | ||
throw new Error(`xlsxSData type="${type}" invalid usage, trackedCells needs to exists on internal data`) | ||
} | ||
function _D () { | ||
const values = [] | ||
const argsLength = arguments.length | ||
let optionsToUse | ||
if (lazyFormulas == null) { | ||
throw new Error(`xlsxSData type="${type}" invalid usage, lazyFormulas needs to exists on internal data`) | ||
} | ||
if (argsLength > 1) { | ||
optionsToUse = arguments[argsLength - 1] | ||
if (originalCellRef == null) { | ||
throw new Error('xlsxSData type="formula" helper originalCellRef arg is required') | ||
} | ||
if (originalFormula == null) { | ||
throw new Error('xlsxSData type="formula" helper originalFormula arg is required') | ||
} | ||
if (currentLoopIncrement == null) { | ||
throw new Error(`xlsxSData type="${type}" invalid usage, currentLoopIncrement needs to exists on internal data`) | ||
} | ||
const parsedOriginCellRef = parseCellRef(originalCellRef) | ||
const originCellIsFromLoop = optionsToUse.data.currentLoopId != null | ||
const { formula: newFormula } = getNewFormula(type, originalFormula, parsedOriginCellRef, { | ||
type: 'normal', | ||
originCellIsFromLoop, | ||
previousLoopIncrement, | ||
currentLoopIncrement, | ||
trackedCells, | ||
includeLoopIncrementResolver: (cellRefIsFromLoop, cellRefInfo) => { | ||
return ( | ||
cellRefIsFromLoop && | ||
trackedCells[cellRefInfo.localRef] != null && | ||
trackedCells[cellRefInfo.localRef].loopHierarchyId === getCurrentLoopItem(optionsToUse.data.currentLoopId, optionsToUse.data.loopItems)?.hierarchyId | ||
) | ||
}, | ||
lazyFormulas, | ||
currentCellRef | ||
}) | ||
output = newFormula | ||
for (let idx = 0; idx < argsLength - 1; idx++) { | ||
values.push(arguments[idx]) | ||
} | ||
return output | ||
} else { | ||
optionsToUse = arguments[0] | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'mergeCells' | ||
) { | ||
const targetItems = optionsToUse.data.meta.mergeCells | ||
const newData = Handlebars.createFrame(optionsToUse.data) | ||
const type = optionsToUse.hash.t | ||
newData.mergeCellsCount = targetItems.length | ||
newData.mergeCellsTemplates = Object.create(null) | ||
__xlsxD.assertDataArg(type != null, '_D helper t arg is required') | ||
return optionsToUse.fn(this, { data: newData }) | ||
} | ||
return __xlsxD.resolveHelper(type, arguments.length, this, values, optionsToUse) | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'mergeCellsItems' | ||
) { | ||
const targetItems = optionsToUse.data.meta.mergeCells | ||
// alias for {{_D t='r'}} helper call, we do it this way to optimize size of the generated xml | ||
function _R (data, options) { | ||
options.hash.t = 'r' | ||
options.hash.o = data | ||
return _D.call(this, options) | ||
} | ||
// run the body to fulfill the merge cells templates | ||
optionsToUse.fn(this) | ||
// alias for {{_D t='c'}} helper call, we do it this way to optimize size of the generated xml | ||
function _C (data, options) { | ||
options.hash.t = 'c' | ||
return _D.call(this, { type: 'normal', originalCellLetter: data, calcChainUpdate: false }, options) | ||
} | ||
const mergeCellsTemplates = optionsToUse.data.mergeCellsTemplates | ||
// alias for {{_D t='c'}} helper call with calcChainUpdate: true | ||
function _c (data, options) { | ||
options.hash.t = 'c' | ||
return _D.call(this, { type: 'normal', originalCellLetter: data, calcChainUpdate: true }, options) | ||
} | ||
const updated = [] | ||
// alias for {{_D t='c'}} helper with autodetect call | ||
function _T () { | ||
let value | ||
let raw | ||
let options | ||
for (const targetItem of targetItems) { | ||
const template = mergeCellsTemplates[targetItem.original] | ||
const output = template({ newRef: targetItem.value }) | ||
updated.push(output) | ||
} | ||
return new Handlebars.SafeString(updated.join('\n')) | ||
if (arguments.length === 1) { | ||
options = arguments[0] | ||
} else if (arguments.length === 2) { | ||
value = arguments[0] | ||
options = arguments[1] | ||
} else { | ||
value = arguments[0] | ||
raw = arguments[1] === 1 | ||
options = arguments[2] | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'mergeCellItem' | ||
) { | ||
const originalCellRefRange = optionsToUse.hash.originalCellRefRange | ||
if (originalCellRefRange == null) { | ||
throw new Error('xlsxSData type="mergeCellItem" helper originalCellRefRange arg is required') | ||
} | ||
optionsToUse.data.mergeCellsTemplates[originalCellRefRange] = optionsToUse.fn | ||
return '' | ||
if (options.hash.t != null) { | ||
options.hash.__originalT__ = options.hash.t | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'formulaSharedRefRange' | ||
) { | ||
const rowNumber = optionsToUse.data.rowNumber | ||
options.hash.t = 'c' | ||
if (rowNumber == null) { | ||
throw new Error('xlsxSData type="formulaSharedRefRange" invalid usage, rowNumber needs to exists on internal data') | ||
} | ||
const data = { type: 'autodetect', originalCellLetter: options.hash.r, calcChainUpdate: false } | ||
const originalSharedRefRange = optionsToUse.hash.originalSharedRefRange | ||
data.value = value | ||
data.escape = true | ||
if (originalSharedRefRange == null) { | ||
throw new Error('xlsxSData type="formulaSharedRefRange" helper originalSharedRefRange arg is required') | ||
} | ||
const { newValue } = evaluateCellRefsFromExpression(originalSharedRefRange, (cellRefInfo) => { | ||
const newCellRef = generateNewCellRefFromRow(cellRefInfo.parsed, rowNumber) | ||
return newCellRef | ||
}) | ||
return newValue | ||
if (raw != null && raw) { | ||
data.escape = false | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'lazyFormulas' | ||
) { | ||
const trackedCells = optionsToUse.data.meta.trackedCells | ||
const lazyFormulas = optionsToUse.data.meta.lazyFormulas | ||
return _D.call(this, data, options) | ||
} | ||
if (trackedCells == null) { | ||
throw new Error(`xlsxSData type="${type}" invalid usage, trackedCells needs to exists on internal data`) | ||
} | ||
// alias for {{_D t='c'}} helper with autodetect call with calcChainUpdate: true | ||
function _t () { | ||
let value | ||
let raw | ||
let options | ||
if (lazyFormulas == null) { | ||
throw new Error(`xlsxSData type="${type}" invalid usage, lazyFormulas needs to exists on internal data`) | ||
} | ||
if (lazyFormulas.count == null || lazyFormulas.count === 0) { | ||
return new Handlebars.SafeString('') | ||
} | ||
const result = [] | ||
const lazyFormulaIds = Object.keys(lazyFormulas.data) | ||
for (const lazyFormulaId of lazyFormulaIds) { | ||
const lazyFormulaInfo = lazyFormulas.data[lazyFormulaId] | ||
const { | ||
formula, | ||
parsedOriginCellRef, | ||
originCellIsFromLoop, | ||
previousLoopIncrement, | ||
currentLoopIncrement, | ||
cellRefs | ||
} = lazyFormulaInfo | ||
const { formula: newFormula } = getNewFormula(type, formula, parsedOriginCellRef, { | ||
type: 'lazy', | ||
originCellIsFromLoop, | ||
previousLoopIncrement, | ||
currentLoopIncrement, | ||
trackedCells, | ||
lazyCellRefs: cellRefs | ||
}) | ||
result.push(`<item id="${lazyFormulaId}">${newFormula}</item>`) | ||
} | ||
return new Handlebars.SafeString(`<lazyFormulas>${result.join('\n')}</lazyFormulas>`) | ||
if (arguments.length === 1) { | ||
options = arguments[0] | ||
} else if (arguments.length === 2) { | ||
value = arguments[0] | ||
options = arguments[1] | ||
} else { | ||
value = arguments[0] | ||
raw = arguments[1] === 1 | ||
options = arguments[2] | ||
} | ||
// TODO: this should be refactored at some point to be more generic | ||
// and support nested loops, maybe the logic will be similar to mergeCell or formula helpers | ||
// when this is done we can remove all the methods that are only used here "getNewCellRef", "getCurrentAndPreviousLoopItemsByTarget" | ||
if ( | ||
arguments.length === 1 && | ||
type === 'newCellRef' | ||
) { | ||
const updatedOriginalCells = optionsToUse.data.meta.updatedOriginalCells | ||
const loopItems = optionsToUse.data.loopItems | ||
let targetItems = [] | ||
const updated = [] | ||
if (type === 'newCellRef') { | ||
targetItems = [{ value: optionsToUse.hash.originalCellRefRange }] | ||
} | ||
for (const targetItem of targetItems) { | ||
const regexp = /(\$?[A-Z]+\$?\d+:)?(\$?[A-Z]+\$?\d+)/g | ||
const newValue = targetItem.value.replace(regexp, (...args) => { | ||
const [match, _startingCellRef, endingCellRef] = args | ||
const isRange = _startingCellRef != null | ||
let newCellRef | ||
const ctx = { | ||
updatedOriginalCells, | ||
loopItems | ||
} | ||
if (isRange) { | ||
const startingCellRef = _startingCellRef.slice(0, -1) | ||
const newStartingCellRef = getNewCellRef(type === 'formulas' ? [targetItem.cellRef, startingCellRef] : startingCellRef, targetItem.loopMeta, 'rangeStart', ctx) | ||
const newEndingCellRef = getNewCellRef(type === 'formulas' ? [targetItem.cellRef, endingCellRef] : endingCellRef, targetItem.loopMeta, 'rangeEnd', ctx) | ||
return `${newStartingCellRef}:${newEndingCellRef}` | ||
} else { | ||
newCellRef = getNewCellRef(type === 'formulas' ? [targetItem.cellRef, endingCellRef] : endingCellRef, targetItem.loopMeta, 'standalone', ctx) | ||
} | ||
return newCellRef | ||
}) | ||
if (type === 'newCellRef') { | ||
updated.push(newValue) | ||
} | ||
} | ||
return new Handlebars.SafeString(updated.join('\n')) | ||
if (options.hash.t != null) { | ||
options.hash.__originalT__ = options.hash.t | ||
} | ||
if ( | ||
arguments.length === 1 && | ||
type === 'dimensionRef' | ||
) { | ||
const originalCellRefRange = optionsToUse.hash.originalCellRefRange | ||
options.hash.t = 'c' | ||
if (originalCellRefRange == null) { | ||
throw new Error('xlsxSData type="dimensionRef" helper originalCellRefRange arg is required') | ||
} | ||
const data = { type: 'autodetect', originalCellLetter: options.hash.r, calcChainUpdate: true } | ||
const refsParts = originalCellRefRange.split(':') | ||
data.value = value | ||
data.escape = true | ||
if (refsParts.length === 1) { | ||
return refsParts[0] | ||
} | ||
const lastCellRef = optionsToUse.data.meta.lastCellRef | ||
const parsedEndCellRef = parseCellRef(refsParts[1]) | ||
const parsedLastCellRef = parseCellRef(lastCellRef) | ||
return `${refsParts[0]}:${parsedEndCellRef.letter}${parsedLastCellRef.rowNumber}` | ||
if (raw != null && raw) { | ||
data.escape = false | ||
} | ||
throw new Error(`invalid usage of xlsxSData helper${type != null ? ` (type: ${type})` : ''}`) | ||
return _D.call(this, data, options) | ||
} |
253125
38
5458
92
14
+ Addednanoid@3.2.0
+ Addednanoid@3.2.0(transitive)
- Removeduuid@8.3.2
- Removeduuid@8.3.2(transitive)