@jsreport/jsreport-pptx
Advanced tools
Comparing version 4.0.1 to 4.1.0
const slides = require('./slides') | ||
const style = require('./style') | ||
const image = require('./image') | ||
const chart = require('./chart') | ||
const clearOriginalSlideNumber = require('./clearOriginalSlideNumber') | ||
module.exports = async (files) => { | ||
slides(files) | ||
style(files) | ||
await image(files) | ||
await chart(files) | ||
clearOriginalSlideNumber(files) | ||
} |
@@ -39,5 +39,11 @@ const { nodeListToArray } = require('../utils') | ||
slideNumber++ | ||
const newSlideDoc = new DOMParser().parseFromString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${slides[i].toString()}`) | ||
// this attribute is going to be removed later as some of the last steps of postprocess | ||
newSlideDoc.documentElement.setAttribute('originalSlideNumber', originalSlideNumber) | ||
files.push({ | ||
path: `ppt/slides/slide${slideNumber}.xml`, | ||
doc: slides[i] | ||
doc: newSlideDoc | ||
}) | ||
@@ -44,0 +50,0 @@ |
@@ -5,2 +5,4 @@ const concatTags = require('./concatTags') | ||
const table = require('./table') | ||
const chart = require('./chart') | ||
const style = require('./style') | ||
@@ -10,4 +12,6 @@ module.exports = (files) => { | ||
slides(files) | ||
chart(files) | ||
list(files) | ||
table(files) | ||
style(files) | ||
} |
@@ -10,3 +10,3 @@ const { DOMParser, XMLSerializer } = require('@xmldom/xmldom') | ||
module.exports = (reporter) => async (inputs, req) => { | ||
module.exports = async (reporter, inputs, req) => { | ||
const { pptxTemplateContent, outputPath } = inputs | ||
@@ -84,11 +84,15 @@ | ||
for (const f of files) { | ||
let isXML = false | ||
let shouldSerializeFromDoc | ||
if (f.data == null) { | ||
isXML = f.path.includes('.xml') | ||
shouldSerializeFromDoc = f.path.includes('.xml') | ||
} else { | ||
isXML = contentIsXML(f.data) | ||
shouldSerializeFromDoc = contentIsXML(f.data) | ||
} | ||
if (isXML) { | ||
if (f.serializeFromDoc != null) { | ||
shouldSerializeFromDoc = f.serializeFromDoc === true | ||
} | ||
if (shouldSerializeFromDoc) { | ||
f.data = Buffer.from(new XMLSerializer().serializeToString(f.doc)) | ||
@@ -95,0 +99,0 @@ } |
@@ -1,2 +0,1 @@ | ||
const fs = require('fs') | ||
const { response } = require('@jsreport/office') | ||
@@ -38,3 +37,3 @@ const processPptx = require('./processPptx') | ||
const result = await processPptx(reporter)({ | ||
const result = await processPptx(reporter, { | ||
pptxTemplateContent: templateAsset.content, | ||
@@ -46,10 +45,8 @@ outputPath | ||
res.stream = fs.createReadStream(result.pptxFilePath) | ||
await response({ | ||
previewOptions: definition.options.preview, | ||
officeDocumentType: 'pptx', | ||
stream: res.stream, | ||
filePath: result.pptxFilePath, | ||
logger: reporter.logger | ||
}, req, res) | ||
} |
352
lib/utils.js
@@ -0,2 +1,11 @@ | ||
const { XMLSerializer } = require('@xmldom/xmldom') | ||
function nodeListToArray (nodes) { | ||
const arr = [] | ||
for (let i = 0; i < nodes.length; i++) { | ||
arr.push(nodes[i]) | ||
} | ||
return arr | ||
} | ||
function pxToEMU (val) { | ||
@@ -14,2 +23,329 @@ return Math.round(val * 914400 / 96) | ||
function getNewRelIdFromBaseId (relsDoc, itemsMap, baseId) { | ||
const relationsNodes = nodeListToArray(relsDoc.getElementsByTagName('Relationship')) | ||
const getId = (id) => { | ||
const regExp = /^rId(\d+)$/ | ||
const match = regExp.exec(id) | ||
if (!match || !match[1]) { | ||
return null | ||
} | ||
return parseInt(match[1], 10) | ||
} | ||
const maxId = relationsNodes.reduce((lastId, node) => { | ||
const nodeId = node.getAttribute('Id') | ||
const num = getId(nodeId) | ||
if (num == null) { | ||
return lastId | ||
} | ||
if (num > lastId) { | ||
return num | ||
} | ||
return lastId | ||
}, 0) | ||
const baseIdNum = getId(baseId) | ||
if (baseIdNum == null) { | ||
throw new Error(`Unable to get numeric id from rel id "${baseId}"`) | ||
} | ||
let newId = getNewIdFromBaseId(itemsMap, baseIdNum, maxId) | ||
newId = `rId${newId}` | ||
return newId | ||
} | ||
function getNewIdFromBaseId (itemsMap, baseId, maxId) { | ||
const counter = itemsMap.get(baseId) || 0 | ||
itemsMap.set(baseId, counter + 1) | ||
if (counter === 0) { | ||
return baseId | ||
} | ||
return maxId + 1 | ||
} | ||
function getCNvPrEl (graphicFrameEl) { | ||
const containerEl = nodeListToArray(graphicFrameEl.childNodes).find((node) => node.nodeName === 'p:nvGraphicFramePr') | ||
if (!containerEl) { | ||
return | ||
} | ||
const cNvPrEl = nodeListToArray(containerEl.childNodes).find((node) => node.nodeName === 'p:cNvPr') | ||
return cNvPrEl | ||
} | ||
function getChartEl (graphicFrameEl) { | ||
let parentEl = graphicFrameEl.parentNode | ||
const graphicEl = nodeListToArray(graphicFrameEl.childNodes).find((node) => node.nodeName === 'a:graphic') | ||
if (!graphicEl) { | ||
return | ||
} | ||
const graphicDataEl = nodeListToArray(graphicEl.childNodes).find((node) => node.nodeName === 'a:graphicData') | ||
if (!graphicDataEl) { | ||
return | ||
} | ||
let chartDrawingEl = nodeListToArray(graphicDataEl.childNodes).find((node) => ( | ||
node.nodeName === 'c:chart' || node.nodeName === 'cx:chart' | ||
)) | ||
if (!chartDrawingEl) { | ||
return | ||
} | ||
while (parentEl != null) { | ||
// ignore charts that are part of Fallback tag | ||
if (parentEl.nodeName === 'mc:Fallback') { | ||
chartDrawingEl = null | ||
break | ||
} | ||
parentEl = parentEl.parentNode | ||
} | ||
return chartDrawingEl | ||
} | ||
function getClosestEl (el, targetNodeNameOrFn, targetType = 'parent') { | ||
let currentEl = el | ||
let parentEl | ||
const nodeTest = (n) => { | ||
if (typeof targetNodeNameOrFn === 'string') { | ||
return n.nodeName === targetNodeNameOrFn | ||
} else { | ||
return targetNodeNameOrFn(n) | ||
} | ||
} | ||
if (targetType !== 'parent' && targetType !== 'previous') { | ||
throw new Error(`Invalid target type "${targetType}"`) | ||
} | ||
do { | ||
if (targetType === 'parent') { | ||
currentEl = currentEl.parentNode | ||
} else if (targetType === 'previous') { | ||
currentEl = currentEl.previousSibling | ||
} | ||
if (currentEl != null && nodeTest(currentEl)) { | ||
parentEl = currentEl | ||
} | ||
} while (currentEl != null && !nodeTest(currentEl)) | ||
return parentEl | ||
} | ||
function clearEl (el, filterFn) { | ||
// by default we clear all children | ||
const testFn = filterFn || (() => false) | ||
const childEls = nodeListToArray(el.childNodes) | ||
for (const childEl of childEls) { | ||
const result = testFn(childEl) | ||
if (result === true) { | ||
continue | ||
} | ||
childEl.parentNode.removeChild(childEl) | ||
} | ||
} | ||
function findOrCreateChildNode (docNode, nodeName, targetNode) { | ||
let result | ||
const existingNode = findChildNode(nodeName, targetNode) | ||
if (!existingNode) { | ||
result = docNode.createElement(nodeName) | ||
targetNode.appendChild(result) | ||
} else { | ||
result = existingNode | ||
} | ||
return result | ||
} | ||
function findChildNode (nodeNameOrFn, targetNode, allNodes = false) { | ||
const result = [] | ||
let testFn | ||
if (typeof nodeNameOrFn === 'string') { | ||
testFn = (n) => n.nodeName === nodeNameOrFn | ||
} else { | ||
testFn = nodeNameOrFn | ||
} | ||
for (let i = 0; i < targetNode.childNodes.length; i++) { | ||
let found = false | ||
const childNode = targetNode.childNodes[i] | ||
const testResult = testFn(childNode) | ||
if (testResult) { | ||
found = true | ||
result.push(childNode) | ||
} | ||
if (found && !allNodes) { | ||
break | ||
} | ||
} | ||
return allNodes ? result : result[0] | ||
} | ||
function createNewRAndTextNode (textOrOptions, templateRNode, doc) { | ||
const text = typeof textOrOptions === 'string' ? textOrOptions : textOrOptions.text | ||
const attributes = typeof textOrOptions === 'string' ? {} : (textOrOptions.attributes || {}) | ||
const newRNode = templateRNode.cloneNode(true) | ||
const textEl = doc.createElement('a:t') | ||
textEl.setAttribute('xml:space', 'preserve') | ||
for (const [attrName, attrValue] of Object.entries(attributes)) { | ||
textEl.setAttribute(attrName, attrValue) | ||
} | ||
textEl.textContent = text | ||
newRNode.appendChild(textEl) | ||
return [newRNode, textEl] | ||
} | ||
module.exports.normalizeSingleTextElInRun = (textEl, doc) => { | ||
const rEl = getClosestEl(textEl, 'a:r') | ||
if (rEl == null) { | ||
return | ||
} | ||
const textElements = nodeListToArray(rEl.childNodes).filter((n) => n.nodeName === 'a:t') | ||
const leftTextNodes = [] | ||
const rightTextNodes = [] | ||
let found = false | ||
for (const tEl of textElements) { | ||
if (tEl === textEl) { | ||
found = true | ||
} else if (found) { | ||
rightTextNodes.push(tEl) | ||
} else { | ||
leftTextNodes.push(tEl) | ||
} | ||
} | ||
const templateRNode = rEl.cloneNode(true) | ||
// remove text elements and inherit the rest | ||
clearEl(templateRNode, (c) => c.nodeName !== 'a:t') | ||
const results = [] | ||
for (const tNode of leftTextNodes) { | ||
const [newRNode, newTextNode] = createNewRAndTextNode(tNode.textContent, templateRNode, doc) | ||
rEl.removeChild(tNode) | ||
rEl.parentNode.insertBefore(newRNode, rEl) | ||
results.push(newTextNode) | ||
} | ||
results.push(textEl) | ||
for (const tNode of [...rightTextNodes].reverse()) { | ||
const [newRNode, newTextNode] = createNewRAndTextNode(tNode.textContent, templateRNode, doc) | ||
rEl.removeChild(tNode) | ||
rEl.parentNode.insertBefore(newRNode, rEl.nextSibling) | ||
results.push(newTextNode) | ||
} | ||
return results | ||
} | ||
module.exports.normalizeSingleContentInText = (textEl, getMatchRegexp, doc) => { | ||
const rEl = getClosestEl(textEl, 'a:r') | ||
const paragraphEl = getClosestEl(textEl, 'a:p') | ||
if (rEl == null || paragraphEl == null) { | ||
return | ||
} | ||
let newContent = textEl.textContent | ||
const textParts = [] | ||
const matchParts = [] | ||
let match | ||
do { | ||
match = newContent.match(getMatchRegexp()) | ||
if (match != null) { | ||
const leftContent = newContent.slice(0, match.index) | ||
if (leftContent !== '') { | ||
textParts.push(leftContent) | ||
} | ||
const matchInfo = { | ||
content: match[0], | ||
rest: match.slice(1) | ||
} | ||
textParts.push(matchInfo) | ||
matchParts.push(matchInfo) | ||
newContent = newContent.slice(match.index + match[0].length) | ||
} | ||
} while (match != null) | ||
if (newContent !== '') { | ||
textParts.push(newContent) | ||
} | ||
const templateRNode = rEl.cloneNode(true) | ||
// remove text elements and inherit the rest | ||
clearEl(templateRNode, (c) => c.nodeName !== 'a:t') | ||
const results = [] | ||
for (const item of textParts) { | ||
const isMatchInfo = typeof item !== 'string' | ||
const textContent = isMatchInfo ? item.content : item | ||
const [newRNode, newTextNode] = createNewRAndTextNode(textContent, templateRNode, doc) | ||
rEl.parentNode.insertBefore(newRNode, rEl) | ||
const result = { | ||
tEl: newTextNode | ||
} | ||
if (isMatchInfo) { | ||
result.match = item | ||
} | ||
results.push(result) | ||
} | ||
rEl.parentNode.removeChild(rEl) | ||
return results | ||
} | ||
module.exports.contentIsXML = (content) => { | ||
@@ -25,11 +361,13 @@ if (!Buffer.isBuffer(content) && typeof content !== 'string') { | ||
module.exports.nodeListToArray = (nodes) => { | ||
const arr = [] | ||
for (let i = 0; i < nodes.length; i++) { | ||
arr.push(nodes[i]) | ||
} | ||
return arr | ||
} | ||
module.exports.serializeXml = (doc) => new XMLSerializer().serializeToString(doc).replace(/ xmlns(:[a-z0-9]+)?=""/g, '') | ||
module.exports.nodeListToArray = nodeListToArray | ||
module.exports.pxToEMU = pxToEMU | ||
module.exports.cmToEMU = cmToEMU | ||
module.exports.getNewRelIdFromBaseId = getNewRelIdFromBaseId | ||
module.exports.getNewIdFromBaseId = getNewIdFromBaseId | ||
module.exports.getCNvPrEl = getCNvPrEl | ||
module.exports.getChartEl = getChartEl | ||
module.exports.clearEl = clearEl | ||
module.exports.findOrCreateChildNode = findOrCreateChildNode | ||
module.exports.findChildNode = findChildNode |
{ | ||
"name": "@jsreport/jsreport-pptx", | ||
"version": "4.0.1", | ||
"version": "4.1.0", | ||
"description": "jsreport recipe rendering pptx files", | ||
@@ -36,12 +36,14 @@ "keywords": [ | ||
"dependencies": { | ||
"@jsreport/office": "3.0.0", | ||
"@jsreport/office": "4.0.0", | ||
"@xmldom/xmldom": "0.8.6", | ||
"axios": "0.24.0", | ||
"axios": "1.6.2", | ||
"html-entities": "2.4.0", | ||
"image-size": "0.7.4" | ||
"image-size": "0.7.4", | ||
"js-excel-date-convert": "1.0.2", | ||
"moment": "2.29.4" | ||
}, | ||
"devDependencies": { | ||
"@jsreport/jsreport-assets": "4.0.1", | ||
"@jsreport/jsreport-core": "4.0.1", | ||
"@jsreport/jsreport-handlebars": "4.0.0", | ||
"@jsreport/jsreport-assets": "4.0.2", | ||
"@jsreport/jsreport-core": "4.1.0", | ||
"@jsreport/jsreport-handlebars": "4.0.1", | ||
"@jsreport/studio-dev": "4.0.0", | ||
@@ -48,0 +50,0 @@ "handlebars": "4.7.7", |
@@ -10,2 +10,7 @@ # @jsreport/jsreport-pptx | ||
### 4.1.0 | ||
- add pptxStyle support | ||
- add pptxChart support | ||
### 4.0.1 | ||
@@ -46,3 +51,1 @@ | ||
Adaptations for the v3 APIs | ||
@@ -225,2 +225,13 @@ /* eslint no-unused-vars: 0 */ | ||
function pptxStyle (options) { | ||
const Handlebars = require('handlebars') | ||
const textColor = options.hash.textColor || '' | ||
const backgroundColor = options.hash.backgroundColor || '' | ||
return new Handlebars.SafeString( | ||
`<pptxStyle id="${options.hash.id}" textColor="${textColor}" backgroundColor="${backgroundColor}" />` | ||
) | ||
} | ||
function pptxSlides (data, options) { | ||
@@ -290,1 +301,29 @@ const Handlebars = require('handlebars') | ||
} | ||
function pptxChart (options) { | ||
const Handlebars = require('handlebars') | ||
if (options.hash.data == null) { | ||
throw new Error('pptxChart helper requires data parameter to be set') | ||
} | ||
if (!Array.isArray(options.hash.data.labels) || options.hash.data.labels.length === 0) { | ||
throw new Error('pptxChart 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('pptxChart 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('pptxChart helper when options parameter is set, it should be an object') | ||
} | ||
return new Handlebars.SafeString('$pptxChart' + Buffer.from(JSON.stringify(options.hash)).toString('base64') + '$') | ||
} |
137522
28
2896
50
1
7
+ Addedjs-excel-date-convert@1.0.2
+ Addedmoment@2.29.4
+ Added@jsreport/office@4.0.0(transitive)
+ Addedaxios@1.6.2(transitive)
+ Addedform-data@4.0.1(transitive)
+ Addedjs-excel-date-convert@1.0.2(transitive)
+ Addedmoment@2.29.4(transitive)
+ Addedproxy-from-env@1.1.0(transitive)
- Removed@jsreport/office@3.0.0(transitive)
- Removedany-promise@1.3.0(transitive)
- Removedaxios@0.23.00.24.0(transitive)
- Removedstream-to-array@2.3.0(transitive)
Updated@jsreport/office@4.0.0
Updatedaxios@1.6.2