@jsreport/jsreport-xlsx
Advanced tools
Comparing version 4.0.0 to 4.0.1
@@ -27,5 +27,5 @@ const office = require('@jsreport/office') | ||
requires: { | ||
core: '3.x.x', | ||
studio: '3.x.x' | ||
core: '4.x.x', | ||
studio: '4.x.x' | ||
} | ||
} |
@@ -22,88 +22,2 @@ const { DOMParser } = require('@xmldom/xmldom') | ||
const outOfLoopItems = new Map() | ||
sheetFile.data = await recursiveStringReplaceAsync( | ||
sheetFile.data.toString(), | ||
'<outOfLoop>', | ||
'</outOfLoop>', | ||
'g', | ||
async (val, content, hasNestedMatch) => { | ||
if (hasNestedMatch) { | ||
return val | ||
} | ||
const doc = new DOMParser().parseFromString(val) | ||
const outOfLoopEl = doc.documentElement | ||
const childNodes = nodeListToArray(outOfLoopEl.childNodes) | ||
const pendingReplacements = [] | ||
let itemIndex | ||
for (const childNode of childNodes) { | ||
if (childNode.nodeName === 'item') { | ||
itemIndex = parseInt(childNode.textContent, 10) | ||
} else if (childNode.nodeName === 'data') { | ||
const dataChildNodes = nodeListToArray(childNode.childNodes) | ||
const generatedEls = [] | ||
for (const dataChildNode of dataChildNodes) { | ||
generatedEls.push(dataChildNode.cloneNode(true)) | ||
} | ||
pendingReplacements.push({ | ||
elements: generatedEls | ||
}) | ||
} | ||
} | ||
if (!outOfLoopItems.has(itemIndex)) { | ||
outOfLoopItems.set(itemIndex, { pendingReplacements: [] }) | ||
} | ||
outOfLoopItems.get(itemIndex).pendingReplacements.push(...pendingReplacements) | ||
return '' | ||
} | ||
) | ||
sheetFile.data = await recursiveStringReplaceAsync( | ||
sheetFile.data.toString(), | ||
'<outOfLoopPlaceholder>', | ||
'</outOfLoopPlaceholder>', | ||
'g', | ||
async (val, content, hasNestedMatch) => { | ||
if (hasNestedMatch) { | ||
return val | ||
} | ||
const doc = new DOMParser().parseFromString(val) | ||
const itemEl = doc.documentElement.firstChild | ||
const itemIndex = parseInt(itemEl.textContent, 10) | ||
const pendingReplacements = outOfLoopItems.get(itemIndex)?.pendingReplacements | ||
if (pendingReplacements == null) { | ||
throw new Error(`outOfLoopPlaceholder can not find metadata with index "${itemIndex}"`) | ||
} | ||
const pendingReplacement = pendingReplacements.shift() | ||
if (pendingReplacement == null) { | ||
throw new Error('outOfLoopPlaceholder does not match with pending elements to replace') | ||
} | ||
const generatedEls = pendingReplacement.elements | ||
let newContent = '' | ||
if (generatedEls == null || generatedEls.length === 0) { | ||
return newContent | ||
} | ||
for (const generatedEl of generatedEls) { | ||
newContent += serializeXml(generatedEl) | ||
} | ||
return newContent | ||
} | ||
) | ||
const updatedCalcChainCountMap = new Map() | ||
@@ -110,0 +24,0 @@ |
@@ -30,324 +30,285 @@ const path = require('path') | ||
const graphicDataEl = drawingDoc.getElementsByTagName('a:graphicData')[0] | ||
// drawing in xlsx are in separate files (not inline), this means that it is possible to | ||
// have multiple charts in a single drawing, | ||
// so we assume there is going to be more than one chart from the drawing. | ||
// this was also validated by verifying the output in Excel by duplicating | ||
// a chart, it always create a drawing with multiple chart definitions. | ||
const graphicDataEls = nodeListToArray(drawingDoc.getElementsByTagName('a:graphicData')) | ||
if ( | ||
graphicDataEl == null | ||
) { | ||
return | ||
} | ||
for (const graphicDataEl of graphicDataEls) { | ||
if ( | ||
graphicDataEl.getAttribute('uri') !== 'http://schemas.openxmlformats.org/drawingml/2006/chart' && | ||
graphicDataEl.getAttribute('uri') !== 'http://schemas.microsoft.com/office/drawing/2014/chartex' | ||
) { | ||
continue | ||
} | ||
if ( | ||
graphicDataEl.getAttribute('uri') !== 'http://schemas.openxmlformats.org/drawingml/2006/chart' && | ||
graphicDataEl.getAttribute('uri') !== 'http://schemas.microsoft.com/office/drawing/2014/chartex' | ||
) { | ||
return | ||
} | ||
const graphicDataChartEl = nodeListToArray(graphicDataEl.childNodes).find((el) => { | ||
let found = false | ||
const graphicDataChartEl = nodeListToArray(graphicDataEl.childNodes).find((el) => { | ||
let found = false | ||
found = ( | ||
el.nodeName === 'c:chart' && | ||
el.getAttribute('xmlns:c') === 'http://schemas.openxmlformats.org/drawingml/2006/chart' && | ||
el.getAttribute('xmlns:r') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' | ||
) | ||
if (!found) { | ||
found = ( | ||
el.nodeName === 'cx:chart' && | ||
el.getAttribute('xmlns:cx') === 'http://schemas.microsoft.com/office/drawing/2014/chartex' && | ||
el.nodeName === 'c:chart' && | ||
el.getAttribute('xmlns:c') === 'http://schemas.openxmlformats.org/drawingml/2006/chart' && | ||
el.getAttribute('xmlns:r') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' | ||
) | ||
} | ||
return found | ||
}) | ||
if (!found) { | ||
found = ( | ||
el.nodeName === 'cx:chart' && | ||
el.getAttribute('xmlns:cx') === 'http://schemas.microsoft.com/office/drawing/2014/chartex' && | ||
el.getAttribute('xmlns:r') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' | ||
) | ||
} | ||
if (graphicDataChartEl == null) { | ||
return | ||
} | ||
return found | ||
}) | ||
const chartRelId = graphicDataChartEl.getAttribute('r:id') | ||
if (graphicDataChartEl == null) { | ||
continue | ||
} | ||
const drawingRelsPath = path.posix.join(path.posix.dirname(drawingPath), '_rels', `${path.posix.basename(drawingPath)}.rels`) | ||
const chartRelId = graphicDataChartEl.getAttribute('r:id') | ||
const drawingRelsDoc = files.find((file) => file.path === drawingRelsPath)?.doc | ||
const drawingRelsPath = path.posix.join(path.posix.dirname(drawingPath), '_rels', `${path.posix.basename(drawingPath)}.rels`) | ||
if (drawingRelsDoc == null) { | ||
return | ||
} | ||
const drawingRelsDoc = files.find((file) => file.path === drawingRelsPath)?.doc | ||
const drawingRelationshipEls = nodeListToArray(drawingRelsDoc.getElementsByTagName('Relationship')) | ||
if (drawingRelsDoc == null) { | ||
continue | ||
} | ||
const chartRelationshipEl = drawingRelationshipEls.find((r) => r.getAttribute('Id') === chartRelId) | ||
const drawingRelationshipEls = nodeListToArray(drawingRelsDoc.getElementsByTagName('Relationship')) | ||
if (chartRelationshipEl == null) { | ||
return | ||
} | ||
const chartRelationshipEl = drawingRelationshipEls.find((r) => r.getAttribute('Id') === chartRelId) | ||
if ( | ||
graphicDataChartEl.prefix === 'cx' && | ||
chartRelationshipEl.getAttribute('Type') !== 'http://schemas.microsoft.com/office/2014/relationships/chartEx' | ||
) { | ||
return | ||
} | ||
if (chartRelationshipEl == null) { | ||
continue | ||
} | ||
if ( | ||
graphicDataChartEl.prefix === 'c' && | ||
chartRelationshipEl.getAttribute('Type') !== 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart' | ||
) { | ||
return | ||
} | ||
if ( | ||
graphicDataChartEl.prefix === 'cx' && | ||
chartRelationshipEl.getAttribute('Type') !== 'http://schemas.microsoft.com/office/2014/relationships/chartEx' | ||
) { | ||
continue | ||
} | ||
const chartPath = path.posix.join(path.posix.dirname(sheetFilepath), chartRelationshipEl.getAttribute('Target')) | ||
const chartFile = files.find((file) => file.path === chartPath) | ||
const chartDoc = chartFile?.doc | ||
if ( | ||
graphicDataChartEl.prefix === 'c' && | ||
chartRelationshipEl.getAttribute('Type') !== 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart' | ||
) { | ||
continue | ||
} | ||
if (chartDoc == null) { | ||
return | ||
} | ||
const chartPath = path.posix.join(path.posix.dirname(sheetFilepath), chartRelationshipEl.getAttribute('Target')) | ||
const chartFile = files.find((file) => file.path === chartPath) | ||
const chartDoc = chartFile?.doc | ||
let chartEl | ||
if (chartDoc == null) { | ||
continue | ||
} | ||
if (graphicDataChartEl.prefix === 'cx') { | ||
chartEl = chartDoc.getElementsByTagName('cx:chart')[0] | ||
} else { | ||
chartEl = chartDoc.getElementsByTagName('c:chart')[0] | ||
} | ||
let chartEl | ||
if (chartEl == null) { | ||
return | ||
} | ||
if (graphicDataChartEl.prefix === 'cx') { | ||
chartEl = chartDoc.getElementsByTagName('cx:chart')[0] | ||
} else { | ||
chartEl = chartDoc.getElementsByTagName('c:chart')[0] | ||
} | ||
const chartTitles = nodeListToArray(chartEl.getElementsByTagName(`${graphicDataChartEl.prefix}:title`)) | ||
const chartMainTitleEl = chartTitles[0] | ||
if (chartEl == null) { | ||
continue | ||
} | ||
if (chartMainTitleEl == null) { | ||
return | ||
} | ||
const chartTitles = nodeListToArray(chartEl.getElementsByTagName(`${graphicDataChartEl.prefix}:title`)) | ||
const chartMainTitleEl = chartTitles[0] | ||
const chartMainTitleTextElements = nodeListToArray(chartMainTitleEl.getElementsByTagName('a:t')) | ||
for (const chartMainTitleTextEl of chartMainTitleTextElements) { | ||
const textContent = chartMainTitleTextEl.textContent | ||
if (!textContent.includes('$xlsxChart')) { | ||
if (chartMainTitleEl == null) { | ||
continue | ||
} | ||
const match = textContent.match(/\$xlsxChart([^$]*)\$/) | ||
const chartConfig = JSON.parse(Buffer.from(match[1], 'base64').toString()) | ||
const chartMainTitleTextElements = nodeListToArray(chartMainTitleEl.getElementsByTagName('a:t')) | ||
// remove chart helper text | ||
chartMainTitleTextEl.textContent = chartMainTitleTextEl.textContent.replace(match[0], '') | ||
for (const chartMainTitleTextEl of chartMainTitleTextElements) { | ||
const textContent = chartMainTitleTextEl.textContent | ||
if (graphicDataChartEl.prefix === 'cx') { | ||
const chartSeriesEl = chartDoc.getElementsByTagName('cx:plotArea')[0].getElementsByTagName('cx:series')[0] | ||
const chartType = chartSeriesEl.getAttribute('layoutId') | ||
const supportedCharts = ['waterfall', 'treemap', 'sunburst', 'funnel', 'clusteredColumn'] | ||
if (!supportedCharts.includes(chartType)) { | ||
throw new Error(`"${chartType}" type (chartEx) is not supported`) | ||
if (!textContent.includes('$xlsxChart')) { | ||
continue | ||
} | ||
const chartDataEl = chartDoc.getElementsByTagName('cx:chartData')[0] | ||
const existingDataItemsElements = nodeListToArray(chartDataEl.getElementsByTagName('cx:data')) | ||
const dataPlaceholderEl = chartDoc.createElement('xlsxChartexDataReplace') | ||
const seriesPlaceholderEl = chartDoc.createElement('xlsxChartexSeriesReplace') | ||
const match = textContent.match(/\$xlsxChart([^$]*)\$/) | ||
const chartConfig = JSON.parse(Buffer.from(match[1], 'base64').toString()) | ||
dataPlaceholderEl.textContent = 'sample' | ||
seriesPlaceholderEl.textContent = 'sample' | ||
// remove chart helper text | ||
chartMainTitleTextEl.textContent = chartMainTitleTextEl.textContent.replace(match[0], '') | ||
chartDataEl.appendChild(dataPlaceholderEl) | ||
chartSeriesEl.parentNode.insertBefore(seriesPlaceholderEl, chartSeriesEl.nextSibling) | ||
if (graphicDataChartEl.prefix === 'cx') { | ||
const chartSeriesEl = chartDoc.getElementsByTagName('cx:plotArea')[0].getElementsByTagName('cx:series')[0] | ||
const chartType = chartSeriesEl.getAttribute('layoutId') | ||
const supportedCharts = ['waterfall', 'treemap', 'sunburst', 'funnel', 'clusteredColumn'] | ||
existingDataItemsElements.forEach((dataItemEl) => { | ||
dataItemEl.parentNode.removeChild(dataItemEl) | ||
}) | ||
if (!supportedCharts.includes(chartType)) { | ||
throw new Error(`"${chartType}" type (chartEx) is not supported`) | ||
} | ||
chartSeriesEl.parentNode.removeChild(chartSeriesEl) | ||
const chartDataEl = chartDoc.getElementsByTagName('cx:chartData')[0] | ||
const existingDataItemsElements = nodeListToArray(chartDataEl.getElementsByTagName('cx:data')) | ||
const dataPlaceholderEl = chartDoc.createElement('xlsxChartexDataReplace') | ||
const seriesPlaceholderEl = chartDoc.createElement('xlsxChartexSeriesReplace') | ||
chartFile.data = serializeXml(chartFile.doc) | ||
chartFile.serializeFromDoc = false | ||
dataPlaceholderEl.textContent = 'sample' | ||
seriesPlaceholderEl.textContent = 'sample' | ||
let newDataItemElement = existingDataItemsElements[0].cloneNode(true) | ||
chartDataEl.appendChild(dataPlaceholderEl) | ||
chartSeriesEl.parentNode.insertBefore(seriesPlaceholderEl, chartSeriesEl.nextSibling) | ||
newDataItemElement.setAttribute('id', 0) | ||
existingDataItemsElements.forEach((dataItemEl) => { | ||
dataItemEl.parentNode.removeChild(dataItemEl) | ||
}) | ||
addChartexItem(chartDoc, { | ||
name: 'cx:strDim', | ||
type: chartType, | ||
data: Array.isArray(chartConfig.data.labels[0]) ? chartConfig.data.labels.map((subLabels) => ({ items: subLabels })) : [{ items: chartConfig.data.labels }] | ||
}, newDataItemElement) | ||
chartSeriesEl.parentNode.removeChild(chartSeriesEl) | ||
addChartexItem(chartDoc, { name: 'cx:numDim', type: chartType, data: [{ items: chartConfig.data.datasets[0].data || [] }] }, newDataItemElement) | ||
chartFile.data = serializeXml(chartFile.doc) | ||
chartFile.serializeFromDoc = false | ||
let newChartSeriesElement = chartSeriesEl.cloneNode(true) | ||
let newDataItemElement = existingDataItemsElements[0].cloneNode(true) | ||
addChartexItem(chartDoc, { name: 'cx:tx', data: chartConfig.data.datasets[0].label || '' }, newChartSeriesElement) | ||
addChartexItem(chartDoc, { name: 'cx:dataId', data: newDataItemElement.getAttribute('id') }, newChartSeriesElement) | ||
newDataItemElement.setAttribute('id', 0) | ||
newDataItemElement = serializeXml(newDataItemElement) | ||
newChartSeriesElement = serializeXml(newChartSeriesElement) | ||
addChartexItem(chartDoc, { | ||
name: 'cx:strDim', | ||
type: chartType, | ||
data: Array.isArray(chartConfig.data.labels[0]) ? chartConfig.data.labels.map((subLabels) => ({ items: subLabels })) : [{ items: chartConfig.data.labels }] | ||
}, newDataItemElement) | ||
chartFile.data = chartFile.data.replace(/<xlsxChartexDataReplace[^>]*>[^]*?(?=<\/xlsxChartexDataReplace>)<\/xlsxChartexDataReplace>/g, newDataItemElement) | ||
chartFile.data = chartFile.data.replace(/<xlsxChartexSeriesReplace[^>]*>[^]*?(?=<\/xlsxChartexSeriesReplace>)<\/xlsxChartexSeriesReplace>/g, newChartSeriesElement) | ||
} else { | ||
const chartPlotAreaEl = chartDoc.getElementsByTagName('c:plotArea')[0] | ||
addChartexItem(chartDoc, { name: 'cx:numDim', type: chartType, data: [{ items: chartConfig.data.datasets[0].data || [] }] }, newDataItemElement) | ||
const supportedCharts = [ | ||
'barChart', 'lineChart', | ||
'stockChart', 'scatterChart', 'bubbleChart' | ||
// 'areaChart', 'area3DChart', 'barChart', 'bar3DChart', 'lineChart', 'line3DChart', | ||
// 'pieChart', 'pie3DChart', 'doughnutChart', 'stockChart', 'scatterChart', 'bubbleChart' | ||
] | ||
let newChartSeriesElement = chartSeriesEl.cloneNode(true) | ||
const existingChartSeriesElements = nodeListToArray(chartDoc.getElementsByTagName('c:ser')) | ||
addChartexItem(chartDoc, { name: 'cx:tx', data: chartConfig.data.datasets[0].label || '' }, newChartSeriesElement) | ||
addChartexItem(chartDoc, { name: 'cx:dataId', data: newDataItemElement.getAttribute('id') }, newChartSeriesElement) | ||
if (existingChartSeriesElements.length === 0) { | ||
throw new Error(`Base chart in xlsx must have at least one data serie defined, ref: "${chartPath}"`) | ||
} | ||
newDataItemElement = serializeXml(newDataItemElement) | ||
newChartSeriesElement = serializeXml(newChartSeriesElement) | ||
if (chartConfig.options != null) { | ||
const existingAxesNodes = [] | ||
chartFile.data = chartFile.data.replace(/<xlsxChartexDataReplace[^>]*>[^]*?(?=<\/xlsxChartexDataReplace>)<\/xlsxChartexDataReplace>/g, newDataItemElement) | ||
chartFile.data = chartFile.data.replace(/<xlsxChartexSeriesReplace[^>]*>[^]*?(?=<\/xlsxChartexSeriesReplace>)<\/xlsxChartexSeriesReplace>/g, newChartSeriesElement) | ||
} else { | ||
const chartPlotAreaEl = chartDoc.getElementsByTagName('c:plotArea')[0] | ||
for (let i = 0; i < chartPlotAreaEl.childNodes.length; i++) { | ||
const currentNode = chartPlotAreaEl.childNodes[i] | ||
const supportedCharts = [ | ||
'barChart', 'lineChart', | ||
'stockChart', 'scatterChart', 'bubbleChart' | ||
// 'areaChart', 'area3DChart', 'barChart', 'bar3DChart', 'lineChart', 'line3DChart', | ||
// 'pieChart', 'pie3DChart', 'doughnutChart', 'stockChart', 'scatterChart', 'bubbleChart' | ||
] | ||
if (currentNode.nodeName === 'c:catAx' || currentNode.nodeName === 'c:valAx') { | ||
existingAxesNodes.push(currentNode) | ||
} | ||
const existingChartSeriesElements = nodeListToArray(chartDoc.getElementsByTagName('c:ser')) | ||
if (existingChartSeriesElements.length === 0) { | ||
throw new Error(`Base chart in xlsx must have at least one data serie defined, ref: "${chartPath}"`) | ||
} | ||
if (chartConfig.options.scales && Array.isArray(chartConfig.options.scales.xAxes) && chartConfig.options.scales.xAxes.length > 0) { | ||
const primaryXAxisConfig = chartConfig.options.scales.xAxes[0] | ||
const secondaryXAxisConfig = chartConfig.options.scales.xAxes[1] | ||
const primaryXAxisEl = existingAxesNodes[0] | ||
const secondaryXAxisEl = existingAxesNodes[3] | ||
if (chartConfig.options != null) { | ||
const existingAxesNodes = [] | ||
if (primaryXAxisConfig && primaryXAxisEl) { | ||
configureAxis(chartDoc, primaryXAxisConfig, primaryXAxisEl) | ||
} | ||
for (let i = 0; i < chartPlotAreaEl.childNodes.length; i++) { | ||
const currentNode = chartPlotAreaEl.childNodes[i] | ||
if (secondaryXAxisConfig && secondaryXAxisEl) { | ||
configureAxis(chartDoc, secondaryXAxisConfig, secondaryXAxisEl) | ||
if (currentNode.nodeName === 'c:catAx' || currentNode.nodeName === 'c:valAx') { | ||
existingAxesNodes.push(currentNode) | ||
} | ||
} | ||
} | ||
if (chartConfig.options.scales && Array.isArray(chartConfig.options.scales.yAxes) && chartConfig.options.scales.yAxes.length > 0) { | ||
const primaryYAxisConfig = chartConfig.options.scales.yAxes[0] | ||
const secondaryYAxisConfig = chartConfig.options.scales.yAxes[1] | ||
const primaryYAxisEl = existingAxesNodes[1] | ||
const secondaryYAxisEl = existingAxesNodes[2] | ||
if (chartConfig.options.scales && Array.isArray(chartConfig.options.scales.xAxes) && chartConfig.options.scales.xAxes.length > 0) { | ||
const primaryXAxisConfig = chartConfig.options.scales.xAxes[0] | ||
const secondaryXAxisConfig = chartConfig.options.scales.xAxes[1] | ||
const primaryXAxisEl = existingAxesNodes[0] | ||
const secondaryXAxisEl = existingAxesNodes[3] | ||
if (primaryYAxisConfig && primaryYAxisEl) { | ||
configureAxis(chartDoc, primaryYAxisConfig, primaryYAxisEl) | ||
} | ||
if (primaryXAxisConfig && primaryXAxisEl) { | ||
configureAxis(chartDoc, primaryXAxisConfig, primaryXAxisEl) | ||
} | ||
if (secondaryYAxisConfig && secondaryYAxisEl) { | ||
configureAxis(chartDoc, secondaryYAxisConfig, secondaryYAxisEl) | ||
if (secondaryXAxisConfig && secondaryXAxisEl) { | ||
configureAxis(chartDoc, secondaryXAxisConfig, secondaryXAxisEl) | ||
} | ||
} | ||
} | ||
// NOTE: option "storeDataInSheet" not supported for now | ||
// it requires to complete the implementation to put | ||
// cell references in chart series data | ||
delete chartConfig.options.storeDataInSheet | ||
if (chartConfig.options.scales && Array.isArray(chartConfig.options.scales.yAxes) && chartConfig.options.scales.yAxes.length > 0) { | ||
const primaryYAxisConfig = chartConfig.options.scales.yAxes[0] | ||
const secondaryYAxisConfig = chartConfig.options.scales.yAxes[1] | ||
const primaryYAxisEl = existingAxesNodes[1] | ||
const secondaryYAxisEl = existingAxesNodes[2] | ||
if (chartConfig.options.storeDataInSheet === true) { | ||
// creating new sheet to store the data for the chart | ||
const newSheetInfo = getNewSheet(files) | ||
if (primaryYAxisConfig && primaryYAxisEl) { | ||
configureAxis(chartDoc, primaryYAxisConfig, primaryYAxisEl) | ||
} | ||
// transform the chart data to the order in which the sheet data | ||
// expects to be | ||
const sheetData = [ | ||
[null, ...chartConfig.data.datasets.map((d) => d.label)], | ||
...chartConfig.data.labels.map((label, idx) => { | ||
const arr = [label] | ||
if (secondaryYAxisConfig && secondaryYAxisEl) { | ||
configureAxis(chartDoc, secondaryYAxisConfig, secondaryYAxisEl) | ||
} | ||
} | ||
for (const dataset of chartConfig.data.datasets) { | ||
const datasetVal = dataset.data[idx] | ||
// NOTE: option "storeDataInSheet" not supported for now | ||
// it requires to complete the implementation to put | ||
// cell references in chart series data | ||
delete chartConfig.options.storeDataInSheet | ||
if (datasetVal != null) { | ||
arr.push(datasetVal) | ||
} else { | ||
arr.push(null) | ||
} | ||
} | ||
if (chartConfig.options.storeDataInSheet === true) { | ||
// creating new sheet to store the data for the chart | ||
const newSheetInfo = getNewSheet(files) | ||
return arr | ||
}) | ||
] | ||
// transform the chart data to the order in which the sheet data | ||
// expects to be | ||
const sheetData = [ | ||
[null, ...chartConfig.data.datasets.map((d) => d.label)], | ||
...chartConfig.data.labels.map((label, idx) => { | ||
const arr = [label] | ||
addDataToSheet(newSheetInfo, sheetData) | ||
addNewSheetToWorkbook(newSheetInfo, files) | ||
} | ||
} | ||
for (const dataset of chartConfig.data.datasets) { | ||
const datasetVal = dataset.data[idx] | ||
const lastExistingChartSerieEl = existingChartSeriesElements[existingChartSeriesElements.length - 1] | ||
let lastChartTypeContentEl | ||
if (datasetVal != null) { | ||
arr.push(datasetVal) | ||
} else { | ||
arr.push(null) | ||
} | ||
} | ||
for (const [serieIdx, serieEl] of existingChartSeriesElements.entries()) { | ||
const chartTypeContentEl = serieEl.parentNode | ||
const chartType = chartTypeContentEl.localName | ||
return arr | ||
}) | ||
] | ||
lastChartTypeContentEl = chartTypeContentEl | ||
if (!supportedCharts.includes(chartType)) { | ||
throw new Error(`Chart "${chartType}" type is not supported, ref: "${chartPath}"`) | ||
addDataToSheet(newSheetInfo, sheetData) | ||
addNewSheetToWorkbook(newSheetInfo, files) | ||
} | ||
} | ||
const refNode = serieEl.nextSibling | ||
const lastExistingChartSerieEl = existingChartSeriesElements[existingChartSeriesElements.length - 1] | ||
let lastChartTypeContentEl | ||
serieEl.parentNode.removeChild(serieEl) | ||
for (const [serieIdx, serieEl] of existingChartSeriesElements.entries()) { | ||
const chartTypeContentEl = serieEl.parentNode | ||
const chartType = chartTypeContentEl.localName | ||
const currentDataset = chartConfig.data.datasets[serieIdx] | ||
lastChartTypeContentEl = chartTypeContentEl | ||
if (!currentDataset) { | ||
continue | ||
} | ||
if (!supportedCharts.includes(chartType)) { | ||
throw new Error(`Chart "${chartType}" type is not supported, ref: "${chartPath}"`) | ||
} | ||
const newChartSerieNode = serieEl.cloneNode(true) | ||
const refNode = serieEl.nextSibling | ||
prepareChartSerie(chartDoc, chartType, newChartSerieNode, { | ||
serieIdx, | ||
serieLabel: currentDataset.label, | ||
generalLabels: chartConfig.data.labels, | ||
dataErrors: currentDataset.dataErrors, | ||
dataLabels: currentDataset.dataLabels, | ||
dataValues: currentDataset.data | ||
}) | ||
serieEl.parentNode.removeChild(serieEl) | ||
refNode.parentNode.insertBefore(newChartSerieNode, refNode) | ||
} | ||
const currentDataset = chartConfig.data.datasets[serieIdx] | ||
if (chartConfig.data.datasets.length > existingChartSeriesElements.length) { | ||
const lastSerieIdx = existingChartSeriesElements.length - 1 | ||
const seriesInLastChartNodes = nodeListToArray(lastChartTypeContentEl.getElementsByTagName('c:ser')) | ||
const chartType = lastChartTypeContentEl.localName | ||
const remainingDatasets = chartConfig.data.datasets.slice(existingChartSeriesElements.length) | ||
const refNode = seriesInLastChartNodes[seriesInLastChartNodes.length - 1].nextSibling | ||
for (const [remainingIdx, currentDataset] of remainingDatasets.entries()) { | ||
// create based on the last serie, but without predefined shape properties | ||
const newChartSerieNode = lastExistingChartSerieEl.cloneNode(true) | ||
const shapePropertiesEl = findChildNode('c:spPr', newChartSerieNode) | ||
if (shapePropertiesEl) { | ||
shapePropertiesEl.parentNode.removeChild(shapePropertiesEl) | ||
if (!currentDataset) { | ||
continue | ||
} | ||
const markerEl = findChildNode('c:marker', newChartSerieNode) | ||
const newChartSerieNode = serieEl.cloneNode(true) | ||
if (markerEl) { | ||
const symbolEl = findChildNode('c:symbol', markerEl) | ||
if (symbolEl && symbolEl.getAttribute('val') !== 'none') { | ||
symbolEl.setAttribute('val', 'none') | ||
} | ||
} | ||
prepareChartSerie(chartDoc, chartType, newChartSerieNode, { | ||
serieIdx: lastSerieIdx + remainingIdx + 1, | ||
serieIdx, | ||
serieLabel: currentDataset.label, | ||
@@ -362,6 +323,46 @@ generalLabels: chartConfig.data.labels, | ||
} | ||
if (chartConfig.data.datasets.length > existingChartSeriesElements.length) { | ||
const lastSerieIdx = existingChartSeriesElements.length - 1 | ||
const seriesInLastChartNodes = nodeListToArray(lastChartTypeContentEl.getElementsByTagName('c:ser')) | ||
const chartType = lastChartTypeContentEl.localName | ||
const remainingDatasets = chartConfig.data.datasets.slice(existingChartSeriesElements.length) | ||
const refNode = seriesInLastChartNodes[seriesInLastChartNodes.length - 1].nextSibling | ||
for (const [remainingIdx, currentDataset] of remainingDatasets.entries()) { | ||
// create based on the last serie, but without predefined shape properties | ||
const newChartSerieNode = lastExistingChartSerieEl.cloneNode(true) | ||
const shapePropertiesEl = findChildNode('c:spPr', newChartSerieNode) | ||
if (shapePropertiesEl) { | ||
shapePropertiesEl.parentNode.removeChild(shapePropertiesEl) | ||
} | ||
const markerEl = findChildNode('c:marker', newChartSerieNode) | ||
if (markerEl) { | ||
const symbolEl = findChildNode('c:symbol', markerEl) | ||
if (symbolEl && symbolEl.getAttribute('val') !== 'none') { | ||
symbolEl.setAttribute('val', 'none') | ||
} | ||
} | ||
prepareChartSerie(chartDoc, chartType, newChartSerieNode, { | ||
serieIdx: lastSerieIdx + remainingIdx + 1, | ||
serieLabel: currentDataset.label, | ||
generalLabels: chartConfig.data.labels, | ||
dataErrors: currentDataset.dataErrors, | ||
dataLabels: currentDataset.dataLabels, | ||
dataValues: currentDataset.data | ||
}) | ||
refNode.parentNode.insertBefore(newChartSerieNode, refNode) | ||
} | ||
} | ||
chartFile.data = serializeXml(chartFile.doc) | ||
chartFile.serializeFromDoc = false | ||
} | ||
chartFile.data = serializeXml(chartFile.doc) | ||
chartFile.serializeFromDoc = false | ||
} | ||
@@ -368,0 +369,0 @@ } |
const path = require('path') | ||
const { num2col } = require('xlsx-coordinates') | ||
const { nodeListToArray, isWorksheetFile, isWorksheetRelsFile, getClosestEl, getSheetInfo } = require('../../utils') | ||
const { nodeListToArray, isWorksheetFile, isWorksheetRelsFile, getSheetInfo } = require('../../utils') | ||
const { parseCellRef, evaluateCellRefsFromExpression } = require('../../cellUtils') | ||
@@ -490,5 +490,6 @@ const startLoopRegexp = /{{#each ([^{}]{0,500})}}/ | ||
const match = matches[0] | ||
const shouldEscape = !match[0].startsWith('{{{') | ||
const expressionValue = match[2] | ||
cellValueWrapperEl.textContent = `{{#xlsxSData type='cellValue' value=${expressionValue.includes(' ') ? `(${expressionValue})` : expressionValue}` | ||
cellValueWrapperEl.textContent = `{{#xlsxSData type='cellValue' value=${expressionValue.includes(' ') ? `(${expressionValue})` : expressionValue}${shouldEscape ? ' escape=true' : ''}` | ||
} else { | ||
@@ -538,14 +539,10 @@ cellValueWrapperEl.textContent = "{{#xlsxSData type='cellValue'" | ||
for (const { loopDetected, startingRowEl, endingRowEl, types } of outOfLoopElsToHandle) { | ||
const sortedOutOfLoopElsToHandle = getOutOfLoopElsSortedByHierarchy(outOfLoopElsToHandle) | ||
for (const { loopDetected, startingRowEl, endingRowEl, types } of sortedOutOfLoopElsToHandle) { | ||
const loopLevel = loopDetected.hierarchyId.split('#').length | ||
const isOuterLevel = loopLevel === 1 | ||
if (loopDetected.type === 'row' && loopDetected.children.length !== 0) { | ||
throw new Error('Multiple nested row loops are not supported') | ||
} | ||
for (const type of types) { | ||
const outOfLoopElement = sheetDoc.createElement('outOfLoop') | ||
let itemEl = sheetDoc.createElement('item') | ||
itemEl.textContent = outLoopItemIndex | ||
const outOfLoopEl = sheetDoc.createElement('outOfLoop') | ||
@@ -559,3 +556,2 @@ const rowHandlebarsWrapperText = type === 'left' ? startingRowEl.previousSibling.textContent : endingRowEl.previousSibling.textContent | ||
toCloneEls.push(...getCellElsAndWrappersFrom(currentEl, 'previous')) | ||
toCloneEls.reverse() | ||
@@ -568,46 +564,30 @@ } else { | ||
const newEl = toCloneEl.cloneNode(true) | ||
outOfLoopElement.appendChild(newEl) | ||
outOfLoopEl.appendChild(newEl) | ||
toCloneEl.parentNode.removeChild(toCloneEl) | ||
} | ||
processOpeningTag(sheetDoc, outOfLoopElement.firstChild, rowHandlebarsWrapperText) | ||
processClosingTag(sheetDoc, outOfLoopElement.lastChild, '{{/xlsxSData}}') | ||
processOpeningTag(sheetDoc, outOfLoopEl.firstChild, rowHandlebarsWrapperText) | ||
processClosingTag(sheetDoc, outOfLoopEl.lastChild, '{{/xlsxSData}}') | ||
outOfLoopElement.insertBefore(itemEl, outOfLoopElement.firstChild) | ||
processOpeningTag(sheetDoc, outOfLoopEl.firstChild, `{{#xlsxSData type='outOfLoop' item='${outLoopItemIndex}' }}`) | ||
processClosingTag(sheetDoc, outOfLoopEl.lastChild, '{{/xlsxSData}}') | ||
processOpeningTag(sheetDoc, outOfLoopElement.firstChild.nextSibling, `{{#xlsxSData type='outOfLoop' item='${outLoopItemIndex}' }}`) | ||
processClosingTag(sheetDoc, outOfLoopElement.lastChild, '{{/xlsxSData}}') | ||
const loopEdgeEl = loopDetected.blockStartEl | ||
const loopEdgeEl = loopDetected.blockEndEl | ||
const outOfLoopItemContentEls = nodeListToArray(outOfLoopEl.childNodes) | ||
// we get the last outOfLoop element | ||
const lastOutOfLoopElOrNull = getClosestEl(loopEdgeEl, (n) => ( | ||
n.nodeName !== 'outOfLoop' || | ||
(n.nodeName === 'outOfLoop' && n.nextSibling?.nodeName !== 'outOfLoop') | ||
), 'next') | ||
for (const contentEl of outOfLoopItemContentEls) { | ||
loopEdgeEl.parentNode.insertBefore( | ||
contentEl, | ||
loopEdgeEl | ||
) | ||
} | ||
loopEdgeEl.parentNode.insertBefore( | ||
outOfLoopElement, | ||
lastOutOfLoopElOrNull?.nodeName === 'outOfLoop' ? lastOutOfLoopElOrNull?.nextSibling : lastOutOfLoopElOrNull | ||
) | ||
const outOfLoopPlaceholderEl = sheetDoc.createElement('outOfLoopPlaceholder') | ||
const outOfLoopPlaceholderElement = sheetDoc.createElement('outOfLoopPlaceholder') | ||
itemEl = sheetDoc.createElement('item') | ||
itemEl.textContent = outLoopItemIndex | ||
const contentEl = sheetDoc.createElement('xlsxRemove') | ||
contentEl.textContent = `{{xlsxSData type='outOfLoopPlaceholder' item='${outLoopItemIndex}' }}` | ||
outOfLoopPlaceholderElement.appendChild(itemEl) | ||
outOfLoopPlaceholderEl.appendChild(contentEl) | ||
processOpeningTag(sheetDoc, outOfLoopPlaceholderElement.firstChild, `{{#xlsxSData type='outOfLoopPlaceholder' item='${outLoopItemIndex}' }}`) | ||
processClosingTag(sheetDoc, outOfLoopPlaceholderElement.lastChild, '{{/xlsxSData}}') | ||
if (type === 'left') { | ||
const cellAndWrappers = getCellElAndWrappers(loopDetected.start.el) | ||
loopDetected.start.el.parentNode.insertBefore(outOfLoopPlaceholderElement, cellAndWrappers[0]) | ||
} else { | ||
const cellAndWrappers = getCellElAndWrappers(loopDetected.end.el) | ||
loopDetected.end.el.parentNode.insertBefore(outOfLoopPlaceholderElement, cellAndWrappers[cellAndWrappers.length - 1].nextSibling) | ||
} | ||
outLoopItemIndex++ | ||
// we only want to conditionally render the outOfLoopPlaceholder items if the loop | ||
@@ -617,5 +597,21 @@ // is at the outer level | ||
// we include a if condition to preserve the cells that are before/after the each | ||
processOpeningTag(sheetDoc, outOfLoopPlaceholderElement, `{{#if ${loopDetected.type === 'block' && type === 'right' ? '@last' : '@first'}}}`) | ||
processClosingTag(sheetDoc, outOfLoopPlaceholderElement, '{{/if}}') | ||
processOpeningTag(sheetDoc, outOfLoopPlaceholderEl.firstChild, `{{#if ${loopDetected.type === 'block' && type === 'right' ? '@last' : '@first'}}}`) | ||
processClosingTag(sheetDoc, outOfLoopPlaceholderEl.lastChild, '{{/if}}') | ||
} | ||
const outOfLoopPlaceholderContentEls = nodeListToArray(outOfLoopPlaceholderEl.childNodes) | ||
const cellAndWrappers = getCellElAndWrappers(type === 'left' ? loopDetected.start.el : loopDetected.end.el) | ||
if (type === 'right') { | ||
outOfLoopPlaceholderContentEls.reverse() | ||
} | ||
for (const contentEl of outOfLoopPlaceholderContentEls) { | ||
if (type === 'left') { | ||
loopDetected.start.el.parentNode.insertBefore(contentEl, cellAndWrappers[0]) | ||
} else { | ||
loopDetected.end.el.parentNode.insertBefore(contentEl, cellAndWrappers[cellAndWrappers.length - 1].nextSibling) | ||
} | ||
} | ||
outLoopItemIndex++ | ||
} | ||
@@ -766,2 +762,67 @@ } | ||
function getOutOfLoopElsSortedByHierarchy (outOfLoopElsToHandle) { | ||
const hierarchy = new Map() | ||
const hierarchyIdElMap = new Map() | ||
for (let idx = 0; idx < outOfLoopElsToHandle.length; idx++) { | ||
const outOfLoopElToHandle = outOfLoopElsToHandle[idx] | ||
const hierarchyId = outOfLoopElToHandle.loopDetected.hierarchyId | ||
hierarchyIdElMap.set(hierarchyId, outOfLoopElToHandle) | ||
const parts = hierarchyId.split('#') | ||
let currentHierarchy = hierarchy | ||
for (let partIdx = 0; partIdx < parts.length; partIdx++) { | ||
const occurrenceIdx = parseInt(parts[partIdx], 10) | ||
const isLast = partIdx === parts.length - 1 | ||
if (!currentHierarchy.has(occurrenceIdx)) { | ||
currentHierarchy.set(occurrenceIdx, { | ||
children: new Map() | ||
}) | ||
} | ||
const currentItem = currentHierarchy.get(occurrenceIdx) | ||
if (isLast) { | ||
currentItem.match = hierarchyId | ||
} | ||
currentHierarchy = currentItem.children | ||
} | ||
} | ||
const toArraySortedByOccurrence = (targetMap) => { | ||
const sortedKeysByOccurrence = [...targetMap.keys()].sort((a, b) => a - b) | ||
return sortedKeysByOccurrence.map((key) => targetMap.get(key)) | ||
} | ||
const pending = toArraySortedByOccurrence(hierarchy) | ||
const sortedHierarchyIds = [] | ||
while (pending.length > 0) { | ||
const currentPending = pending.shift() | ||
if (typeof currentPending === 'string') { | ||
sortedHierarchyIds.push(currentPending) | ||
continue | ||
} | ||
const item = currentPending | ||
if (item.children.size > 0) { | ||
pending.unshift(...toArraySortedByOccurrence(item.children)) | ||
} | ||
if (item.match != null) { | ||
pending.unshift(item.match) | ||
} | ||
} | ||
const result = sortedHierarchyIds.map((hierarchyId) => hierarchyIdElMap.get(hierarchyId)) | ||
return result | ||
} | ||
function getLatestNotClosedLoop (loopsDetected) { | ||
@@ -768,0 +829,0 @@ let loopFound |
@@ -23,135 +23,136 @@ const path = require('path') | ||
const graphicDataEl = drawingDoc.getElementsByTagName('a:graphicData')[0] | ||
// drawing in xlsx are in separate files (not inline), this means that it is possible to | ||
// have multiple charts in a single drawing, | ||
// so we assume there is going to be more than one chart from the drawing. | ||
// this was also validated by verifying the output in Excel by duplicating | ||
// a chart, it always create a drawing with multiple chart definitions. | ||
const graphicDataEls = nodeListToArray(drawingDoc.getElementsByTagName('a:graphicData')) | ||
if ( | ||
graphicDataEl == null | ||
) { | ||
return | ||
} | ||
for (const graphicDataEl of graphicDataEls) { | ||
if ( | ||
graphicDataEl.getAttribute('uri') !== 'http://schemas.openxmlformats.org/drawingml/2006/chart' && | ||
graphicDataEl.getAttribute('uri') !== 'http://schemas.microsoft.com/office/drawing/2014/chartex' | ||
) { | ||
continue | ||
} | ||
if ( | ||
graphicDataEl.getAttribute('uri') !== 'http://schemas.openxmlformats.org/drawingml/2006/chart' && | ||
graphicDataEl.getAttribute('uri') !== 'http://schemas.microsoft.com/office/drawing/2014/chartex' | ||
) { | ||
return | ||
} | ||
const graphicDataChartEl = nodeListToArray(graphicDataEl.childNodes).find((el) => { | ||
let found = false | ||
const graphicDataChartEl = nodeListToArray(graphicDataEl.childNodes).find((el) => { | ||
let found = false | ||
found = ( | ||
el.nodeName === 'c:chart' && | ||
el.getAttribute('xmlns:c') === 'http://schemas.openxmlformats.org/drawingml/2006/chart' && | ||
el.getAttribute('xmlns:r') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' | ||
) | ||
if (!found) { | ||
found = ( | ||
el.nodeName === 'cx:chart' && | ||
el.getAttribute('xmlns:cx') === 'http://schemas.microsoft.com/office/drawing/2014/chartex' && | ||
el.nodeName === 'c:chart' && | ||
el.getAttribute('xmlns:c') === 'http://schemas.openxmlformats.org/drawingml/2006/chart' && | ||
el.getAttribute('xmlns:r') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' | ||
) | ||
} | ||
return found | ||
}) | ||
if (!found) { | ||
found = ( | ||
el.nodeName === 'cx:chart' && | ||
el.getAttribute('xmlns:cx') === 'http://schemas.microsoft.com/office/drawing/2014/chartex' && | ||
el.getAttribute('xmlns:r') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' | ||
) | ||
} | ||
if (graphicDataChartEl == null) { | ||
return | ||
} | ||
return found | ||
}) | ||
const chartRelId = graphicDataChartEl.getAttribute('r:id') | ||
if (graphicDataChartEl == null) { | ||
continue | ||
} | ||
const drawingRelsPath = path.posix.join(path.posix.dirname(drawingPath), '_rels', `${path.posix.basename(drawingPath)}.rels`) | ||
const chartRelId = graphicDataChartEl.getAttribute('r:id') | ||
const drawingRelsDoc = files.find((file) => file.path === drawingRelsPath)?.doc | ||
const drawingRelsPath = path.posix.join(path.posix.dirname(drawingPath), '_rels', `${path.posix.basename(drawingPath)}.rels`) | ||
if (drawingRelsDoc == null) { | ||
return | ||
} | ||
const drawingRelsDoc = files.find((file) => file.path === drawingRelsPath)?.doc | ||
const drawingRelationshipEls = nodeListToArray(drawingRelsDoc.getElementsByTagName('Relationship')) | ||
if (drawingRelsDoc == null) { | ||
continue | ||
} | ||
const chartRelationshipEl = drawingRelationshipEls.find((r) => r.getAttribute('Id') === chartRelId) | ||
const drawingRelationshipEls = nodeListToArray(drawingRelsDoc.getElementsByTagName('Relationship')) | ||
if (chartRelationshipEl == null) { | ||
return | ||
} | ||
const chartRelationshipEl = drawingRelationshipEls.find((r) => r.getAttribute('Id') === chartRelId) | ||
if ( | ||
graphicDataChartEl.prefix === 'cx' && | ||
chartRelationshipEl.getAttribute('Type') !== 'http://schemas.microsoft.com/office/2014/relationships/chartEx' | ||
) { | ||
return | ||
} | ||
if (chartRelationshipEl == null) { | ||
continue | ||
} | ||
if ( | ||
graphicDataChartEl.prefix === 'c' && | ||
chartRelationshipEl.getAttribute('Type') !== 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart' | ||
) { | ||
return | ||
} | ||
if ( | ||
graphicDataChartEl.prefix === 'cx' && | ||
chartRelationshipEl.getAttribute('Type') !== 'http://schemas.microsoft.com/office/2014/relationships/chartEx' | ||
) { | ||
continue | ||
} | ||
const chartPath = path.posix.join(path.posix.dirname(sheetFilepath), chartRelationshipEl.getAttribute('Target')) | ||
if ( | ||
graphicDataChartEl.prefix === 'c' && | ||
chartRelationshipEl.getAttribute('Type') !== 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart' | ||
) { | ||
continue | ||
} | ||
const chartDoc = files.find((file) => file.path === chartPath)?.doc | ||
const chartPath = path.posix.join(path.posix.dirname(sheetFilepath), chartRelationshipEl.getAttribute('Target')) | ||
if (chartDoc == null) { | ||
return | ||
} | ||
const chartDoc = files.find((file) => file.path === chartPath)?.doc | ||
let chartEl | ||
if (chartDoc == null) { | ||
continue | ||
} | ||
if (graphicDataChartEl.prefix === 'cx') { | ||
chartEl = chartDoc.getElementsByTagName('cx:chart')[0] | ||
} else { | ||
chartEl = chartDoc.getElementsByTagName('c:chart')[0] | ||
} | ||
let chartEl | ||
if (chartEl == null) { | ||
return | ||
} | ||
if (graphicDataChartEl.prefix === 'cx') { | ||
chartEl = chartDoc.getElementsByTagName('cx:chart')[0] | ||
} else { | ||
chartEl = chartDoc.getElementsByTagName('c:chart')[0] | ||
} | ||
// ensuring the cached strings in xlsx are not processed by handlebars, because it will | ||
// give errors otherwise | ||
if (graphicDataChartEl.prefix === 'c') { | ||
// it seems only the standard charts "c:" cache data in the chart definition, | ||
// for the chartex it is not needed that we do something | ||
const existingChartSeriesElements = nodeListToArray(chartDoc.getElementsByTagName('c:ser')) | ||
if (chartEl == null) { | ||
continue | ||
} | ||
for (const chartSerieEl of existingChartSeriesElements) { | ||
const tagsToCheck = ['c:tx', 'c:cat', 'c:val', 'c:xVal', 'c:yVal', 'c:bubbleSize'] | ||
// ensuring the cached strings in xlsx are not processed by handlebars, because it will | ||
// give errors otherwise | ||
if (graphicDataChartEl.prefix === 'c') { | ||
// it seems only the standard charts "c:" cache data in the chart definition, | ||
// for the chartex it is not needed that we do something | ||
const existingChartSeriesElements = nodeListToArray(chartDoc.getElementsByTagName('c:ser')) | ||
for (const targetTag of tagsToCheck) { | ||
const existingTagEl = findChildNode(targetTag, chartSerieEl) | ||
for (const chartSerieEl of existingChartSeriesElements) { | ||
const tagsToCheck = ['c:tx', 'c:cat', 'c:val', 'c:xVal', 'c:yVal', 'c:bubbleSize'] | ||
if (existingTagEl == null) { | ||
continue | ||
} | ||
for (const targetTag of tagsToCheck) { | ||
const existingTagEl = findChildNode(targetTag, chartSerieEl) | ||
const strRefEl = findChildNode('c:strRef', existingTagEl) | ||
const numRefEl = findChildNode('c:numRef', existingTagEl) | ||
for (const targetInfo of [{ el: strRefEl, cacheTag: 'strCache' }, { el: numRefEl, cacheTag: 'numCache' }]) { | ||
if (targetInfo.el == null) { | ||
if (existingTagEl == null) { | ||
continue | ||
} | ||
const cacheEl = findChildNode(`c:${targetInfo.cacheTag}`, targetInfo.el) | ||
const strRefEl = findChildNode('c:strRef', existingTagEl) | ||
const numRefEl = findChildNode('c:numRef', existingTagEl) | ||
if (cacheEl == null) { | ||
continue | ||
} | ||
for (const targetInfo of [{ el: strRefEl, cacheTag: 'strCache' }, { el: numRefEl, cacheTag: 'numCache' }]) { | ||
if (targetInfo.el == null) { | ||
continue | ||
} | ||
const ptEls = findChildNode('c:pt', cacheEl, true) | ||
const cacheEl = findChildNode(`c:${targetInfo.cacheTag}`, targetInfo.el) | ||
for (const ptEl of ptEls) { | ||
const ptValueEl = findChildNode('c:v', ptEl) | ||
if (ptValueEl == null) { | ||
if (cacheEl == null) { | ||
continue | ||
} | ||
if (ptValueEl.textContent.includes('{{') && ptValueEl.textContent.includes('}}')) { | ||
ptValueEl.textContent = `{{{{xlsxSData type='raw'}}}}${ptValueEl.textContent}{{{{/xlsxSData}}}}` | ||
const ptEls = findChildNode('c:pt', cacheEl, true) | ||
for (const ptEl of ptEls) { | ||
const ptValueEl = findChildNode('c:v', ptEl) | ||
if (ptValueEl == null) { | ||
continue | ||
} | ||
if (ptValueEl.textContent.includes('{{') && ptValueEl.textContent.includes('}}')) { | ||
ptValueEl.textContent = `{{{{xlsxSData type='raw'}}}}${ptValueEl.textContent}{{{{/xlsxSData}}}}` | ||
} | ||
} | ||
@@ -162,20 +163,20 @@ } | ||
} | ||
} | ||
const chartTitles = nodeListToArray(chartEl.getElementsByTagName(`${graphicDataChartEl.prefix}:title`)) | ||
const chartMainTitleEl = chartTitles[0] | ||
const chartTitles = nodeListToArray(chartEl.getElementsByTagName(`${graphicDataChartEl.prefix}:title`)) | ||
const chartMainTitleEl = chartTitles[0] | ||
if (chartMainTitleEl == null) { | ||
return | ||
} | ||
if (chartMainTitleEl == null) { | ||
continue | ||
} | ||
if (graphicDataChartEl.prefix === 'cx') { | ||
const chartMainTitleTxEl = nodeListToArray(chartMainTitleEl.childNodes).find((el) => el.nodeName === 'cx:tx') | ||
const chartMainTitleTxDataEl = chartMainTitleTxEl != null ? nodeListToArray(chartMainTitleTxEl.childNodes).find((el) => el.nodeName === 'cx:txData') : undefined | ||
const chartMainTitleTxDataValueEl = chartMainTitleTxDataEl != null ? nodeListToArray(chartMainTitleTxDataEl.childNodes).find((el) => el.nodeName === 'cx:v') : undefined | ||
if (graphicDataChartEl.prefix === 'cx') { | ||
const chartMainTitleTxEl = nodeListToArray(chartMainTitleEl.childNodes).find((el) => el.nodeName === 'cx:tx') | ||
const chartMainTitleTxDataEl = chartMainTitleTxEl != null ? nodeListToArray(chartMainTitleTxEl.childNodes).find((el) => el.nodeName === 'cx:txData') : undefined | ||
const chartMainTitleTxDataValueEl = chartMainTitleTxDataEl != null ? nodeListToArray(chartMainTitleTxDataEl.childNodes).find((el) => el.nodeName === 'cx:v') : undefined | ||
if (chartMainTitleTxDataValueEl?.textContent.startsWith('{{xlsxChart')) { | ||
chartMainTitleTxDataValueEl.textContent = '' | ||
if (chartMainTitleTxDataValueEl?.textContent.startsWith('{{xlsxChart')) { | ||
chartMainTitleTxDataValueEl.textContent = '' | ||
} | ||
} | ||
} | ||
} |
const { DOMParser, XMLSerializer } = require('@xmldom/xmldom') | ||
const decodeXML = require('unescape') | ||
const { decode } = require('html-entities') | ||
const { decompress, saveXmlsToOfficeFile } = require('@jsreport/office') | ||
@@ -8,2 +8,4 @@ const preprocess = require('./preprocess/preprocess') | ||
const decodeXML = (str) => decode(str, { level: 'xml' }) | ||
module.exports = (reporter) => async (inputs, req) => { | ||
@@ -37,5 +39,16 @@ const { xlsxTemplateContent, options, outputPath } = inputs | ||
const xmlStr = new XMLSerializer().serializeToString(f.doc, undefined, (node) => { | ||
// 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) | ||
return str.replace(/{{#?>/g, (m) => { | ||
return decodeXML(m) | ||
}) | ||
} | ||
@@ -60,3 +73,4 @@ | ||
// we remove NUL unicode characters, which is the only character that is illegal in XML | ||
// we remove NUL, VERTICAL TAB unicode characters, which are characters that are illegal in XML. | ||
// NOTE: we should likely find a way to remove illegal characters more generally, using some kind of unicode ranges | ||
// eslint-disable-next-line no-control-regex | ||
@@ -63,0 +77,0 @@ const contents = newContent.toString().replace(/\u0000|\u000b/g, '').split('$$$xlsxFile$$$') |
{ | ||
"name": "@jsreport/jsreport-xlsx", | ||
"version": "4.0.0", | ||
"version": "4.0.1", | ||
"description": "jsreport recipe rendering excels directly from open xml", | ||
@@ -34,2 +34,3 @@ "keywords": [ | ||
"@xmldom/xmldom": "0.8.6", | ||
"html-entities": "2.4.0", | ||
"js-excel-date-convert": "1.0.2", | ||
@@ -44,3 +45,2 @@ "lodash": "4.17.21", | ||
"string-replace-async": "2.0.0", | ||
"unescape": "1.0.1", | ||
"uuid": "8.3.2", | ||
@@ -51,9 +51,9 @@ "xlsx-coordinates": "1.0.1", | ||
"devDependencies": { | ||
"@jsreport/jsreport-assets": "4.0.0", | ||
"@jsreport/jsreport-assets": "4.0.1", | ||
"@jsreport/jsreport-components": "4.0.0", | ||
"@jsreport/jsreport-core": "4.0.0", | ||
"@jsreport/jsreport-core": "4.0.1", | ||
"@jsreport/jsreport-data": "4.0.0", | ||
"@jsreport/jsreport-handlebars": "4.0.0", | ||
"@jsreport/jsreport-jsrender": "4.0.0", | ||
"@jsreport/studio-dev": "3.2.1", | ||
"@jsreport/studio-dev": "4.0.0", | ||
"cross-env": "6.0.3", | ||
@@ -60,0 +60,0 @@ "handlebars": "4.7.7", |
@@ -10,2 +10,9 @@ # @jsreport/jsreport-xlsx | ||
### 4.0.1 | ||
- fix nested loops with closing tags on single line | ||
- fix xlsxChart not working when copy/paste charts found | ||
- fix xml/html entities encode | ||
- make handlebars partials to work | ||
### 4.0.0 | ||
@@ -12,0 +19,0 @@ |
@@ -112,3 +112,3 @@ /* eslint no-unused-vars: 0 */ | ||
newData.evaluatedLoopsIds = [] | ||
newData.outOfLoopItems = Object.create(null) | ||
newData.outOfLoopTemplates = Object.create(null) | ||
@@ -264,3 +264,3 @@ return optionsToUse.fn(this, { data: newData }) | ||
arguments.length === 1 && | ||
type === 'outOfLoopPlaceholder' | ||
type === 'outOfLoop' | ||
) { | ||
@@ -270,26 +270,18 @@ const item = optionsToUse.hash.item | ||
if (item == null) { | ||
throw new Error('xlsxSData type="outOfLoopPlaceholder" helper item arg is required') | ||
throw new Error('xlsxSData type="outOfLoop" helper item arg is required') | ||
} | ||
const currentLoopId = optionsToUse.data.currentLoopId | ||
optionsToUse.data.outOfLoopTemplates[item] = (currentLoopId, currentIdx) => { | ||
const newData = Handlebars.createFrame(optionsToUse.data) | ||
if (currentLoopId == null) { | ||
throw new Error('xlsxSData type="outOfLoopPlaceholder" helper invalid usage, currentLoopId not found') | ||
} | ||
newData.currentLoopId = currentLoopId | ||
let metaItem | ||
if (currentIdx != null) { | ||
newData.index = currentIdx | ||
} | ||
if (optionsToUse.data.outOfLoopItems[item] != null) { | ||
metaItem = optionsToUse.data.outOfLoopItems[item] | ||
} else { | ||
metaItem = { pendingExecutions: [] } | ||
optionsToUse.data.outOfLoopItems[item] = metaItem | ||
return optionsToUse.fn(this, { data: newData }) | ||
} | ||
metaItem.pendingExecutions.push({ | ||
loopId: currentLoopId, | ||
index: optionsToUse.data.index | ||
}) | ||
return optionsToUse.fn(this, { data: optionsToUse.data }) | ||
return new Handlebars.SafeString('') | ||
} | ||
@@ -299,3 +291,3 @@ | ||
arguments.length === 1 && | ||
type === 'outOfLoop' | ||
type === 'outOfLoopPlaceholder' | ||
) { | ||
@@ -305,41 +297,22 @@ const item = optionsToUse.hash.item | ||
if (item == null) { | ||
throw new Error('xlsxSData type="outOfLoop" helper item arg is required') | ||
throw new Error('xlsxSData type="outOfLoopPlaceholder" helper item arg is required') | ||
} | ||
const outOfLoopItem = optionsToUse.data.outOfLoopItems[item] | ||
const outOfLoopTemplate = optionsToUse.data.outOfLoopTemplates[item] | ||
if (outOfLoopItem == null) { | ||
throw new Error('xlsxSData type="outOfLoop" helper invalid usage, outOfLoopItem was not found') | ||
if (outOfLoopTemplate == null) { | ||
throw new Error('xlsxSData type="outOfLoopPlaceholder" helper invalid usage, outOfLoopItem was not found') | ||
} | ||
const pendingExecutions = outOfLoopItem.pendingExecutions.splice(0, outOfLoopItem.pendingExecutions.length) | ||
const currentLoopId = optionsToUse.data.currentLoopId | ||
if (pendingExecutions == null || pendingExecutions.length === 0) { | ||
throw new Error('xlsxSData type="outOfLoop" helper invalid usage, pendingExecution was not found') | ||
if (currentLoopId == null) { | ||
throw new Error('xlsxSData type="outOfLoopPlaceholder" helper invalid usage, currentLoopId not found') | ||
} | ||
const output = [] | ||
const currentIdx = optionsToUse.data.index | ||
for (const pendingExecution of pendingExecutions) { | ||
const currentLoopId = pendingExecution.loopId | ||
const currentIdx = pendingExecution.index | ||
const output = outOfLoopTemplate(currentLoopId, currentIdx) | ||
if (currentLoopId == null) { | ||
throw new Error('xlsxSData type="outOfLoop" helper invalid usage, loopId was not found') | ||
} | ||
const newData = Handlebars.createFrame(optionsToUse.data) | ||
newData.currentLoopId = currentLoopId | ||
if (currentIdx != null) { | ||
newData.index = currentIdx | ||
} | ||
const content = optionsToUse.fn(this, { data: newData }) | ||
output.push(`<data>${content}</data>`) | ||
} | ||
return new Handlebars.SafeString(output.join('')) | ||
return new Handlebars.SafeString(output) | ||
} | ||
@@ -676,2 +649,16 @@ | ||
newData.currentCellValueInfo.value = optionsToUse.hash.value | ||
let toEscape = false | ||
// 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' | ||
} | ||
if (toEscape) { | ||
newData.currentCellValueInfo.value = Handlebars.escapeExpression(newData.currentCellValueInfo.value) | ||
} | ||
} | ||
@@ -678,0 +665,0 @@ |
241447
69
5200
+ Addedhtml-entities@2.4.0
+ Addedhtml-entities@2.4.0(transitive)
- Removedunescape@1.0.1
- Removedextend-shallow@2.0.1(transitive)
- Removedis-extendable@0.1.1(transitive)
- Removedunescape@1.0.1(transitive)