@jsreport/jsreport-docx
Advanced tools
Comparing version 3.4.0 to 3.5.0
@@ -10,5 +10,9 @@ const bookmark = require('./bookmark') | ||
const removeBlockHelper = require('./removeBlockHelper') | ||
const html = require('./html') | ||
module.exports = async (files, options) => { | ||
const newBookmarksMap = new Map() | ||
// we handle the html step as the first to ensure no other step | ||
// work with the attribute and comment we put for the <w:p> elements for the html handling | ||
await html(files) | ||
await bookmark(files, newBookmarksMap) | ||
@@ -15,0 +19,0 @@ await watermark(files) |
const { DOMParser, XMLSerializer } = require('@xmldom/xmldom') | ||
const moment = require('moment') | ||
const toExcelDate = require('js-excel-date-convert').toExcelDate | ||
const { serializeXml, nodeListToArray, getChartEl, getNewRelIdFromBaseId } = require('../utils') | ||
const { serializeXml, nodeListToArray, getChartEl, getNewRelIdFromBaseId, clearEl, findChildNode, findOrCreateChildNode } = require('../utils') | ||
@@ -29,3 +29,3 @@ module.exports = async function processChart (files, drawingEl, originalChartsXMLMap, newRelIdCounterMap) { | ||
let chartRelsFilename = `word/charts/_rels/${chartFilename.split('/').slice(-1)[0]}.rels` | ||
// take the original (not modifed) document | ||
// take the original (not modified) document | ||
let chartRelsDoc = originalChartsXMLMap.has(chartRelsFilename) ? new DOMParser().parseFromString(originalChartsXMLMap.get(chartRelsFilename)) : files.find(f => f.path === chartRelsFilename).doc | ||
@@ -531,3 +531,3 @@ | ||
removeChildNodes('c:extLst', baseChartSerieEl) | ||
clearEl(baseChartSerieEl, (c) => c.nodeName !== 'c:extLst') | ||
@@ -705,3 +705,3 @@ addChartSerieItem(chartDoc, { name: 'c:idx', data: serieIdx }, baseChartSerieEl) | ||
removeChildNodes('cx:f', newNode) | ||
clearEl(newNode, (c) => c.nodeName !== 'cx:f') | ||
@@ -774,3 +774,3 @@ const existingLvlNodes = findChildNode('cx:lvl', newNode, true) | ||
removeChildNodes('cx:f', txDataNode) | ||
clearEl(txDataNode, (c) => c.nodeName !== 'cx:f') | ||
@@ -935,3 +935,3 @@ const txValueNode = findOrCreateChildNode(docNode, 'cx:v', txDataNode) | ||
const refNode = findOrCreateChildNode(docNode, numType ? 'c:numRef' : 'c:strRef', parentNode) | ||
removeChildNodes('c:f', refNode) | ||
clearEl(refNode, (c) => c.nodeName !== 'c:f') | ||
const cacheNode = findOrCreateChildNode(docNode, numType ? 'c:numCache' : 'c:strCache', refNode) | ||
@@ -991,45 +991,1 @@ const existingFormatNode = findChildNode('c:formatCode', cacheNode) | ||
} | ||
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 (nodeName, targetNode, allNodes = false) { | ||
const result = [] | ||
for (let i = 0; i < targetNode.childNodes.length; i++) { | ||
let found = false | ||
const childNode = targetNode.childNodes[i] | ||
if (childNode.nodeName === nodeName) { | ||
found = true | ||
result.push(childNode) | ||
} | ||
if (found && !allNodes) { | ||
break | ||
} | ||
} | ||
return allNodes ? result : result[0] | ||
} | ||
function removeChildNodes (nodeName, targetNode) { | ||
for (let i = 0; i < targetNode.childNodes.length; i++) { | ||
const childNode = targetNode.childNodes[i] | ||
if (childNode.nodeName === nodeName) { | ||
targetNode.removeChild(childNode) | ||
} | ||
} | ||
} |
@@ -90,3 +90,3 @@ const sizeOf = require('image-size') | ||
// somehow there are duplicated hlinkclick els produced by word, we need to clean them up | ||
// somehow there are duplicated linkclick els produced by word, we need to clean them up | ||
for (let i = 1; i < linkClickEls.length; i++) { | ||
@@ -129,3 +129,3 @@ const elLinkClick = linkClickEls[i] | ||
// some servers returns the image content type withoyt the "image/" prefix | ||
// some servers returns the image content type without the "image/" prefix | ||
imageExtension = extensionsParts.length === 1 ? extensionsParts[0] : extensionsParts[1] | ||
@@ -132,0 +132,0 @@ imageBuffer = Buffer.from(response.data) |
@@ -6,3 +6,5 @@ const { DOMParser } = require('@xmldom/xmldom') | ||
module.exports = async (files) => { | ||
const stylesFile = files.find(f => f.path === 'word/styles.xml').doc | ||
const stylesDoc = files.find(f => f.path === 'word/styles.xml').doc | ||
const settingsDoc = files.find(f => f.path === 'word/settings.xml').doc | ||
const contentTypesDoc = files.find(f => f.path === '[Content_Types].xml').doc | ||
const documentFile = files.find(f => f.path === 'word/document.xml') | ||
@@ -15,4 +17,63 @@ const titles = [] | ||
const TOCExists = contentTypesDoc.documentElement.getAttribute('TOCExists') === '1' | ||
let updateFields = true | ||
if (TOCExists) { | ||
contentTypesDoc.documentElement.removeAttribute('TOCExists') | ||
} | ||
documentFile.data = await recursiveStringReplaceAsync( | ||
documentFile.data.toString(), | ||
'<TOCHeading>', | ||
'</TOCHeading>', | ||
'g', | ||
async (val, content, hasNestedMatch) => { | ||
if (hasNestedMatch) { | ||
return val | ||
} | ||
const docEl = new DOMParser().parseFromString(content).documentElement | ||
const tEls = nodeListToArray(docEl.getElementsByTagName('w:t')) | ||
for (const tEl of tEls) { | ||
const match = tEl.textContent.match(/\$docxTOCOptions([^$]*)\$/) | ||
if (match == null || match[1] == null || match[1] === '') { | ||
continue | ||
} | ||
// remove chart helper text | ||
tEl.textContent = tEl.textContent.replace(match[0], '') | ||
const tocOptions = JSON.parse(Buffer.from(match[1], 'base64').toString()) | ||
if (tocOptions.updateFields == null || typeof tocOptions.updateFields !== 'boolean') { | ||
continue | ||
} | ||
updateFields = tocOptions.updateFields | ||
break | ||
} | ||
return serializeXml(docEl) | ||
} | ||
) | ||
if (TOCExists && updateFields) { | ||
// add here the setting of the document to automatically recalculate fields on open, | ||
// this allows the MS Word to prompt the user to update the page numbers or toc table | ||
// when opening the generated file | ||
const existingUpdateFieldsEl = settingsDoc.documentElement.getElementsByTagName('w:updateFields')[0] | ||
// if the setting is already on the document we don't generate it | ||
if (existingUpdateFieldsEl == null) { | ||
const doc = new DOMParser().parseFromString('<w:p></w:p>') | ||
const newUpdateFieldsEl = doc.createElement('w:updateFields') | ||
newUpdateFieldsEl.setAttribute('w:val', 'true') | ||
settingsDoc.documentElement.insertBefore(newUpdateFieldsEl, settingsDoc.documentElement.firstChild) | ||
} | ||
} | ||
documentFile.data = await recursiveStringReplaceAsync( | ||
documentFile.data.toString(), | ||
'<TOCTitle>', | ||
@@ -26,3 +87,3 @@ '</TOCTitle>', | ||
let tocTitlePrefix = findDefaultStyleIdForName(stylesFile, 'heading 1') | ||
let tocTitlePrefix = findDefaultStyleIdForName(stylesDoc, 'heading 1') | ||
const tocTitleMatch = tocStyleIdRegExp.exec(tocTitlePrefix) | ||
@@ -113,3 +174,3 @@ | ||
let tocAlternativeTitlePrefix = findDefaultStyleIdForName(stylesFile, 'toc 1') | ||
let tocAlternativeTitlePrefix = findDefaultStyleIdForName(stylesDoc, 'toc 1') | ||
const tocAlternativeTitleMatch = tocStyleIdRegExp.exec(tocAlternativeTitlePrefix) | ||
@@ -116,0 +177,0 @@ |
@@ -6,3 +6,3 @@ | ||
const recursiveStringReplaceAsync = require('../recursiveStringReplaceAsync') | ||
const { nodeListToArray } = require('../utils') | ||
const { nodeListToArray, findOrCreateChildNode } = require('../utils') | ||
@@ -164,35 +164,1 @@ module.exports = async (files) => { | ||
} | ||
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 (nodeName, targetNode, allNodes = false) { | ||
const result = [] | ||
for (let i = 0; i < targetNode.childNodes.length; i++) { | ||
let found = false | ||
const childNode = targetNode.childNodes[i] | ||
if (childNode.nodeName === nodeName) { | ||
found = true | ||
result.push(childNode) | ||
} | ||
if (found && !allNodes) { | ||
break | ||
} | ||
} | ||
return allNodes ? result : result[0] | ||
} |
@@ -12,2 +12,3 @@ const concatTags = require('./concatTags') | ||
const watermark = require('./watermark') | ||
const html = require('./html') | ||
@@ -26,2 +27,5 @@ module.exports = (files) => { | ||
pageBreak(files) | ||
// we handle the html step as the last to ensure no other step | ||
// work with the attribute and comment we put in the <w:p> elements for the html handling | ||
html(files) | ||
} |
const { nodeListToArray, findDefaultStyleIdForName } = require('../utils') | ||
module.exports = (files) => { | ||
const stylesFile = files.find(f => f.path === 'word/styles.xml').doc | ||
const settingsFile = files.find(f => f.path === 'word/settings.xml').doc | ||
const documentFile = files.find(f => f.path === 'word/document.xml').doc | ||
const contentTypesFile = files.find(f => f.path === '[Content_Types].xml').doc | ||
const paragraphEls = nodeListToArray(documentFile.getElementsByTagName('w:p')) | ||
const stylesDoc = files.find(f => f.path === 'word/styles.xml').doc | ||
const documentDoc = files.find(f => f.path === 'word/document.xml').doc | ||
const contentTypesDoc = files.find(f => f.path === '[Content_Types].xml').doc | ||
// we depend on the preprocess - bookmark to execute first, to get the max bookmark currently | ||
let maxBookmarkId = contentTypesFile.documentElement.getAttribute('bookmarkMaxId') | ||
let maxBookmarkId = contentTypesDoc.documentElement.getAttribute('bookmarkMaxId') | ||
@@ -19,6 +17,56 @@ if (maxBookmarkId != null && maxBookmarkId !== '') { | ||
const tocStyleIdRegExp = /^([^\d]+)(\d+)/ | ||
const tocStyleIdRegExp = /^([^\d]+)(\d+)$/ | ||
const tocHeadingStyleId = findDefaultStyleIdForName(stylesDoc, 'TOC Heading') | ||
let tocAlternativeTitlePrefix = findDefaultStyleIdForName(stylesDoc, 'toc 1') | ||
let tocTitlePrefix = findDefaultStyleIdForName(stylesFile, 'heading 1') | ||
const tocAlternativeTitleMatch = tocStyleIdRegExp.exec(tocAlternativeTitlePrefix) | ||
if (tocAlternativeTitleMatch != null && tocAlternativeTitleMatch[1] != null) { | ||
tocAlternativeTitlePrefix = tocAlternativeTitleMatch[1] | ||
} else { | ||
tocAlternativeTitlePrefix = '' | ||
} | ||
let hasTOC = false | ||
let paragraphTOCHeadingEl | ||
const sdtContentEls = nodeListToArray(documentDoc.getElementsByTagName('w:sdtContent')) | ||
for (const sdtContentEl of sdtContentEls) { | ||
const paragraphEls = nodeListToArray(sdtContentEl.getElementsByTagName('w:p')) | ||
const paragraphAlternativeTOCTitleEl = paragraphEls.find((pEl) => { | ||
const styleId = getParagraphStyleId(pEl) | ||
if (styleId == null || tocAlternativeTitlePrefix === '') { | ||
return false | ||
} | ||
return styleId.startsWith(tocAlternativeTitlePrefix) && tocStyleIdRegExp.test(styleId) | ||
}) | ||
paragraphTOCHeadingEl = paragraphEls.find((pEl) => getParagraphStyleId(pEl) === tocHeadingStyleId) | ||
hasTOC = paragraphAlternativeTOCTitleEl != null || paragraphTOCHeadingEl != null | ||
if (hasTOC) { | ||
break | ||
} | ||
} | ||
if (!hasTOC) { | ||
return | ||
} | ||
contentTypesDoc.documentElement.setAttribute('TOCExists', '1') | ||
if (paragraphTOCHeadingEl != null) { | ||
const wrapperEl = documentDoc.createElement('TOCHeading') | ||
const clonedParagraphEl = paragraphTOCHeadingEl.cloneNode(true) | ||
wrapperEl.appendChild(clonedParagraphEl) | ||
paragraphTOCHeadingEl.parentNode.replaceChild(wrapperEl, paragraphTOCHeadingEl) | ||
} | ||
let tocTitlePrefix = findDefaultStyleIdForName(stylesDoc, 'heading 1') | ||
if (tocTitlePrefix == null) { | ||
@@ -36,18 +84,13 @@ return | ||
let hasTOC = false | ||
const paragraphEls = nodeListToArray(documentDoc.getElementsByTagName('w:p')) | ||
paragraphEls.forEach((paragraphEl, paragraphIdx) => { | ||
const pPrEl = nodeListToArray(paragraphEl.childNodes).find((el) => el.nodeName === 'w:pPr') | ||
let hasTOCTitle = false | ||
if (pPrEl == null) { | ||
return | ||
} | ||
const paragraphStyleId = getParagraphStyleId(paragraphEl) | ||
const pStyleEl = nodeListToArray(pPrEl.childNodes).find((el) => el.nodeName === 'w:pStyle') | ||
const titleRegexp = new RegExp(`^${tocTitlePrefix}(\\d+)$`) | ||
if (paragraphStyleId != null) { | ||
const titleRegexp = new RegExp(`^${tocTitlePrefix}(\\d+)$`) | ||
const result = titleRegexp.exec(paragraphStyleId) | ||
if (pStyleEl != null) { | ||
const result = titleRegexp.exec(pStyleEl.getAttribute('w:val')) | ||
if (result != null) { | ||
@@ -59,77 +102,73 @@ const titleType = parseInt(result[1], 10) | ||
if (hasTOCTitle) { | ||
hasTOC = true | ||
if (!hasTOCTitle) { | ||
return | ||
} | ||
let evaluated = false | ||
let startIfNode | ||
let evaluated = false | ||
let startIfNode | ||
const getIfOpeningBlockRegExp = () => /{{#if\s[^}]+}}/g | ||
const getIfClosingBlockRegExp = () => /{{\/if}}/g | ||
const getIfOpeningBlockRegExp = () => /{{#if\s[^}]+}}/g | ||
const getIfClosingBlockRegExp = () => /{{\/if}}/g | ||
do { | ||
evaluated = true | ||
do { | ||
evaluated = true | ||
const originalMeaningfulTextNodes = nodeListToArray(paragraphEl.getElementsByTagName('w:t')).filter((t) => { | ||
return t.textContent != null && t.textContent.trim() !== '' | ||
}) | ||
const originalMeaningfulTextNodes = nodeListToArray(paragraphEl.getElementsByTagName('w:t')).filter((t) => { | ||
return t.textContent != null && t.textContent.trim() !== '' | ||
}) | ||
if (originalMeaningfulTextNodes.length === 0) { | ||
break | ||
} | ||
if (originalMeaningfulTextNodes.length === 0) { | ||
break | ||
} | ||
startIfNode = originalMeaningfulTextNodes[0].textContent.startsWith('{{#if ') ? originalMeaningfulTextNodes[0] : undefined | ||
startIfNode = originalMeaningfulTextNodes[0].textContent.startsWith('{{#if ') ? originalMeaningfulTextNodes[0] : undefined | ||
if (startIfNode == null) { | ||
break | ||
} | ||
if (startIfNode == null) { | ||
break | ||
} | ||
// pre-process headings to move `{{#if cond}}` opening block helpers to new level right before | ||
// the current paragraph if they are at the very beginning of the heading and | ||
// matching `{{/if}}` closing block helpers it is on the next paragraph | ||
const ifOpeningBlockHelperMatches = paragraphEl.textContent.match(getIfOpeningBlockRegExp()) || [] | ||
const ifClosingBlockHelperMatches = paragraphEl.textContent.match(getIfClosingBlockRegExp()) || [] | ||
// pre-process headings to move `{{#if cond}}` opening block helpers to new level right before | ||
// the current paragraph if they are at the very beginning of the heading and | ||
// matching `{{/if}}` closing block helpers it is on the next paragraph | ||
const ifOpeningBlockHelperMatches = paragraphEl.textContent.match(getIfOpeningBlockRegExp()) || [] | ||
const ifClosingBlockHelperMatches = paragraphEl.textContent.match(getIfClosingBlockRegExp()) || [] | ||
// leave heading untouched as the number of opening and closing block helpers are matching | ||
if (ifOpeningBlockHelperMatches.length === ifClosingBlockHelperMatches.length) { | ||
break | ||
} | ||
// leave heading untouched as the number of opening and closing block helpers are matching | ||
if (ifOpeningBlockHelperMatches.length === ifClosingBlockHelperMatches.length) { | ||
break | ||
} | ||
// inspect next paragraphs and search for the exact close if for this node | ||
const nextParagraphEls = paragraphEls.slice(paragraphIdx + 1) | ||
// inspect next paragraphs and search for the exact close if for this node | ||
const nextParagraphEls = paragraphEls.slice(paragraphIdx + 1) | ||
if (nextParagraphEls.length === 0) { | ||
break | ||
} | ||
if (nextParagraphEls.length === 0) { | ||
break | ||
} | ||
let closeIfTextMatchInfo | ||
let openedIfTags = ifOpeningBlockHelperMatches.length - ifClosingBlockHelperMatches.length | ||
let closeIfTextMatchInfo | ||
let openedIfTags = ifOpeningBlockHelperMatches.length - ifClosingBlockHelperMatches.length | ||
for (const nextParagraphEl of nextParagraphEls) { | ||
const nextParagraphTextNodes = nodeListToArray(nextParagraphEl.getElementsByTagName('w:t')) | ||
for (const nextParagraphEl of nextParagraphEls) { | ||
const nextParagraphTextNodes = nodeListToArray(nextParagraphEl.getElementsByTagName('w:t')) | ||
for (const [nptIndex, nptNode] of nextParagraphTextNodes.entries()) { | ||
const childIfOpeningBlockHelperMatches = [...nptNode.textContent.matchAll(getIfOpeningBlockRegExp())] | ||
const childIfClosingBlockHelperMatches = [...nptNode.textContent.matchAll(getIfClosingBlockRegExp())] | ||
for (const [nptIndex, nptNode] of nextParagraphTextNodes.entries()) { | ||
const childIfOpeningBlockHelperMatches = [...nptNode.textContent.matchAll(getIfOpeningBlockRegExp())] | ||
const childIfClosingBlockHelperMatches = [...nptNode.textContent.matchAll(getIfClosingBlockRegExp())] | ||
openedIfTags += childIfOpeningBlockHelperMatches.length | ||
openedIfTags -= childIfClosingBlockHelperMatches.length | ||
openedIfTags += childIfOpeningBlockHelperMatches.length | ||
openedIfTags -= childIfClosingBlockHelperMatches.length | ||
const remainingTextNodesInParagraph = nextParagraphTextNodes.slice(nptIndex + 1) | ||
const remainingTextNodesInParagraph = nextParagraphTextNodes.slice(nptIndex + 1) | ||
// we match only when found the close if and also there is no more text nodes in the paragraph | ||
// that contain the close if | ||
if (openedIfTags === 0 && remainingTextNodesInParagraph.length === 0) { | ||
closeIfTextMatchInfo = { | ||
paragraphNode: nextParagraphEl, | ||
node: nptNode, | ||
// this only works fine when the close if node does not contain another close if (like "{{/if}}{{/if}}") node in there, | ||
// and also there is no more text on the same text node after the close if (like "{{/if}}something"), that won't work | ||
match: childIfClosingBlockHelperMatches[0] | ||
} | ||
break | ||
// we match only when found the close if and also there is no more text nodes in the paragraph | ||
// that contain the close if | ||
if (openedIfTags === 0 && remainingTextNodesInParagraph.length === 0) { | ||
closeIfTextMatchInfo = { | ||
paragraphNode: nextParagraphEl, | ||
node: nptNode, | ||
// this only works fine when the close if node does not contain another close if (like "{{/if}}{{/if}}") node in there, | ||
// and also there is no more text on the same text node after the close if (like "{{/if}}something"), that won't work | ||
match: childIfClosingBlockHelperMatches[0] | ||
} | ||
} | ||
if (closeIfTextMatchInfo != null) { | ||
break | ||
@@ -139,82 +178,93 @@ } | ||
if (closeIfTextMatchInfo == null) { | ||
if (closeIfTextMatchInfo != null) { | ||
break | ||
} | ||
} | ||
// start the normalization | ||
if (closeIfTextMatchInfo == null) { | ||
break | ||
} | ||
// remove `{{#if cond}}` from heading and insert it as tmp block before the current paragraph | ||
startIfNode.textContent = startIfNode.textContent.slice(ifOpeningBlockHelperMatches[0].length) | ||
// start the normalization | ||
const fakeOpenIfElement = documentFile.createElement('docxRemove') | ||
fakeOpenIfElement.textContent = ifOpeningBlockHelperMatches[0] | ||
// remove `{{#if cond}}` from heading and insert it as tmp block before the current paragraph | ||
startIfNode.textContent = startIfNode.textContent.slice(ifOpeningBlockHelperMatches[0].length) | ||
paragraphEl.parentNode.insertBefore(fakeOpenIfElement, paragraphEl) | ||
const fakeOpenIfElement = documentDoc.createElement('docxRemove') | ||
fakeOpenIfElement.textContent = ifOpeningBlockHelperMatches[0] | ||
// remove `{{/if}}` from next paragraph and insert it as tmp block before | ||
// the paragraph that contains the close if | ||
const fakeCloseIfElement = documentFile.createElement('docxRemove') | ||
paragraphEl.parentNode.insertBefore(fakeOpenIfElement, paragraphEl) | ||
closeIfTextMatchInfo.node.textContent = `${ | ||
closeIfTextMatchInfo.node.textContent.slice(0, closeIfTextMatchInfo.match.index) | ||
}${ | ||
closeIfTextMatchInfo.node.textContent.slice(closeIfTextMatchInfo.match.index + closeIfTextMatchInfo.match[0].length) | ||
}` | ||
// remove `{{/if}}` from next paragraph and insert it as tmp block before | ||
// the paragraph that contains the close if | ||
const fakeCloseIfElement = documentDoc.createElement('docxRemove') | ||
fakeCloseIfElement.textContent = '{{/if}}' | ||
closeIfTextMatchInfo.node.textContent = `${ | ||
closeIfTextMatchInfo.node.textContent.slice(0, closeIfTextMatchInfo.match.index) | ||
}${ | ||
closeIfTextMatchInfo.node.textContent.slice(closeIfTextMatchInfo.match.index + closeIfTextMatchInfo.match[0].length) | ||
}` | ||
closeIfTextMatchInfo.paragraphNode.parentNode.insertBefore(fakeCloseIfElement, closeIfTextMatchInfo.paragraphNode.nextSibling) | ||
fakeCloseIfElement.textContent = '{{/if}}' | ||
const newMeaningfulTextNodes = nodeListToArray(paragraphEl.getElementsByTagName('w:t')).filter((t) => { | ||
return t.textContent != null && t.textContent.trim() !== '' | ||
}) | ||
closeIfTextMatchInfo.paragraphNode.parentNode.insertBefore(fakeCloseIfElement, closeIfTextMatchInfo.paragraphNode.nextSibling) | ||
// if the new text content in paragraph start with if the do again the same normalization | ||
if (newMeaningfulTextNodes.length > 0 && newMeaningfulTextNodes[0].textContent.startsWith('{{#if ')) { | ||
evaluated = false | ||
} | ||
} while (!evaluated) | ||
const newMeaningfulTextNodes = nodeListToArray(paragraphEl.getElementsByTagName('w:t')).filter((t) => { | ||
return t.textContent != null && t.textContent.trim() !== '' | ||
}) | ||
const clonedParagraphEl = paragraphEl.cloneNode(true) | ||
const textNode = clonedParagraphEl.getElementsByTagName('w:t')[0] | ||
// if the new text content in paragraph start with if the do again the same normalization | ||
if (newMeaningfulTextNodes.length > 0 && newMeaningfulTextNodes[0].textContent.startsWith('{{#if ')) { | ||
evaluated = false | ||
} | ||
} while (!evaluated) | ||
// we verify that bookmark exists on title elements, if not there it means that we have to create it | ||
if (textNode != null && textNode.parentNode.previousSibling?.nodeName !== 'w:bookmarkStart') { | ||
const rNode = textNode.parentNode | ||
const bookmarkStartEl = documentFile.createElement('w:bookmarkStart') | ||
const bookmarkEndEl = documentFile.createElement('w:bookmarkEnd') | ||
const clonedParagraphEl = paragraphEl.cloneNode(true) | ||
const textNode = clonedParagraphEl.getElementsByTagName('w:t')[0] | ||
maxBookmarkId++ | ||
bookmarkStartEl.setAttribute('w:id', maxBookmarkId) | ||
bookmarkStartEl.setAttribute('w:name', `_Toc${randomInteger(30000000, 90000000)}_r'`) | ||
bookmarkEndEl.setAttribute('w:id', maxBookmarkId) | ||
// we verify that bookmark exists on title elements, if not there it means that we have to create it | ||
if (textNode != null && textNode.parentNode.previousSibling?.nodeName !== 'w:bookmarkStart') { | ||
const rNode = textNode.parentNode | ||
const bookmarkStartEl = documentDoc.createElement('w:bookmarkStart') | ||
const bookmarkEndEl = documentDoc.createElement('w:bookmarkEnd') | ||
rNode.parentNode.insertBefore(bookmarkStartEl, rNode) | ||
rNode.parentNode.insertBefore(bookmarkEndEl, rNode.nextSibling) | ||
} | ||
maxBookmarkId++ | ||
bookmarkStartEl.setAttribute('w:id', maxBookmarkId) | ||
bookmarkStartEl.setAttribute('w:name', `_Toc${randomInteger(30000000, 90000000)}_r'`) | ||
bookmarkEndEl.setAttribute('w:id', maxBookmarkId) | ||
const wrapperEl = documentFile.createElement('TOCTitle') | ||
wrapperEl.appendChild(clonedParagraphEl) | ||
paragraphEl.parentNode.replaceChild(wrapperEl, paragraphEl) | ||
rNode.parentNode.insertBefore(bookmarkStartEl, rNode) | ||
rNode.parentNode.insertBefore(bookmarkEndEl, rNode.nextSibling) | ||
} | ||
const wrapperEl = documentDoc.createElement('TOCTitle') | ||
wrapperEl.appendChild(clonedParagraphEl) | ||
paragraphEl.parentNode.replaceChild(wrapperEl, paragraphEl) | ||
}) | ||
// add here the setting of the document to automatically recalculate fields on open, | ||
// this allows the MS Word to prompt the user to update the page numbers or toc table | ||
// when opening the generated file | ||
if (hasTOC) { | ||
const existingUpdateFieldsEl = settingsFile.documentElement.getElementsByTagName('w:updateFields')[0] | ||
if (maxBookmarkId != null) { | ||
contentTypesDoc.documentElement.setAttribute('bookmarkMaxId', maxBookmarkId) | ||
} | ||
} | ||
// if the setting is already on the document we don't generate it | ||
if (existingUpdateFieldsEl == null) { | ||
const newUpdateFieldsEl = documentFile.createElement('w:updateFields') | ||
newUpdateFieldsEl.setAttribute('w:val', 'true') | ||
function getParagraphStyleId (pEl) { | ||
const pPrEl = nodeListToArray(pEl.childNodes).find((el) => el.nodeName === 'w:pPr') | ||
settingsFile.documentElement.insertBefore(newUpdateFieldsEl, settingsFile.documentElement.firstChild) | ||
} | ||
if (pPrEl == null) { | ||
return | ||
} | ||
if (maxBookmarkId != null) { | ||
contentTypesFile.documentElement.setAttribute('bookmarkMaxId', maxBookmarkId) | ||
const pStyleEl = nodeListToArray(pPrEl.childNodes).find((el) => el.nodeName === 'w:pStyle') | ||
if (pStyleEl == null) { | ||
return | ||
} | ||
const styleId = pStyleEl.getAttribute('w:val') | ||
if (styleId == null || styleId === '') { | ||
return | ||
} | ||
return styleId | ||
} | ||
@@ -221,0 +271,0 @@ |
211
lib/utils.js
@@ -117,2 +117,188 @@ const { XMLSerializer } = require('@xmldom/xmldom') | ||
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 createNode (doc, name, opts = {}) { | ||
const attributes = opts.attributes || {} | ||
const children = opts.children || [] | ||
const newEl = doc.createElement(name) | ||
for (const [attrName, attrValue] of Object.entries(attributes)) { | ||
newEl.setAttribute(attrName, attrValue) | ||
} | ||
for (const child of children) { | ||
newEl.appendChild(child) | ||
} | ||
return newEl | ||
} | ||
function pxToEMU (val) { | ||
return Math.round(val * 914400 / 96) | ||
} | ||
function cmToEMU (val) { | ||
// cm to dxa | ||
// eslint-disable-next-line no-loss-of-precision | ||
const dxa = val * 567.058823529411765 | ||
// dxa to EMU | ||
return Math.round(dxa * 914400 / 72 / 20) | ||
} | ||
function pxToPt (val) { | ||
if (typeof val !== 'number') { | ||
return null | ||
} | ||
return val * 72 / 96 | ||
} | ||
function ptToHalfPoint (val) { | ||
if (typeof val !== 'number') { | ||
return null | ||
} | ||
return val * 2 | ||
} | ||
// pt to twentieths of a point (dxa) | ||
function ptToTOAP (val) { | ||
if (typeof val !== 'number') { | ||
return null | ||
} | ||
return val * 20 | ||
} | ||
function lengthToPx (value) { | ||
if (!value) { | ||
return null | ||
} | ||
if (typeof value === 'number') { | ||
return value | ||
} | ||
const pt = value.match(/([.\d]+)pt/i) | ||
if (pt && pt.length === 2) { | ||
return parseFloat(pt[1], 10) * 96 / 72 | ||
} | ||
const em = value.match(/([.\d]+)r?em/i) | ||
if (em && em.length === 2) { | ||
return parseFloat(em[1], 10) * 16 | ||
} | ||
let px = value.match(/([.\d]+)px/i) | ||
if (px && px.length === 2) { | ||
return parseFloat(px[1], 10) | ||
} | ||
const pe = value.match(/([.\d]+)%/i) | ||
if (pe && pe.length === 2) { | ||
return (parseFloat(pe[1], 10) / 100) * 16 | ||
} | ||
// if no unit is specified and number, assume px | ||
px = value.match(/([.\d]+)/i) | ||
if (px && px.length === 2) { | ||
return parseFloat(px[1], 10) | ||
} | ||
return null | ||
} | ||
module.exports.findDefaultStyleIdForName = (stylesDoc, name, type = 'paragraph') => { | ||
@@ -148,14 +334,16 @@ const styleEls = nodeListToArray(stylesDoc.getElementsByTagName('w:style')) | ||
module.exports.pxToEMU = (val) => { | ||
return Math.round(val * 914400 / 96) | ||
} | ||
module.exports.lengthToPt = (value) => { | ||
const sizeInPx = lengthToPx(value) | ||
module.exports.cmToEMU = (val) => { | ||
// cm to dxa | ||
// eslint-disable-next-line no-loss-of-precision | ||
const dxa = val * 567.058823529411765 | ||
// dxa to EMU | ||
return Math.round(dxa * 914400 / 72 / 20) | ||
if (sizeInPx == null) { | ||
return sizeInPx | ||
} | ||
return pxToPt(sizeInPx) | ||
} | ||
module.exports.pxToEMU = pxToEMU | ||
module.exports.cmToEMU = cmToEMU | ||
module.exports.ptToHalfPoint = ptToHalfPoint | ||
module.exports.ptToTOAP = ptToTOAP | ||
module.exports.serializeXml = (doc) => new XMLSerializer().serializeToString(doc).replace(/ xmlns(:[a-z0-9]+)?=""/g, '') | ||
@@ -166,2 +354,7 @@ module.exports.getNewRelId = getNewRelId | ||
module.exports.getChartEl = getChartEl | ||
module.exports.getClosestEl = getClosestEl | ||
module.exports.clearEl = clearEl | ||
module.exports.findOrCreateChildNode = findOrCreateChildNode | ||
module.exports.findChildNode = findChildNode | ||
module.exports.createNode = createNode | ||
module.exports.nodeListToArray = nodeListToArray |
{ | ||
"name": "@jsreport/jsreport-docx", | ||
"version": "3.4.0", | ||
"version": "3.5.0", | ||
"description": "jsreport recipe rendering docx files", | ||
@@ -39,14 +39,18 @@ "keywords": [ | ||
"axios": "0.24.0", | ||
"cheerio": "1.0.0-rc.12", | ||
"image-size": "0.7.4", | ||
"js-excel-date-convert": "1.0.2", | ||
"moment": "2.29.4", | ||
"nanoid": "3.2.0", | ||
"parse-css-sides": "3.0.1", | ||
"semaphore-async-await": "1.5.1", | ||
"string-replace-async": "2.0.0", | ||
"style-attr": "1.3.0", | ||
"tinycolor2": "1.4.2", | ||
"unescape": "1.0.1" | ||
}, | ||
"devDependencies": { | ||
"@jsreport/jsreport-assets": "3.4.3", | ||
"@jsreport/jsreport-core": "3.7.0", | ||
"@jsreport/jsreport-handlebars": "3.2.0", | ||
"@jsreport/jsreport-assets": "3.4.4", | ||
"@jsreport/jsreport-core": "3.8.1", | ||
"@jsreport/jsreport-handlebars": "3.2.1", | ||
"@jsreport/studio-dev": "3.2.0", | ||
@@ -53,0 +57,0 @@ "handlebars": "4.7.7", |
@@ -10,2 +10,7 @@ # @jsreport/jsreport-docx | ||
### 3.5.0 | ||
- add initial support for embedding html in docx (docxHtml helper) | ||
- add helper `docxTOCOptions` to support configuring TOC behavior (only option available there right now is `updateFields` which controls if the generated docx file should show a prompt when it is being open in Word to decide if the TOC should be updated) | ||
### 3.4.0 | ||
@@ -12,0 +17,0 @@ |
@@ -378,1 +378,16 @@ /* eslint no-unused-vars: 0 */ | ||
} | ||
function docxHtml (options) { | ||
const Handlebars = require('handlebars') | ||
if (options.hash.content == null) { | ||
throw new Error('docxHtml helper requires content parameter to be set') | ||
} | ||
return new Handlebars.SafeString('$docxHtml' + Buffer.from(JSON.stringify(options.hash)).toString('base64') + '$') | ||
} | ||
function docxTOCOptions (options) { | ||
const Handlebars = require('handlebars') | ||
return new Handlebars.SafeString('$docxTOCOptions' + Buffer.from(JSON.stringify(options.hash)).toString('base64') + '$') | ||
} |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
270070
49
6223
38
14
3
+ Addedcheerio@1.0.0-rc.12
+ Addednanoid@3.2.0
+ Addedparse-css-sides@3.0.1
+ Addedtinycolor2@1.4.2
+ Addedboolbase@1.0.0(transitive)
+ Addedcheerio@1.0.0-rc.12(transitive)
+ Addedcheerio-select@2.1.0(transitive)
+ Addedcss-list-helpers@2.0.0(transitive)
+ Addedcss-select@5.1.0(transitive)
+ Addedcss-what@6.1.0(transitive)
+ Addeddom-serializer@2.0.0(transitive)
+ Addeddomelementtype@2.3.0(transitive)
+ Addeddomhandler@5.0.3(transitive)
+ Addeddomutils@3.2.2(transitive)
+ Addedentities@4.5.0(transitive)
+ Addedhtmlparser2@8.0.2(transitive)
+ Addednanoid@3.2.0(transitive)
+ Addednth-check@2.1.1(transitive)
+ Addedparse-css-sides@3.0.1(transitive)
+ Addedparse5@7.2.1(transitive)
+ Addedparse5-htmlparser2-tree-adapter@7.1.0(transitive)
+ Addedtinycolor2@1.4.2(transitive)