@jsreport/jsreport-xlsx
Version:
jsreport recipe rendering excels directly from open xml
1,390 lines (1,101 loc) • 44.7 kB
JavaScript
/* global structuredClone */
/* eslint no-unused-vars: 0 */
function xlsxContext (options) {
const Handlebars = require('handlebars')
const { type: contextType, path: xlsxFilePath } = options.hash
let data
if (contextType === 'global') {
const jsreport = require('jsreport-proxy')
data = Handlebars.createFrame(options.data)
data.evalId = jsreport.req.context.__xlsxSharedData.evalId
} else if (contextType === 'file') {
const jsreport = require('jsreport-proxy')
const fileData = jsreport.req.context.__xlsxSharedData.fileDataMap.get(xlsxFilePath)
data = Handlebars.createFrame(options.data)
if (fileData?.dataVariables != null) {
Object.assign(data, fileData.dataVariables)
}
data.xlsxFilePath = xlsxFilePath
}
const context = {}
if (data) {
context.data = data
}
const result = options.fn(this, context)
return result
}
function xlsxCType (options) {
const type = options.hash.t
if (type == null) {
throw new Error('xlsxCType helper requires type parameter to be set')
}
const validTypes = ['s', 'b', 'n']
if (!validTypes.includes(type)) {
throw new Error(`xlsxCType helper requires type parameter to be one of: ${validTypes.join(', ')}`)
}
options.data.cellType = type === 's' ? 'inlineStr' : type
return ''
}
function xlsxColAutofit (options) {
const jsreport = require('jsreport-proxy')
const { runtime } = jsreport.req.context.__xlsxSharedData.fileDataMap.get(options.data.xlsxFilePath)
if (
runtime.autoFit.enabledFor.length > 0 &&
options.hash.all === true
) {
runtime.autoFit.enabledFor = [true]
}
const allColsEnabled = runtime.autoFit.enabledFor[0] === true
if (!allColsEnabled) {
for (const [colLetter] of runtime.autoFit.cols) {
// remove cells that are not enabled for autofit
if (!runtime.autoFit.enabledFor.includes(colLetter)) {
runtime.autoFit.cols.delete(colLetter)
}
}
}
return ''
}
function xlsxChart (options) {
const jsreport = require('jsreport-proxy')
const { runtime } = jsreport.req.context.__xlsxSharedData.fileDataMap.get(options.data.xlsxFilePath)
if (options.hash.data == null) {
throw new Error('xlsxChart helper requires data parameter to be set')
}
if (!Array.isArray(options.hash.data.labels) || options.hash.data.labels.length === 0) {
throw new Error('xlsxChart helper requires data parameter with labels to be set, data.labels must be an array with items')
}
if (!Array.isArray(options.hash.data.datasets) || options.hash.data.datasets.length === 0) {
throw new Error('xlsxChart helper requires data parameter with datasets to be set, data.datasets must be an array with items')
}
if (
options.hash.options != null &&
(
typeof options.hash.options !== 'object' ||
Array.isArray(options.hash.options)
)
) {
throw new Error('xlsxChart helper when options parameter is set, it should be an object')
}
runtime.configuration.data = options.hash.data
runtime.configuration.options = options.hash.options
return ''
}
const __xlsxD = (function () {
let __assetOkHelper
function assertOk (...args) {
let fn
if (__assetOkHelper == null) {
const { helpers: { generationUtils } } = getSharedData()
__assetOkHelper = generationUtils.assertOk
fn = generationUtils.assertOk
} else {
fn = __assetOkHelper
}
return fn(...args)
}
function getSharedData () {
const jsreport = require('jsreport-proxy')
if (jsreport.req.context.__xlsxSharedData == null) {
throw new Error('__xlsxSharedData needs to exists on request context')
}
return jsreport.req.context.__xlsxSharedData
}
function getFileData (xlsxFilePath) {
assertOk(xlsxFilePath != null, 'xlsxFilePath needs to exists on internal data')
const { fileDataMap } = getSharedData()
assertOk(fileDataMap != null, 'fileDataMap needs to exists on internal data')
return fileDataMap.get(xlsxFilePath)
}
// helper to allow ignoring handlebars tags in specific cases
function raw (options) {
return options.fn()
}
function staticRange (options) {
const startElementIdx = options.hash.start
const endElementIdx = options.hash.end
assertOk(startElementIdx != null, 'start arg is required')
assertOk(endElementIdx != null, 'end arg is required')
const { templateItems } = getFileData(options.data.xlsxFilePath)
for (let elementIdx = startElementIdx; elementIdx <= endElementIdx; elementIdx++) {
const elementItem = templateItems.data[elementIdx]
const elementItemType = elementItem.type ?? 'row'
if (elementItemType === 'row') {
const elementItemMeta = templateItems.elementMetaMap.get(elementItem)
options.fn({
rowNumber: elementItem.id,
cells: [...elementItemMeta.columnLetterChildrenMap.keys()]
})
} else {
throw new Error(`staticRange helper does not support element item type "${elementItemType}"`)
}
}
return ''
}
// executes a different types of loops over data and tracks the loop information in the internal data
// to update row, cell indexes
function loop (...args) {
let data
let options
if (args.length === 1) {
options = args[0]
} else {
data = args[0]
options = args[1]
}
const Handlebars = require('handlebars')
const customCells = options.hash.cells
const customCellsForColumns = options.hash.cellsT === 'columns'
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)
const isVertical = Object.hasOwn(options.hash, 'vertical')
const { helpers: { generationUtils: { getParentLoopItemByHierarchy } } } = getSharedData()
const { runtime } = getFileData(options.data.xlsxFilePath)
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')
let targetData = data
if (Object.hasOwn(options.hash, 'cells')) {
targetData = customCells
if (customCells == null || customCells.length === 0) {
newData.emptyCells = true
}
} else if (customCellsForColumns) {
targetData = options.data.emptyCells === true ? [] : this
}
// for empty we create an array with one empty object,
// this is needed because we want to preserve the original row
if (
targetData == null ||
(Array.isArray(targetData) && targetData.length === 0)
) {
targetData = [{}]
}
let type
if (customCells) {
type = 'row'
} else if (isVertical || customCellsForColumns) {
type = 'vertical'
} else {
type = end == null ? 'row' : 'block'
}
const loopItem = {
type,
id: null,
hierarchyId,
start,
columnStart,
end,
columnEnd,
length: targetData.length,
parentLoopIndex: options.data.index,
children: [],
completed: false
}
if (type === 'vertical') {
loopItem.rowNumber = options.data.r
loopItem.trackedCells = new Map()
}
const parentLoopItem = getParentLoopItemByHierarchy(loopItem, runtime.loops.data)
let container
if (parentLoopItem) {
container = parentLoopItem.children
} else {
container = runtime.loops.data
}
loopItem.id = `${parentLoopItem != null ? `${parentLoopItem.id}#` : ''}${container.length}`
container.push(loopItem)
newData.currentLoopId = loopItem.id
runtime.loops.evaluated.push(loopItem.id)
const nonExistingCellsInCurrentLoop = runtime.loops.nonExistingCellRefsByLoopHierarchy.get(hierarchyId) ?? []
// updating the currentLoopId for the non existing cells that are part of this loop
for (const nonExistingCellRef of nonExistingCellsInCurrentLoop) {
const trackedCell = runtime.trackedCells.get(nonExistingCellRef)
trackedCell.currentLoopId = loopItem.id
}
const dynamicCells = customCells || customCellsForColumns
if (dynamicCells && !Array.isArray(targetData)) {
throw new Error(`Invalid data to generate dynamic cells. data for ${customCells ? 'rows' : 'columns'} is not an array`)
}
for (let i = 0; i < targetData.length; i++) {
newData.index = i
newData.key = i
if (customCellsForColumns) {
newData.firstColumn = i === 0
newData.lastColumn = i === targetData.length - 1
newData.columnIndex = i
} else {
newData.first = i === 0
newData.last = i === targetData.length - 1
}
if (customCells) {
newData.rowIndex = i
}
let dataForItem = targetData[i]
if (dynamicCells && dataForItem != null && typeof dataForItem === 'object' && !Array.isArray(dataForItem)) {
// when dynamic cells if data is plain object, use the value property
dataForItem = dataForItem.value ?? ''
}
const newOptions = {
...options,
data: newData
}
const addBlockParams = options.fn.blockParams != null && options.fn.blockParams > 0
if (addBlockParams) {
// since we use our custom loop helper (not the built-in each)
// we need propagate the expected block params from the loop in order
// for the body of the loop helper to render the values appropriately.
// we detect if the user is expecting block params by checking options.fn.blockParams
newOptions.blockParams = [dataForItem, newData.key].slice(0, options.fn.blockParams)
}
options.fn(dataForItem, newOptions)
}
loopItem.completed = true
return ''
}
loop.dynamicParameters = true
// stores values generated when rendering data template
function r (originalRowNumber, options) {
const Handlebars = require('handlebars')
const { helpers: { generationUtils: { getIncrementWithLoop, updateMergeCells } } } = getSharedData()
const { runtime, templateItems, mergeCellItems, dataItems } = getFileData(options.data.xlsxFilePath)
assertOk(originalRowNumber != null, 'originalRowNumber arg is required')
const rowElementIdx = templateItems.rowNumberElementIdxMap.get(originalRowNumber)
const rowElementItem = templateItems.data[rowElementIdx]
if (rowElementItem == null) {
throw new Error(`No element row found at index ${rowElementIdx}`)
}
const elementType = rowElementItem.type ?? 'row'
if (elementType !== 'row') {
throw new Error(`Element at index ${rowElementIdx} is not a row`)
}
const {
increment: rowIncrement,
currentLoopIncrement: rowCurrentLoopIncrement,
previousRootLoopIncrement: rowPreviousRootLoopIncrement,
previousLoopIncrement: rowPreviousLoopIncrement
} = getIncrementWithLoop(
'row',
{
loopId: options.data.currentLoopId,
loopIndex: options.data.index,
evaluatedLoopsIds: runtime.loops.evaluated,
loopItems: runtime.loops.data
}
)
const newRowNumber = originalRowNumber + rowIncrement
const newRowItem = { idx: rowElementIdx, r: newRowNumber, cells: [] }
dataItems.push(newRowItem)
const newData = Handlebars.createFrame(options.data)
newData.originalRowNumber = originalRowNumber
newData.r = newRowNumber
// only a value that represents the increment of previous loops defined before the cell
newData.rowPreviousLoopIncrement = rowPreviousRootLoopIncrement
// this is a value that represents all the executions of the current loop (considering nested loops too)
newData.rowCurrentLoopIncrement = rowCurrentLoopIncrement + (rowPreviousLoopIncrement - rowPreviousRootLoopIncrement)
newData.cellOutputsMap = new Map()
options.fn(this, { ...options, data: newData })
const mergeStartLetterMap = templateItems.elementMetaMap.get(rowElementItem).mergeStartLetterMap ?? new Map()
// we resolve merge cells on the row level, because there can be merge cells definitions
// that reference cells that does not exists
// (cell tag not present only empty row tag in xml)
if (newData.cellOutputsMap.size === 0) {
for (const [cellLetter, mergeCellInfo] of mergeStartLetterMap) {
updateMergeCells(mergeCellItems, mergeCellInfo, {
letter: cellLetter,
rowNumber: newRowNumber
})
}
} else {
for (const [cellLetter, { originalCellLetter, output }] of newData.cellOutputsMap) {
const newCellInfo = {
r: cellLetter,
output
}
// check if there were merge cells affecting the original cell, if yes,
// add new merge cell
const mergeCellInfo = mergeStartLetterMap.get(originalCellLetter)
if (mergeCellInfo) {
updateMergeCells(mergeCellItems, mergeCellInfo, {
letter: cellLetter,
rowNumber: newRowNumber
})
}
// to minimize the amount of values we store
// (considering that there can be a lot of cells in a xlsx)
// we only put .oldR if it is different than the generated r
if (cellLetter !== originalCellLetter) {
newCellInfo.oldR = originalCellLetter
}
newRowItem.cells.push(newCellInfo)
}
}
return ''
}
// stores values generated when rendering data template
function c (originalCellLetter, options) {
const Handlebars = require('handlebars')
const {
originalRowNumber,
r: rowNumber,
cellOutputsMap
} = options.data
assertOk(originalRowNumber != null, 'originalRowNumber needs to exists on internal data')
assertOk(rowNumber != null, 'r needs to exists on internal data')
assertOk(cellOutputsMap != null, 'cellOutputsMap needs to exists on internal data')
assertOk(originalCellLetter != null, 'originalCellLetter arg is required')
const {
calcChainFilePath,
helpers: {
parseXML,
generationUtils: {
getIncrementWithLoop, getCurrentLoopItem, updateDimension,
getNewFormula, tryToResolvePendingLazyFormula
},
cellUtils: {
parseCellRef, getColumnFor, generateNewCellRefFrom, evaluateCellRefsFromExpression,
getFontSizeFromStyle, getPixelWidthOfValue
}
}
} = getSharedData()
const { templateItems, runtime } = getFileData(options.data.xlsxFilePath)
const rowElementItem = templateItems.data[templateItems.rowNumberElementIdxMap.get(originalRowNumber)]
const rowElementItemMetadata = templateItems.elementMetaMap.get(rowElementItem)
const cellElementMetadata = templateItems.elementMetaMap.get(
rowElementItem.children[rowElementItemMetadata.columnLetterChildrenMap.get(originalCellLetter)]
)
const {
increment: columnIncrement,
currentLoopIncrement: columnCurrentLoopIncrement,
previousRootLoopIncrement: columnPreviousRootLoopIncrement,
previousLoopIncrement: columnPreviousLoopIncrement
} = getIncrementWithLoop(
'column',
{
loopId: options.data.currentLoopId,
loopIndex: options.data.index,
rowNumber,
evaluatedLoopsIds: runtime.loops.evaluated,
loopItems: runtime.loops.data
}
)
const [columnLetter, columnNumber] = getColumnFor(
originalCellLetter,
columnIncrement
)
const updatedCellRef = `${columnLetter}${rowNumber}`
updateDimension(runtime, { rowNumber, columnNumber })
const originalCellRef = originalCellLetter + originalRowNumber
// check if the pending not completed loops are done, if so,
// resolve pending lazy formulas
if (runtime.lazyFormulas.pending.notCompletedLoops.size > 0) {
const targetLoopIds = [...runtime.lazyFormulas.pending.notCompletedLoops.keys()]
for (const loopId of targetLoopIds) {
const lazyCellRefToFormulas = runtime.lazyFormulas.pending.notCompletedLoops.get(loopId)
const loopItem = getCurrentLoopItem(loopId, runtime.loops.data)
if (!loopItem.completed) {
continue
}
for (const [lazyCellRef, lazyFormulaIds] of lazyCellRefToFormulas) {
for (const lazyFormulaId of lazyFormulaIds) {
tryToResolvePendingLazyFormula(lazyFormulaId, lazyCellRef, runtime.lazyFormulas, runtime.trackedCells, runtime.loops.data)
}
}
runtime.lazyFormulas.pending.notCompletedLoops.delete(loopId)
}
}
let trackedCell = runtime.trackedCells.get(originalCellRef)
if (!trackedCell) {
trackedCell = { first: null, last: null, count: 0 }
runtime.trackedCells.set(originalCellRef, trackedCell)
}
const lazyFormulaIdsForCell = runtime.lazyFormulas.pending.cellsToFormulaIds.get(originalCellRef) || []
let targetLazyFormulas = []
const isPartOfLazyFormula = lazyFormulaIdsForCell.length > 0
let loopItem
if (options.data.currentLoopId != null) {
loopItem = getCurrentLoopItem(options.data.currentLoopId, runtime.loops.data)
if (loopItem?.type === 'vertical') {
let item
if (loopItem.trackedCells.has(originalCellRef)) {
item = loopItem.trackedCells.get(originalCellRef)
} else {
item = new Map()
loopItem.trackedCells.set(originalCellRef, item)
}
item.set(columnLetter, updatedCellRef)
}
trackedCell.currentLoopId = options.data.currentLoopId
}
if (isPartOfLazyFormula) {
targetLazyFormulas = lazyFormulaIdsForCell.map((lazyFormulaId) => {
const lazyFormula = runtime.lazyFormulas.data.get(lazyFormulaId)
const originCellLoopId = runtime.trackedCells.get(lazyFormula.originCellRef)?.currentLoopId
let inSameLoopLevel
// check if the referenced cell is at same level than the origin formula cell
if (originCellLoopId == null && options.data.currentLoopId == null) {
inSameLoopLevel = true
} else {
inSameLoopLevel = originCellLoopId === options.data.currentLoopId
}
let resolve = true
if (!inSameLoopLevel && loopItem != null) {
resolve = loopItem.completed
}
return {
id: lazyFormulaId,
loopItem,
resolve
}
})
}
if (trackedCell.first == null) {
trackedCell.first = updatedCellRef
}
trackedCell.last = updatedCellRef
trackedCell.count += 1
// update calChain if the cell was referenced
if (calcChainFilePath != null && cellElementMetadata?.calcChainElementIdx != null) {
const { dataItems: calcChainDataItems } = getFileData(calcChainFilePath)
calcChainDataItems.push({
idx: cellElementMetadata.calcChainElementIdx,
r: updatedCellRef
})
}
// update table ref if the cell is part of a table ref
if (cellElementMetadata?.tableRef != null) {
const tableRef = cellElementMetadata.tableRef
const tableRefParts = cellElementMetadata.tableRef.split(':')
const type = originalCellRef === tableRefParts[0] ? 'start' : 'end'
const tableRefMeta = runtime.tables.refsMeta.get(tableRef)
const tableFilePath = runtime.tables.filePaths[tableRefMeta.tableFilePathIdx]
const { dataVariables: tableDataVariables } = getFileData(tableFilePath)
let newTableRef
if (type === 'start') {
newTableRef = `${trackedCell.last}:${tableRefParts[1]}`
} else {
newTableRef = `${tableRefParts[0]}:${trackedCell.last}`
}
tableDataVariables[tableRefMeta.dataVariableName] = newTableRef
}
// we try to resolve lazy formulas here if any
for (const targetLazyFormula of targetLazyFormulas) {
if (targetLazyFormula.resolve) {
tryToResolvePendingLazyFormula(targetLazyFormula.id, originalCellRef, runtime.lazyFormulas, runtime.trackedCells, runtime.loops.data)
} else if (targetLazyFormula.loopItem != null) {
let cellsForLoopMap = runtime.lazyFormulas.pending.notCompletedLoops.get(targetLazyFormula.loopItem.id)
if (!cellsForLoopMap) {
cellsForLoopMap = new Map()
runtime.lazyFormulas.pending.notCompletedLoops.set(targetLazyFormula.loopItem.id, cellsForLoopMap)
}
let formulasInCell = cellsForLoopMap.get(originalCellRef)
if (!formulasInCell) {
formulasInCell = []
cellsForLoopMap.set(originalCellRef, formulasInCell)
}
if (!formulasInCell.includes(targetLazyFormula.id)) {
formulasInCell.push(targetLazyFormula.id)
}
}
}
let cellValue
let cellType
if (cellElementMetadata?.formula != null) {
cellType = 'str'
const {
rowPreviousLoopIncrement,
rowCurrentLoopIncrement
} = options.data
assertOk(rowPreviousLoopIncrement != null, 'row previousLoopIncrement needs to exists on internal data')
assertOk(rowCurrentLoopIncrement != null, 'row currentLoopIncrement needs to exists on internal data')
const originalFormula = cellElementMetadata.formula.value
const originCellIsFromLoop = options.data.currentLoopId != null
const parsedOriginCellRef = parseCellRef(originalCellRef)
cellValue = {}
if (cellElementMetadata.formula.attributes) {
cellValue.attributes = structuredClone(cellElementMetadata.formula.attributes)
}
if (cellElementMetadata.formula.shared?.type === 'reference') {
// originalFormula is just empty string in this case so it is going to
// be empty "f"
cellValue.formula = originalFormula
} else {
// update the formula ref with the values of updated cell ref or
// queue lazy formulas to resolve them later
const { lazyUsedCells = {}, formula: newFormula } = getNewFormula(originalFormula, parsedOriginCellRef, {
type: 'normal',
originCellIsFromLoop,
rowPreviousLoopIncrement,
rowCurrentLoopIncrement,
columnPreviousLoopIncrement,
columnCurrentLoopIncrement,
trackedCells: runtime.trackedCells,
getCurrentLoopItem: (currentLoopId) => {
return getCurrentLoopItem(currentLoopId, runtime.loops.data)
},
includeLoopIncrementResolver: (cellRefIsFromLoop, cellRefInfo) => {
const trackedCell = runtime.trackedCells.get(cellRefInfo.localRef)
// this is used when referencing a cell that it is not defined in the sheet
return (
cellRefIsFromLoop &&
trackedCell?.fromNonExistingLoopHierarchyId === getCurrentLoopItem(options.data.currentLoopId, runtime.loops.data)?.hierarchyId
)
},
lazyFormulas: runtime.lazyFormulas,
currentCellRef: updatedCellRef
})
if (Object.keys(lazyUsedCells).length > 0) {
cellValue.lazy = {
id: newFormula,
usedCells: lazyUsedCells
}
} else {
cellValue.formula = newFormula
}
if (cellElementMetadata.formula.shared?.type === 'source') {
const { newValue: newRef } = evaluateCellRefsFromExpression(cellElementMetadata.formula.shared.sourceRef, (cellRefInfo) => {
const isRange = cellRefInfo.type === 'rangeStart' || cellRefInfo.type === 'rangeEnd'
assertOk(isRange, `cell ref expected to be a range. value: "${cellElementMetadata.formula.shared.sourceRef}`)
const columnIncrement = cellRefInfo.type === 'rangeEnd' ? cellRefInfo.parsedRangeEnd.columnNumber - cellRefInfo.parsedRangeStart.columnNumber : 0
const [newColumnLetter] = getColumnFor(columnNumber, columnIncrement)
const rowIncrement = cellRefInfo.type === 'rangeEnd' ? cellRefInfo.parsedRangeEnd.rowNumber - cellRefInfo.parsedRangeStart.rowNumber : 0
const newRowNumber = rowNumber + rowIncrement
const newCellRef = generateNewCellRefFrom(cellRefInfo.parsed, {
columnLetter: newColumnLetter,
rowNumber: newRowNumber
})
return newCellRef
})
// we know there is going to always attributes if we get to here
cellValue.attributes.set('ref', newRef)
}
}
} else if (options.fn != null) {
const newData = Handlebars.createFrame(options.data)
newData.originalColumnLetter = originalCellLetter
newData.originalCellRef = originalCellRef
newData.l = columnLetter
newData.currentCellRef = updatedCellRef
// only a value that represents the increment of previous loops defined before the cell
newData.columnPreviousLoopIncrement = columnPreviousRootLoopIncrement
// this is a value that represents all the executions of the current loop (considering nested loops too)
newData.columnCurrentLoopIncrement = columnCurrentLoopIncrement + (columnPreviousLoopIncrement - columnPreviousRootLoopIncrement)
newData.cellValue = null
newData.cellType = null
const cellTemplateOptions = {
...options,
data: newData
}
// if we get to this point the cell contains dynamic parts,
// we call the body of the cell helper to resolve those values
const cellRawValue = options.fn(this, cellTemplateOptions)
if (newData.cellValue != null) {
// there will be cellValue set if there was a cell possible to auto detect
cellValue = newData.cellValue
} else {
// otherwise we use the text content from the raw value
const tmpDoc = parseXML(cellRawValue)
cellValue = tmpDoc.documentElement.textContent || ''
}
if (newData.cellType != null) {
cellType = newData.cellType
// if we got explicit cellType, try to parse the cell value
// to the type specified
if (cellType === 'inlineStr' && typeof cellValue !== 'string') {
if (cellValue == null) {
cellValue = ''
} else {
cellValue = cellValue.toString()
}
} else if (cellType === 'b' && typeof cellValue !== 'boolean') {
if (cellValue == null) {
cellValue = false
} else if (cellValue === 'true' || cellValue === 'false') {
cellValue = cellValue === 'true'
} else {
const asNumber = parseInt(cellValue, 10)
if (isNaN(asNumber)) {
cellValue = false
} else {
cellValue = cellValue !== 0
}
}
} else if (cellType === 'n' && typeof cellValue !== 'number') {
if (cellValue == null) {
cellValue = 0
} else {
const asNumber = parseFloat(cellValue)
if (isNaN(asNumber)) {
cellValue = 0
} else {
cellValue = asNumber
}
}
}
}
if (cellType == null) {
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'
}
}
let isAutoFitEnabled = false
if (
(runtime.autoFit.enabledFor[0] === true) ||
runtime.autoFit.enabledFor.includes(originalCellLetter)
) {
isAutoFitEnabled = true
}
if (isAutoFitEnabled) {
const fontSize = getFontSizeFromStyle(
cellElementMetadata.styleId,
runtime.style.info,
runtime.style.fontSizeCache
)
const colSize = runtime.autoFit.cols.get(originalCellLetter)
const size = getPixelWidthOfValue(cellValue, fontSize)
if (colSize == null || size > colSize) {
runtime.autoFit.cols.set(originalCellLetter, size)
}
}
// only consider the raw value if the value was not empty
if (cellType === 'inlineStr' && cellValue != null && cellValue !== '') {
cellValue = cellRawValue
}
}
// start the cellOutput with empty value, which acts as a signal that the cell should
// take the template element information as it is (with no other modifications)
let cellOutput = null
if (cellType) {
if (cellValue == null || cellValue === '') {
// when we mark cell as empty just use inlineStr type because the original cell
// was a string
cellOutput = { type: 'inlineStr', empty: true }
} else {
cellOutput = { type: cellType }
// construct the final xml values, we use the xmldom because it takes of
// xml encoding automatically
if (cellType === 'inlineStr') {
cellOutput.value = cellValue
} else if (cellType === 'b') {
const tmpDoc = parseXML('<fragment />')
const vEl = tmpDoc.createElement('v')
tmpDoc.documentElement.appendChild(vEl)
vEl.textContent = cellValue ? '1' : '0'
cellOutput.value = tmpDoc.documentElement.childNodes[0].toString()
} else if (cellType === 'n') {
const tmpDoc = parseXML('<fragment />')
const vEl = tmpDoc.createElement('v')
tmpDoc.documentElement.appendChild(vEl)
vEl.textContent = cellValue
cellOutput.value = tmpDoc.documentElement.childNodes[0].toString()
} else if (cellType === 'str') {
const getFormula = (newFormula, _attributesMap) => {
const tmpDoc = parseXML('<fragment />')
const fEl = tmpDoc.createElement('f')
tmpDoc.documentElement.appendChild(fEl)
const attributesMap = _attributesMap ?? new Map()
for (const [attrName, attrValue] of attributesMap) {
fEl.setAttribute(attrName, attrValue)
}
fEl.textContent = newFormula
return tmpDoc.documentElement.childNodes[0].toString()
}
if (cellValue.lazy) {
const { lazy, ...restOfCellValue } = cellValue
// we are going to resolve this to raw string later
cellOutput.value = {
lazy: true,
...restOfCellValue,
getFormula
}
for (const lazyUsedCell of Object.values(lazy.usedCells)) {
let cellToFormulaRefItem = runtime.lazyFormulas.pending.cellsToFormulaIds.get(lazyUsedCell.cellRef)
if (!cellToFormulaRefItem) {
cellToFormulaRefItem = []
runtime.lazyFormulas.pending.cellsToFormulaIds.set(lazyUsedCell.cellRef, cellToFormulaRefItem)
}
if (!cellToFormulaRefItem.includes(lazy.id)) {
cellToFormulaRefItem.push(lazy.id)
}
}
runtime.lazyFormulas.data.get(lazy.id).cellOutput = cellOutput
} else {
cellOutput.value = getFormula(cellValue.formula, cellValue.attributes)
}
}
assertOk(cellOutput.value != null, `cell type "${cellType}" not supported`)
}
}
cellOutputsMap.set(columnLetter, {
originalCellLetter,
output: cellOutput
})
return ''
}
function cValue (...args) {
let _value
let options
let shouldCallBlock = false
if (args.length === 1) {
shouldCallBlock = true
options = args[0]
} else {
_value = args[0]
options = args[1]
}
let value
if (shouldCallBlock) {
const thisUnwrapped = this != null && typeof this.valueOf === 'function' ? this.valueOf() : this
value = options.fn(thisUnwrapped)
} else {
value = _value
// if value is null we try to resolve it from helper, replicating the same
// logic that handlebars does
if (value === undefined && options.hash.n != null) {
const Handlebars = require('handlebars')
if (Handlebars.helpers[options.hash.n]) {
value = Handlebars.helpers[options.hash.n]()
}
}
}
if (value != null && typeof value.valueOf === 'function') {
// we do this because handlebars something wraps the primitive values with their
// object counterparts, so we need to ensure that we get the primitive value for
// conditions to work correctly
value = value.valueOf()
}
options.data.cellValue = value
return value
}
cValue.dynamicParameters = true
// solve any pending formulas that were waiting to complete
function lazyFormulas (options) {
const { helpers: { generationUtils: { tryToResolvePendingLazyFormula } } } = getSharedData()
const { runtime } = getFileData(options.data.xlsxFilePath)
if (runtime.lazyFormulas.data.size === 0) {
return ''
}
const targetLazyFormulaIds = [...runtime.lazyFormulas.data.keys()]
for (const lazyFormulaId of targetLazyFormulaIds) {
const lazyFormulaInfo = runtime.lazyFormulas.data.get(lazyFormulaId)
const pendingCellRefs = [...lazyFormulaInfo.pendingCellRefs]
for (const cellRef of pendingCellRefs) {
// resolve all the lazy pending formulas, the reason we got until this point is likely
// that a formula is referencing a cell that does not have a definition in the sheet
tryToResolvePendingLazyFormula(
lazyFormulaId, cellRef, runtime.lazyFormulas,
runtime.trackedCells, runtime.loops.data
)
}
}
}
// resolves the chart title content
function chartTitleText (options) {
const Handlebars = require('handlebars')
const { runtime } = getFileData(options.data.xlsxFilePath)
const output = options.fn(this)
runtime.chartTitleTextXml = output
return new Handlebars.SafeString(output)
}
// produces the final dimension ref using the information of the latest
// column and row numbers
function dimension (options) {
const xlsxFilePath = options.data.xlsxFilePath
const { runtime } = getFileData(xlsxFilePath)
const { helpers: { cellUtils: { getColumnFor } } } = getSharedData()
const startCellRef = getColumnFor(runtime.dimension.start.columnNumber)[0] + runtime.dimension.start.rowNumber
const endCellRef = getColumnFor(runtime.dimension.end.columnNumber)[0] + runtime.dimension.end.rowNumber
if (startCellRef === endCellRef) {
return startCellRef
}
return `${startCellRef}:${endCellRef}`
}
// produces the final <sheetData> content when rendering xml template
function sd (options) {
const Handlebars = require('handlebars')
const {
helpers: {
parseXML,
generationUtils: { renderDataItems, getAttributeFromElementTypeAttributes }
}
} = getSharedData()
const xlsxFilePath = options.data.xlsxFilePath
const { templateItems, dataItems } = getFileData(xlsxFilePath)
let rowNumber
const output = renderDataItems(parseXML('<sheetData/>'), dataItems, {
prepareItem: (dataItem) => {
const templateItem = templateItems.data[dataItem.idx]
const itemType = templateItem.type ?? 'row'
let newItem
if (itemType === 'row') {
const templateRowMeta = templateItems.elementMetaMap.get(templateItem)
const { children: templateRowChildren, ...templateRowItem } = templateItem
// clone without children, we are going to take care of them
// bellow
newItem = structuredClone(templateRowItem)
newItem.customAttributes = new Map([
['r', dataItem.r]
])
if (dataItem.cells.length > 0) {
const newChildren = []
for (const cellInfo of dataItem.cells) {
const childrenIdx = templateRowMeta.columnLetterChildrenMap.get(cellInfo.oldR ?? cellInfo.r)
const newCell = structuredClone(templateRowChildren[childrenIdx] ?? {})
newCell.type = 'c'
newCell.customAttributes = new Map([
['r', cellInfo.r]
])
// when output is different than null it means the cell generated value
// therefore we need to set the new children (custom content for cell),
// otherwise just leave the information of the element cell as it is
if (cellInfo.output != null) {
newCell.customAttributes.set('t', cellInfo.output.type)
if (cellInfo.output.empty !== true) {
newCell.children = [{ type: '#raw', value: cellInfo.output.value }]
}
}
newChildren.push(newCell)
}
newItem.children = newChildren
}
} else {
newItem = structuredClone(templateItem)
}
return newItem
},
getDefaultItemType: (parentItem) => {
if (parentItem == null) {
return 'row'
}
if (parentItem.type === 'row') {
return 'c'
}
},
processAttribute: (origin, itemType, _attrName, _attrValue) => {
let attrName
let attrValue
if (origin === 'customAttributes') {
attrName = _attrName
attrValue = _attrValue
if (attrName === 'r') {
if (itemType === 'row') {
rowNumber = attrValue
} else if (itemType === 'c') {
if (rowNumber == null) {
throw new Error(`Cannot set cell "r" attribute when rowNumber is not defined in xlsxFilePath "${xlsxFilePath}"`)
}
// make the new cell ref
attrValue = attrValue + rowNumber
}
}
} else {
[attrName, attrValue] = getAttributeFromElementTypeAttributes(
templateItems.elementTypeAttributesMap,
itemType,
_attrName,
_attrValue
)
}
return [attrName, attrValue]
}
})
return new Handlebars.SafeString(output)
}
// produces the final <mergeCells> content when rendering xml template
function mergeCells (options) {
const Handlebars = require('handlebars')
const xlsxFilePath = options.data.xlsxFilePath
const { mergeCellItems } = getFileData(xlsxFilePath)
const parts = []
// sort the merge cell items by idx ASC to preserve the order of the original calcChain elements
mergeCellItems.sort((a, b) => a.idx - b.idx)
for (let idx = 0; idx < mergeCellItems.length; idx++) {
const mergeCellInfo = mergeCellItems[idx]
const isFirst = idx === 0
const isLast = idx === mergeCellItems.length - 1
if (isFirst) {
parts.push(`<mergeCells count="${mergeCellItems.length}">`)
}
parts.push(`<mergeCell ref="${mergeCellInfo.ref}"/>`)
if (isLast) {
parts.push('</mergeCells>')
}
}
return new Handlebars.SafeString(parts.join(''))
}
// produce the content of the <calcChain> doc content when rendering xml template
function calcChain (options) {
const Handlebars = require('handlebars')
const {
helpers: {
parseXML,
generationUtils: { renderDataItems, getAttributeFromElementTypeAttributes }
}
} = getSharedData()
const xlsxFilePath = options.data.xlsxFilePath
const { templateItems, dataItems } = getFileData(xlsxFilePath)
// sort the dataItems by idx ASC to preserve the order of the original calcChain elements
dataItems.sort((a, b) => a.idx - b.idx)
const output = renderDataItems(
parseXML('<fragment />'),
dataItems,
{
prepareItem: (dataItem) => {
const templateItem = templateItems.data[dataItem.idx]
const itemType = templateItem.type ?? 'c'
let newItem
if (itemType === 'c') {
newItem = structuredClone(templateItem)
newItem.customAttributes = new Map([
['r', dataItem.r]
])
} else {
newItem = structuredClone(templateItem)
}
return newItem
},
getDefaultItemType: (parentItem) => {
if (parentItem == null) {
return 'c'
}
},
processAttribute: (origin, itemType, _attrName, _attrValue) => {
if (origin !== 'attributes') {
return
}
return getAttributeFromElementTypeAttributes(
templateItems.elementTypeAttributesMap,
itemType,
_attrName,
_attrValue
)
}
},
false
)
return new Handlebars.SafeString(output)
}
// produce the final <cols> content when rendering xml template
function cols (options) {
const Handlebars = require('handlebars')
const xlsxFilePath = options.data.xlsxFilePath
const {
helpers: { parseXML, cellUtils: { getColumnFor } }
} = getSharedData()
const existingColsXml = options.fn(this)
const doc = parseXML(existingColsXml)
const colsEl = doc.documentElement
const { runtime } = getFileData(xlsxFilePath)
const existingBaseColEls = Array.from(colsEl.getElementsByTagName('col'))
for (const [colLetter, colSize] of runtime.autoFit.cols) {
const colSizeInNumberCharactersMDW = (colSize / 6.5) + 2 // 2 is for padding
const colNumber = getColumnFor(colLetter)[1]
const existingColEl = existingBaseColEls.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)
}
}
let output
// return empty if there was no existing cols and also no new cols were generated
if (existingBaseColEls.length === 0 && colsEl.childNodes.length === 0) {
output = ''
} else {
output = colsEl.toString()
}
return new Handlebars.SafeString(output)
}
const helpers = {
raw,
staticRange,
loop,
r,
c,
cValue,
lazyFormulas,
chartTitleText,
dimension,
sd,
mergeCells,
calcChain,
cols
}
return {
resolveHelper: (helperName, argumentsLength, context, values, options) => {
const targetHelper = helpers[helperName]
if (!targetHelper) {
throw new Error(`Helper "${helperName}" not found`)
}
let validCall
if (targetHelper.dynamicParameters) {
validCall = true
} else {
validCall = targetHelper != null ? argumentsLength === targetHelper.length : false
}
if (!validCall) {
throw new Error(`Invalid usage of _D helper${helperName != null ? ` (t: ${helperName})` : ''}`)
}
try {
if (values.length > 0) {
return targetHelper.call(context, ...values, options)
}
return targetHelper.call(context, options)
} catch (error) {
error.message = `_D t="${helperName}" helper, ${error.message}`
throw error
}
},
assertDataArg: assertOk
}
})()
function _D () {
const values = []
const argsLength = arguments.length
let optionsToUse
if (argsLength > 1) {
optionsToUse = arguments[argsLength - 1]
for (let idx = 0; idx < argsLength - 1; idx++) {
values.push(arguments[idx])
}
} else {
optionsToUse = arguments[0]
}
const type = optionsToUse.hash.t
__xlsxD.assertDataArg(type != null, '_D helper t arg is required')
return __xlsxD.resolveHelper(type, arguments.length, this, values, optionsToUse)
}