Skip to content

Commit

Permalink
fix support for pptxTable and also support for vertical tables
Browse files Browse the repository at this point in the history
fix #1003

technically i replicated the full docxTable implementation however it seems there are some differences with the xml representation used in the pptx, so i skipped the dynamic rows/columns support and tests for now. it requires a bit more investigation and code to support dynamic rows/columns in pptx
  • Loading branch information
bjrmatos committed Nov 21, 2022
1 parent 1cb9751 commit 4ec022c
Show file tree
Hide file tree
Showing 4 changed files with 580 additions and 28 deletions.
348 changes: 321 additions & 27 deletions packages/jsreport-pptx/lib/preprocess/table.js
@@ -1,46 +1,340 @@

const regexp = /{{#pptxTable [^{}]{0,500}}}/

function processClosingTag (doc, el) {
el.textContent = el.textContent.replace('{{/pptxTable}}', '')

const wpElement = el.parentNode.parentNode.parentNode.parentNode
const fakeElement = doc.createElement('docxRemove')
fakeElement.textContent = '{{/pptxTable}}'

wpElement.parentNode.insertBefore(fakeElement, wpElement.nextSibling)
}
const { nodeListToArray } = require('../utils')
const regexp = /{{#?pptxTable [^{}]{0,500}}}/

// the same idea as list, check the docs there
module.exports = (files) => {
for (const f of files.filter(f => f.path.endsWith('.xml'))) {
const doc = f.doc
const elements = doc.getElementsByTagName('w:t')

let openedDocx = false
const elements = doc.getElementsByTagName('a:t')
const openTags = []

for (let i = 0; i < elements.length; i++) {
const el = elements[i]

if (el.textContent.includes('{{/pptxTable}}') && openedDocx) {
openedDocx = false
processClosingTag(doc, el)
if (el.textContent.includes('{{/pptxTable}}') && openTags.length > 0) {
const tag = openTags.pop()
processClosingTag(doc, el, tag.mode === 'column')
}

if (el.textContent.includes('{{#pptxTable')) {
if (
(
el.textContent.includes('{{pptxTable') ||
el.textContent.includes('{{#pptxTable')
) &&
el.textContent.includes('rows=') &&
el.textContent.includes('columns=')
) {
const isBlock = el.textContent.includes('{{#pptxTable')
// full table mode
let helperCall = el.textContent.match(regexp)[0]

if (!isBlock) {
// setting the cell text to be the value for the rows (before we clone)
el.textContent = el.textContent.replace(regexp, '{{this}}')
} else {
el.textContent = el.textContent.replace(regexp, '')
}

const paragraphNode = el.parentNode.parentNode

let newElement = doc.createElement('pptxRemove')
newElement.textContent = '{{#if @placeholderCell}}'

paragraphNode.parentNode.parentNode.insertBefore(newElement, paragraphNode)

const emptyParagraphNode = paragraphNode.cloneNode(true)

while (emptyParagraphNode.firstChild) {
emptyParagraphNode.removeChild(emptyParagraphNode.firstChild)
emptyParagraphNode.removeAttribute('__block_helper_container__')
}

paragraphNode.parentNode.parentNode.insertBefore(emptyParagraphNode, paragraphNode)

newElement = doc.createElement('pptxRemove')
newElement.textContent = '{{else}}'

paragraphNode.parentNode.parentNode.insertBefore(newElement, paragraphNode)

newElement = doc.createElement('pptxRemove')
newElement.textContent = '{{/if}}'

paragraphNode.parentNode.parentNode.insertBefore(newElement, paragraphNode.nextSibling)

const cellNode = paragraphNode.parentNode.parentNode
const cellPropertiesNode = nodeListToArray(cellNode.childNodes).find((node) => node.nodeName === 'a:tcPr')

// insert conditional logic for colspan and rowspan
newElement = doc.createElement('pptxRemove')
newElement.textContent = '{{#pptxTable check="colspan"}}'

cellPropertiesNode.appendChild(newElement)

newElement = doc.createElement('a:gridSpan')
newElement.setAttribute('a:val', '{{this}}')

cellPropertiesNode.appendChild(newElement)

newElement = doc.createElement('pptxRemove')
newElement.textContent = '{{/pptxTable}}'

cellPropertiesNode.appendChild(newElement)

newElement = doc.createElement('pptxRemove')
newElement.textContent = '{{#pptxTable check="rowspan"}}'

cellPropertiesNode.appendChild(newElement)

newElement = doc.createElement('pptxRemove')
newElement.textContent = '{{#if @empty}}'

cellPropertiesNode.appendChild(newElement)

newElement = doc.createElement('a:vMerge')

cellPropertiesNode.appendChild(newElement)

newElement = doc.createElement('pptxRemove')
newElement.textContent = '{{else}}'

cellPropertiesNode.appendChild(newElement)

newElement = doc.createElement('a:vMerge')
newElement.setAttribute('a:val', 'restart')

cellPropertiesNode.appendChild(newElement)

newElement = doc.createElement('pptxRemove')
newElement.textContent = '{{/if}}'

cellPropertiesNode.appendChild(newElement)

newElement = doc.createElement('pptxRemove')
newElement.textContent = '{{/pptxTable}}'

cellPropertiesNode.appendChild(newElement)

const rowNode = cellNode.parentNode
const tableNode = rowNode.parentNode

const newRowNode = rowNode.cloneNode(true)

if (!isBlock) {
helperCall = helperCall.replace('{{pptxTable', '{{#pptxTable')
}

newElement = doc.createElement('pptxRemove')
newElement.textContent = helperCall.replace('{{#pptxTable', '{{#pptxTable wrapper="main"')

tableNode.parentNode.insertBefore(newElement, tableNode)

newElement = doc.createElement('pptxRemove')
newElement.textContent = '{{/pptxTable}}'

tableNode.parentNode.insertBefore(newElement, tableNode.nextSibling)

if (isBlock) {
openTags.push({ mode: 'column' })
}

processOpeningTag(doc, cellNode, helperCall.replace('rows=', 'ignore='))

if (!isBlock) {
processClosingTag(doc, cellNode)
} else {
if (el.textContent.includes('{{/pptxTable')) {
openTags.pop()
processClosingTag(doc, el, true)
}

const clonedTextNodes = nodeListToArray(newRowNode.getElementsByTagName('a:t'))

for (const tNode of clonedTextNodes) {
if (tNode.textContent.includes('{{/pptxTable')) {
tNode.textContent = tNode.textContent.replace('{{/pptxTable}}', '')
}
}
}

// row template, handling the cells for the data values
rowNode.parentNode.insertBefore(newRowNode, rowNode.nextSibling)
const cellInNewRowNode = nodeListToArray(newRowNode.childNodes).find((node) => node.nodeName === 'a:tc')

processOpeningTag(doc, cellInNewRowNode, helperCall.replace('rows=', 'ignore=').replace('columns=', 'ignore='))
processClosingTag(doc, cellInNewRowNode)

processOpeningTag(doc, newRowNode, helperCall)
processClosingTag(doc, newRowNode)
} else if (el.textContent.includes('{{#pptxTable')) {
const helperCall = el.textContent.match(regexp)[0]
const wpElement = el.parentNode.parentNode.parentNode.parentNode
const fakeElement = doc.createElement('docxRemove')
fakeElement.textContent = helperCall
const isVertical = el.textContent.includes('vertical=')
const isNormal = !isVertical

wpElement.parentNode.insertBefore(fakeElement, wpElement)
el.textContent = el.textContent.replace(regexp, '')
if (el.textContent.includes('{{/pptxTable')) {
processClosingTag(doc, el)
if (isNormal) {
openTags.push({ mode: 'row' })
}

if (isVertical) {
const cellNode = el.parentNode.parentNode.parentNode.parentNode
const cellIndex = getCellIndex(cellNode)
const [affectedRows, textNodeTableClose] = getNextRowsUntilTableClose(cellNode.parentNode)

if (textNodeTableClose) {
textNodeTableClose.textContent = textNodeTableClose.textContent.replace('{{/pptxTable}}', '')
}

const tableNode = cellNode.parentNode.parentNode
const tableGridNode = tableNode.getElementsByTagName('a:tblGrid')[0]
const tableGridColNodes = nodeListToArray(tableGridNode.getElementsByTagName('a:gridCol'))

// add loop for colum definitions (pptx table requires this to show the newly created columns)
processOpeningTag(doc, tableGridColNodes[cellIndex], helperCall, isVertical)
processClosingTag(doc, tableGridColNodes[cellIndex], isVertical)

processOpeningTag(doc, el, helperCall, isVertical)
processClosingTag(doc, el, isVertical)

for (const rowNode of affectedRows) {
const cellNodes = nodeListToArray(rowNode.childNodes).filter((node) => node.nodeName === 'a:tc')
const cellNode = cellNodes[cellIndex]

if (cellNode) {
processOpeningTag(doc, cellNode, helperCall, isVertical)
processClosingTag(doc, cellNode, isVertical)
}
}
} else {
openedDocx = true
processOpeningTag(doc, el, helperCall, isVertical)
}

if (isNormal && el.textContent.includes('{{/pptxTable')) {
openTags.pop()
processClosingTag(doc, el)
}
}
}
}
}

function processOpeningTag (doc, el, helperCall, useColumnRef = false) {
if (el.nodeName === 'a:t') {
el.textContent = el.textContent.replace(regexp, '')
}

const fakeElement = doc.createElement('pptxRemove')

fakeElement.textContent = helperCall

let refElement

if (el.nodeName !== 'a:t') {
refElement = el
} else {
if (useColumnRef) {
// ref is the column a:tc
refElement = el.parentNode.parentNode.parentNode.parentNode
} else {
// ref is the row a:tr
refElement = el.parentNode.parentNode.parentNode.parentNode.parentNode
}
}

refElement.parentNode.insertBefore(fakeElement, refElement)
}

function processClosingTag (doc, el, useColumnRef = false) {
if (el.nodeName === 'a:t') {
el.textContent = el.textContent.replace('{{/pptxTable}}', '')
}

const fakeElement = doc.createElement('pptxRemove')

fakeElement.textContent = '{{/pptxTable}}'

let refElement

if (el.nodeName !== 'a:t') {
refElement = el
} else {
if (useColumnRef) {
refElement = el.parentNode.parentNode.parentNode.parentNode
} else {
refElement = el.parentNode.parentNode.parentNode.parentNode.parentNode
}
}

refElement.parentNode.insertBefore(fakeElement, refElement.nextSibling)
}

function getCellIndex (cellEl) {
if (cellEl.nodeName !== 'a:tc') {
throw new Error('Expected a table cell element during the processing')
}

let prevElements = 0

let currentNode = cellEl.previousSibling

while (
currentNode != null &&
currentNode.nodeName === 'a:tc'
) {
prevElements += 1
currentNode = currentNode.previousSibling
}

return prevElements
}

function getNextRowsUntilTableClose (rowEl) {
if (rowEl.nodeName !== 'a:tr') {
throw new Error('Expected a table row element during the processing')
}

let currentNode = rowEl.nextSibling
let tableCloseNode
const rows = []

while (
currentNode != null &&
currentNode.nodeName === 'a:tr'
) {
rows.push(currentNode)

const cellNodes = nodeListToArray(currentNode.childNodes).filter((node) => node.nodeName === 'a:tc')

for (const cellNode of cellNodes) {
let textNodes = nodeListToArray(cellNode.getElementsByTagName('a:t'))

// get text nodes of the current cell, we don't want text
// nodes of nested tables
textNodes = textNodes.filter((tNode) => {
let current = tNode.parentNode

while (current.nodeName !== 'a:tc') {
current = current.parentNode
}

return current === cellNode
})

for (const tNode of textNodes) {
if (tNode.textContent.includes('{{/pptxTable')) {
currentNode = null
tableCloseNode = tNode
break
}
}

if (currentNode == null) {
break
}
}

if (currentNode != null) {
currentNode = currentNode.nextSibling
}
}

return [rows, tableCloseNode]
}

0 comments on commit 4ec022c

Please sign in to comment.