/** * PptxGenJS: XML Generation */ import { BULLET_TYPES, CRLF, DEF_BULLET_MARGIN, DEF_CELL_MARGIN_IN, DEF_PRES_LAYOUT_NAME, DEF_TEXT_GLOW, DEF_TEXT_SHADOW, EMU, LAYOUT_IDX_SERIES_BASE, PLACEHOLDER_TYPES, SLDNUMFLDID, SLIDE_OBJECT_TYPES, } from './core-enums' import { IPresentationProps, ISlideObject, ISlideRel, ISlideRelChart, ISlideRelMedia, ObjectOptions, PresSlide, ShadowProps, SlideLayout, TableCell, TableCellProps, TextProps, TextPropsOptions, } from './core-interfaces' import { convertRotationDegrees, createColorElement, createGlowElement, encodeXmlEntities, genXmlColorSelection, getSmartParseNumber, getUuid, inch2Emu, valToPts, } from './gen-utils' const ImageSizingXml = { cover: function (imgSize: { w: number, h: number }, boxDim: { w: number, h: number, x: number, y: number }) { const imgRatio = imgSize.h / imgSize.w const boxRatio = boxDim.h / boxDim.w const isBoxBased = boxRatio > imgRatio const width = isBoxBased ? boxDim.h / imgRatio : boxDim.w const height = isBoxBased ? boxDim.h : boxDim.w * imgRatio const hzPerc = Math.round(1e5 * 0.5 * (1 - boxDim.w / width)) const vzPerc = Math.round(1e5 * 0.5 * (1 - boxDim.h / height)) return `` }, contain: function (imgSize: { w: number, h: number }, boxDim: { w: number, h: number, x: number, y: number }) { const imgRatio = imgSize.h / imgSize.w const boxRatio = boxDim.h / boxDim.w const widthBased = boxRatio > imgRatio const width = widthBased ? boxDim.w : boxDim.h / imgRatio const height = widthBased ? boxDim.w * imgRatio : boxDim.h const hzPerc = Math.round(1e5 * 0.5 * (1 - boxDim.w / width)) const vzPerc = Math.round(1e5 * 0.5 * (1 - boxDim.h / height)) return `` }, crop: function (imgSize: { w: number, h: number }, boxDim: { w: number, h: number, x: number, y: number }) { const l = boxDim.x const r = imgSize.w - (boxDim.x + boxDim.w) const t = boxDim.y const b = imgSize.h - (boxDim.y + boxDim.h) const lPerc = Math.round(1e5 * (l / imgSize.w)) const rPerc = Math.round(1e5 * (r / imgSize.w)) const tPerc = Math.round(1e5 * (t / imgSize.h)) const bPerc = Math.round(1e5 * (b / imgSize.h)) return `` }, } /** * Transforms a slide or slideLayout to resulting XML string - Creates `ppt/slide*.xml` * @param {PresSlide|SlideLayout} slideObject - slide object created within createSlideObject * @return {string} XML string with as the root */ function slideObjectToXml (slide: PresSlide | SlideLayout): string { let strSlideXml: string = slide._name ? '' : '' let intTableNum = 1 // STEP 1: Add background color/image (ensure only a single `` tag is created, ex: when master-baskground has both `color` and `path`) if (slide._bkgdImgRid) { strSlideXml += `` } else if (slide.background?.color) { strSlideXml += `${genXmlColorSelection(slide.background)}` } else if (!slide.bkgd && slide._name && slide._name === DEF_PRES_LAYOUT_NAME) { // NOTE: Default [white] background is needed on slideMaster1.xml to avoid gray background in Keynote (and Finder previews) strSlideXml += '' } // STEP 2: Continue slide by starting spTree node strSlideXml += '' strSlideXml += '' strSlideXml += '' strSlideXml += '' // STEP 3: Loop over all Slide.data objects and add them to this slide slide._slideObjects.forEach((slideItemObj: ISlideObject, idx: number) => { let x = 0 let y = 0 let cx = getSmartParseNumber('75%', 'X', slide._presLayout) let cy = 0 let placeholderObj: ISlideObject let locationAttr = '' let arrTabRows: TableCell[][] = null let objTabOpts: ObjectOptions = null let intColCnt = 0 let intColW = 0 let cellOpts: TableCellProps = null let strXml: string = null const sizing: ObjectOptions['sizing'] = slideItemObj.options?.sizing const rounding = slideItemObj.options?.rounding if ( (slide as PresSlide)._slideLayout !== undefined && (slide as PresSlide)._slideLayout._slideObjects !== undefined && slideItemObj.options && slideItemObj.options.placeholder ) { placeholderObj = (slide as PresSlide)._slideLayout._slideObjects.filter( (object: ISlideObject) => object.options.placeholder === slideItemObj.options.placeholder )[0] } // A: Set option vars slideItemObj.options = slideItemObj.options || {} if (typeof slideItemObj.options.x !== 'undefined') x = getSmartParseNumber(slideItemObj.options.x, 'X', slide._presLayout) if (typeof slideItemObj.options.y !== 'undefined') y = getSmartParseNumber(slideItemObj.options.y, 'Y', slide._presLayout) if (typeof slideItemObj.options.w !== 'undefined') cx = getSmartParseNumber(slideItemObj.options.w, 'X', slide._presLayout) if (typeof slideItemObj.options.h !== 'undefined') cy = getSmartParseNumber(slideItemObj.options.h, 'Y', slide._presLayout) // Set w/h now that smart parse is done let imgWidth = cx let imgHeight = cy // If using a placeholder then inherit it's position if (placeholderObj) { if (placeholderObj.options.x || placeholderObj.options.x === 0) x = getSmartParseNumber(placeholderObj.options.x, 'X', slide._presLayout) if (placeholderObj.options.y || placeholderObj.options.y === 0) y = getSmartParseNumber(placeholderObj.options.y, 'Y', slide._presLayout) if (placeholderObj.options.w || placeholderObj.options.w === 0) cx = getSmartParseNumber(placeholderObj.options.w, 'X', slide._presLayout) if (placeholderObj.options.h || placeholderObj.options.h === 0) cy = getSmartParseNumber(placeholderObj.options.h, 'Y', slide._presLayout) } // if (slideItemObj.options.flipH) locationAttr += ' flipH="1"' if (slideItemObj.options.flipV) locationAttr += ' flipV="1"' if (slideItemObj.options.rotate) locationAttr += ` rot="${convertRotationDegrees(slideItemObj.options.rotate)}"` // B: Add OBJECT to the current Slide switch (slideItemObj._type) { case SLIDE_OBJECT_TYPES.table: arrTabRows = slideItemObj.arrTabRows objTabOpts = slideItemObj.options intColCnt = 0 intColW = 0 // Calc number of columns // NOTE: Cells may have a colspan, so merely taking the length of the [0] (or any other) row is not // ....: sufficient to determine column count. Therefore, check each cell for a colspan and total cols as reqd arrTabRows[0].forEach(cell => { cellOpts = cell.options || null intColCnt += cellOpts?.colspan ? Number(cellOpts.colspan) : 1 }) // STEP 1: Start Table XML // NOTE: Non-numeric cNvPr id values will trigger "presentation needs repair" type warning in MS-PPT-2013 strXml = `` strXml += '' + ' ' + '' strXml += `` strXml += '' // + ' '; // TODO: Support banded rows, first/last row, etc. // NOTE: Banding, etc. only shows when using a table style! (or set alt row color if banding) // // STEP 2: Set column widths // Evenly distribute cols/rows across size provided when applicable (calc them if only overall dimensions were provided) // A: Col widths provided? // B: Table Width provided without colW? Then distribute cols if (Array.isArray(objTabOpts.colW)) { strXml += '' for (let col = 0; col < intColCnt; col++) { let w = inch2Emu(objTabOpts.colW[col]) if (w == null || isNaN(w)) { w = (typeof slideItemObj.options.w === 'number' ? slideItemObj.options.w : 1) / intColCnt } strXml += `` } strXml += '' } else { intColW = objTabOpts.colW ? objTabOpts.colW : EMU if (slideItemObj.options.w && !objTabOpts.colW) intColW = Math.round((typeof slideItemObj.options.w === 'number' ? slideItemObj.options.w : 1) / intColCnt) strXml += '' for (let colw = 0; colw < intColCnt; colw++) { strXml += `` } strXml += '' } // STEP 3: Build our row arrays into an actual grid to match the XML we will be building next (ISSUE #36) // Note row arrays can arrive "lopsided" as in row1:[1,2,3] row2:[3] when first two cols rowspan!, // so a simple loop below in XML building wont suffice to build table correctly. // We have to build an actual grid now /* EX: (A0:rowspan=3, B1:rowspan=2, C1:colspan=2) /------|------|------|------\ | A0 | B0 | C0 | D0 | | | B1 | C1 | | | | | C2 | D2 | \------|------|------|------/ */ // A: add _hmerge cell for colspan. should reserve rowspan arrTabRows.forEach(cells => { for (let cIdx = 0; cIdx < cells.length;) { const cell = cells[cIdx] const colspan = cell.options?.colspan const rowspan = cell.options?.rowspan if (colspan && colspan > 1) { const vMergeCells = new Array(colspan - 1).fill(undefined).map(() => { return { _type: SLIDE_OBJECT_TYPES.tablecell, options: { rowspan }, _hmerge: true } as const }) cells.splice(cIdx + 1, 0, ...vMergeCells) cIdx += colspan } else { cIdx += 1 } } }) // B: add _vmerge cell for rowspan. should reserve colspan/_hmerge arrTabRows.forEach((cells, rIdx) => { const nextRow = arrTabRows[rIdx + 1] if (!nextRow) return cells.forEach((cell, cIdx) => { const rowspan = cell._rowContinue || cell.options?.rowspan const colspan = cell.options?.colspan const _hmerge = cell._hmerge if (rowspan && rowspan > 1) { const hMergeCell = { _type: SLIDE_OBJECT_TYPES.tablecell, options: { colspan }, _rowContinue: rowspan - 1, _vmerge: true, _hmerge } as const nextRow.splice(cIdx, 0, hMergeCell) } }) }) // STEP 4: Build table rows/cells arrTabRows.forEach((cells, rIdx) => { // A: Table Height provided without rowH? Then distribute rows let intRowH = 0 // IMPORTANT: Default must be zero for auto-sizing to work if (Array.isArray(objTabOpts.rowH) && objTabOpts.rowH[rIdx]) intRowH = inch2Emu(Number(objTabOpts.rowH[rIdx])) else if (objTabOpts.rowH && !isNaN(Number(objTabOpts.rowH))) intRowH = inch2Emu(Number(objTabOpts.rowH)) else if (slideItemObj.options.cy || slideItemObj.options.h) { intRowH = Math.round( (slideItemObj.options.h ? inch2Emu(slideItemObj.options.h) : typeof slideItemObj.options.cy === 'number' ? slideItemObj.options.cy : 1) / arrTabRows.length ) } // B: Start row strXml += `` // C: Loop over each CELL cells.forEach(cellObj => { const cell: TableCell = cellObj const cellSpanAttrs = { rowSpan: cell.options?.rowspan > 1 ? cell.options.rowspan : undefined, gridSpan: cell.options?.colspan > 1 ? cell.options.colspan : undefined, vMerge: cell._vmerge ? 1 : undefined, hMerge: cell._hmerge ? 1 : undefined, } let cellSpanAttrStr = Object.keys(cellSpanAttrs) .map(k => [k, cellSpanAttrs[k]]) .filter(([, v]) => !!v) .map(([k, v]) => `${String(k)}="${String(v)}"`) .join(' ') if (cellSpanAttrStr) cellSpanAttrStr = ' ' + cellSpanAttrStr // 1: COLSPAN/ROWSPAN: Add dummy cells for any active colspan/rowspan if (cell._hmerge || cell._vmerge) { strXml += `` return } // 2: OPTIONS: Build/set cell options const cellOpts = cell.options || {} cell.options = cellOpts // B: Inherit some options from table when cell options dont exist // @see: http://officeopenxml.com/drwTableCellProperties-alignment.php ;['align', 'bold', 'border', 'color', 'fill', 'fontFace', 'fontSize', 'margin', 'textDirection', 'underline', 'valign'].forEach(name => { if (objTabOpts[name] && !cellOpts[name] && cellOpts[name] !== 0) cellOpts[name] = objTabOpts[name] }) const cellValign = cellOpts.valign ? ` anchor="${cellOpts.valign.replace(/^c$/i, 'ctr').replace(/^m$/i, 'ctr').replace('center', 'ctr').replace('middle', 'ctr').replace('top', 't').replace('btm', 'b').replace('bottom', 'b')}"` : '' const cellTextDir = (cellOpts.textDirection && cellOpts.textDirection !== 'horz') ? ` vert="${cellOpts.textDirection}"` : '' let fillColor = cell._optImp?.fill?.color ? cell._optImp.fill.color : cell._optImp?.fill && typeof cell._optImp.fill === 'string' ? cell._optImp.fill : '' fillColor = fillColor || cellOpts.fill ? cellOpts.fill : '' const cellFill = fillColor ? genXmlColorSelection(fillColor) : '' let cellMargin = cellOpts.margin === 0 || cellOpts.margin ? cellOpts.margin : DEF_CELL_MARGIN_IN if (!Array.isArray(cellMargin) && typeof cellMargin === 'number') cellMargin = [cellMargin, cellMargin, cellMargin, cellMargin] /** FUTURE: DEPRECATED: * - Backwards-Compat: Oops! Discovered we were still using points for cell margin before v3.8.0 (UGH!) * - We cant introduce a breaking change before v4.0, so... */ let cellMarginXml = '' if (cellMargin[0] >= 1) { cellMarginXml = ` marL="${valToPts(cellMargin[3])}" marR="${valToPts(cellMargin[1])}" marT="${valToPts(cellMargin[0])}" marB="${valToPts( cellMargin[2] )}"` } else { cellMarginXml = ` marL="${inch2Emu(cellMargin[3])}" marR="${inch2Emu(cellMargin[1])}" marT="${inch2Emu(cellMargin[0])}" marB="${inch2Emu( cellMargin[2] )}"` } // FUTURE: Cell NOWRAP property (textwrap: add to a:tcPr (horzOverflow="overflow" or whatever options exist) // 4: Set CELL content and properties ================================== strXml += `${genXmlTextBody(cell)}` // strXml += `${genXmlTextBody(cell)}` // FIXME: 20200525: ^^^ // // 5: Borders: Add any borders if (cellOpts.border && Array.isArray(cellOpts.border)) { // NOTE: *** IMPORTANT! *** LRTB order matters! (Reorder a line below to watch the borders go wonky in MS-PPT-2013!!) [ { idx: 3, name: 'lnL' }, { idx: 1, name: 'lnR' }, { idx: 0, name: 'lnT' }, { idx: 2, name: 'lnB' }, ].forEach(obj => { if (cellOpts.border[obj.idx].type !== 'none') { strXml += `` strXml += `${createColorElement(cellOpts.border[obj.idx].color)}` strXml += `` strXml += `` } else { strXml += `` } }) } // 6: Close cell Properties & Cell strXml += cellFill strXml += ' ' strXml += ' ' }) // D: Complete row strXml += '' }) // STEP 5: Complete table strXml += ' ' strXml += ' ' strXml += ' ' strXml += '' // STEP 6: Set table XML strSlideXml += strXml // LAST: Increment counter intTableNum++ break case SLIDE_OBJECT_TYPES.text: case SLIDE_OBJECT_TYPES.placeholder: // Lines can have zero cy, but text should not if (!slideItemObj.options.line && cy === 0) cy = EMU * 0.3 // Margin/Padding/Inset for textboxes if (!slideItemObj.options._bodyProp) slideItemObj.options._bodyProp = {} if (slideItemObj.options.margin && Array.isArray(slideItemObj.options.margin)) { slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin[0] || 0) slideItemObj.options._bodyProp.rIns = valToPts(slideItemObj.options.margin[1] || 0) slideItemObj.options._bodyProp.bIns = valToPts(slideItemObj.options.margin[2] || 0) slideItemObj.options._bodyProp.tIns = valToPts(slideItemObj.options.margin[3] || 0) } else if (typeof slideItemObj.options.margin === 'number') { slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin) slideItemObj.options._bodyProp.rIns = valToPts(slideItemObj.options.margin) slideItemObj.options._bodyProp.bIns = valToPts(slideItemObj.options.margin) slideItemObj.options._bodyProp.tIns = valToPts(slideItemObj.options.margin) } // A: Start SHAPE ======================================================= strSlideXml += '' // B: The addition of the "txBox" attribute is the sole determiner of if an object is a shape or textbox strSlideXml += `` // if (slideItemObj.options.hyperlink?.url) { strSlideXml += `` } if (slideItemObj.options.hyperlink?.slide) { strSlideXml += `` } // strSlideXml += '' strSlideXml += '' : '/>') strSlideXml += `${slideItemObj._type === 'placeholder' ? genXmlPlaceholder(slideItemObj) : genXmlPlaceholder(placeholderObj)}` strSlideXml += '' strSlideXml += `` strSlideXml += `` strSlideXml += `` if (slideItemObj.shape === 'custGeom') { strSlideXml += '' strSlideXml += '' strSlideXml += '' strSlideXml += '' strSlideXml += '' strSlideXml += '' strSlideXml += '' strSlideXml += '' strSlideXml += `` slideItemObj.options.points?.forEach((point, i) => { if ('curve' in point) { switch (point.curve.type) { case 'arc': strSlideXml += `` break case 'cubic': strSlideXml += ` ` break case 'quadratic': strSlideXml += ` ` break default: break } } else if ('close' in point) { strSlideXml += '' } else if (point.moveTo || i === 0) { strSlideXml += `` } else { strSlideXml += `` } }) strSlideXml += '' strSlideXml += '' strSlideXml += '' } else { strSlideXml += '' if (slideItemObj.options.rectRadius) { strSlideXml += `` } else if (slideItemObj.options.angleRange) { for (let i = 0; i < 2; i++) { const angle = slideItemObj.options.angleRange[i] strSlideXml += `` } if (slideItemObj.options.arcThicknessRatio) { strSlideXml += `` } } strSlideXml += '' } // Option: FILL strSlideXml += slideItemObj.options.fill ? genXmlColorSelection(slideItemObj.options.fill) : '' // shape Type: LINE: line color if (slideItemObj.options.line) { strSlideXml += slideItemObj.options.line.width ? `` : '' if (slideItemObj.options.line.color) strSlideXml += genXmlColorSelection(slideItemObj.options.line) if (slideItemObj.options.line.dashType) strSlideXml += `` if (slideItemObj.options.line.beginArrowType) strSlideXml += `` if (slideItemObj.options.line.endArrowType) strSlideXml += `` // FUTURE: `endArrowSize` < a: headEnd type = "arrow" w = "lg" len = "lg" /> 'sm' | 'med' | 'lg'(values are 1 - 9, making a 3x3 grid of w / len possibilities) strSlideXml += '' } // EFFECTS > SHADOW: REF: @see http://officeopenxml.com/drwSp-effects.php if (slideItemObj.options.shadow && slideItemObj.options.shadow.type !== 'none') { slideItemObj.options.shadow.type = slideItemObj.options.shadow.type || 'outer' slideItemObj.options.shadow.blur = valToPts(slideItemObj.options.shadow.blur || 8) slideItemObj.options.shadow.offset = valToPts(slideItemObj.options.shadow.offset || 4) slideItemObj.options.shadow.angle = Math.round((slideItemObj.options.shadow.angle || 270) * 60000) slideItemObj.options.shadow.opacity = Math.round((slideItemObj.options.shadow.opacity || 0.75) * 100000) slideItemObj.options.shadow.color = slideItemObj.options.shadow.color || DEF_TEXT_SHADOW.color strSlideXml += '' strSlideXml += ` ` strSlideXml += ` ` strSlideXml += ` ` strSlideXml += ' ' strSlideXml += '' } /* TODO: FUTURE: Text wrapping (copied from MS-PPTX export) // Commented out b/c i'm not even sure this works - current code produces text that wraps in shapes and textboxes, so... if ( slideItemObj.options.textWrap ) { strSlideXml += '' + '' + '' + '' + ''; } */ // B: Close shape Properties strSlideXml += '' // C: Add formatted text (text body "bodyPr") strSlideXml += genXmlTextBody(slideItemObj) // LAST: Close SHAPE ======================================================= strSlideXml += '' break case SLIDE_OBJECT_TYPES.image: strSlideXml += '' strSlideXml += ' ' strSlideXml += `` if (slideItemObj.hyperlink?.url) { strSlideXml += `` } if (slideItemObj.hyperlink?.slide) { strSlideXml += `` } strSlideXml += ' ' strSlideXml += ' ' strSlideXml += ' ' + genXmlPlaceholder(placeholderObj) + '' strSlideXml += ' ' strSlideXml += '' // NOTE: This works for both cases: either `path` or `data` contains the SVG if ( (slide._relsMedia || []).filter(rel => rel.rId === slideItemObj.imageRid)[0] && (slide._relsMedia || []).filter(rel => rel.rId === slideItemObj.imageRid)[0].extn === 'svg' ) { strSlideXml += `` strSlideXml += slideItemObj.options.transparency ? ` ` : '' strSlideXml += ' ' strSlideXml += ' ' strSlideXml += ` ` strSlideXml += ' ' strSlideXml += ' ' strSlideXml += '' } else { strSlideXml += `` strSlideXml += slideItemObj.options.transparency ? `` : '' strSlideXml += '' } if (sizing?.type) { const boxW = sizing.w ? getSmartParseNumber(sizing.w, 'X', slide._presLayout) : cx const boxH = sizing.h ? getSmartParseNumber(sizing.h, 'Y', slide._presLayout) : cy const boxX = getSmartParseNumber(sizing.x || 0, 'X', slide._presLayout) const boxY = getSmartParseNumber(sizing.y || 0, 'Y', slide._presLayout) strSlideXml += ImageSizingXml[sizing.type]({ w: imgWidth, h: imgHeight }, { w: boxW, h: boxH, x: boxX, y: boxY }) imgWidth = boxW imgHeight = boxH } else { strSlideXml += ' ' } strSlideXml += '' strSlideXml += '' strSlideXml += ' ' strSlideXml += ` ` strSlideXml += ` ` strSlideXml += ' ' strSlideXml += ` ` // EFFECTS > SHADOW: REF: @see http://officeopenxml.com/drwSp-effects.php if (slideItemObj.options.shadow && slideItemObj.options.shadow.type !== 'none') { slideItemObj.options.shadow.type = slideItemObj.options.shadow.type || 'outer' slideItemObj.options.shadow.blur = valToPts(slideItemObj.options.shadow.blur || 8) slideItemObj.options.shadow.offset = valToPts(slideItemObj.options.shadow.offset || 4) slideItemObj.options.shadow.angle = Math.round((slideItemObj.options.shadow.angle || 270) * 60000) slideItemObj.options.shadow.opacity = Math.round((slideItemObj.options.shadow.opacity || 0.75) * 100000) slideItemObj.options.shadow.color = slideItemObj.options.shadow.color || DEF_TEXT_SHADOW.color strSlideXml += '' strSlideXml += `` strSlideXml += `` strSlideXml += `` strSlideXml += `` strSlideXml += '' } strSlideXml += '' strSlideXml += '' break case SLIDE_OBJECT_TYPES.media: if (slideItemObj.mtype === 'online') { strSlideXml += '' strSlideXml += ' ' // IMPORTANT: ` strSlideXml += ' ' strSlideXml += ' ' strSlideXml += ` ` strSlideXml += ' ' strSlideXml += ' ' // NOTE: `blip` is diferent than videos; also there's no preview "p:extLst" above but exists in videos strSlideXml += ` ` // NOTE: Preview image is required! strSlideXml += ' ' strSlideXml += ` ` strSlideXml += ' ' strSlideXml += ' ' strSlideXml += '' } else { strSlideXml += '' strSlideXml += ' ' // IMPORTANT: ` strSlideXml += ' ' strSlideXml += ' ' strSlideXml += ` ` strSlideXml += ' ' strSlideXml += ' ' strSlideXml += ` ` strSlideXml += ' ' strSlideXml += ' ' strSlideXml += ' ' strSlideXml += ' ' strSlideXml += ` ` // NOTE: Preview image is required! strSlideXml += ' ' strSlideXml += ` ` strSlideXml += ' ' strSlideXml += ' ' strSlideXml += '' } break case SLIDE_OBJECT_TYPES.chart: strSlideXml += '' strSlideXml += ' ' strSlideXml += ` ` strSlideXml += ' ' strSlideXml += ` ${genXmlPlaceholder(placeholderObj)}` strSlideXml += ' ' strSlideXml += ` ` strSlideXml += ' ' strSlideXml += ' ' strSlideXml += ` ` strSlideXml += ' ' strSlideXml += ' ' strSlideXml += '' break default: strSlideXml += '' break } }) // STEP 4: Add slide numbers (if any) last if (slide._slideNumberProps) { // Set some defaults (done here b/c SlideNumber canbe added to masters or slides and has numerous entry points) if (!slide._slideNumberProps.align) slide._slideNumberProps.align = 'left' strSlideXml += '' strSlideXml += ' ' strSlideXml += ' ' strSlideXml += ' ' strSlideXml += ' ' strSlideXml += ' ' strSlideXml += '' + `` + `` + '' + ' ' + ' ' + '' strSlideXml += '' strSlideXml += '` if (slide._slideNumberProps.color) strSlideXml += genXmlColorSelection(slide._slideNumberProps.color) if (slide._slideNumberProps.fontFace) { strSlideXml += `` } strSlideXml += '' } strSlideXml += '' strSlideXml += '' if (slide._slideNumberProps.align.startsWith('l')) strSlideXml += '' else if (slide._slideNumberProps.align.startsWith('c')) strSlideXml += '' else if (slide._slideNumberProps.align.startsWith('r')) strSlideXml += '' else strSlideXml += '' strSlideXml += `` strSlideXml += `${slide._slideNum}` strSlideXml += '' } // STEP 5: Close spTree and finalize slide XML strSlideXml += '' strSlideXml += '' // LAST: Return return strSlideXml } /** * Transforms slide relations to XML string. * Extra relations that are not dynamic can be passed using the 2nd arg (e.g. theme relation in master file). * These relations use rId series that starts with 1-increased maximum of rIds used for dynamic relations. * @param {PresSlide | SlideLayout} slide - slide object whose relations are being transformed * @param {{ target: string; type: string }[]} defaultRels - array of default relations * @return {string} XML */ function slideObjectRelationsToXml (slide: PresSlide | SlideLayout, defaultRels: Array<{ target: string, type: string }>): string { let lastRid = 0 // stores maximum rId used for dynamic relations let strXml = '' + CRLF + '' // STEP 1: Add all rels for this Slide slide._rels.forEach((rel: ISlideRel) => { lastRid = Math.max(lastRid, rel.rId) if (rel.type.toLowerCase().includes('hyperlink')) { if (rel.data === 'slide') { strXml += `` } else { strXml += `` } } else if (rel.type.toLowerCase().includes('notesSlide')) { strXml += `` } }) ; (slide._relsChart || []).forEach((rel: ISlideRelChart) => { lastRid = Math.max(lastRid, rel.rId) strXml += `` }) ; (slide._relsMedia || []).forEach((rel: ISlideRelMedia) => { const relRid = rel.rId.toString() lastRid = Math.max(lastRid, rel.rId) if (rel.type.toLowerCase().includes('image')) { strXml += '' } else if (rel.type.toLowerCase().includes('audio')) { // As media has *TWO* rel entries per item, check for first one, if found add second rel with alt style if (strXml.includes(' Target="' + rel.Target + '"')) { strXml += '' } else { strXml += '' } } else if (rel.type.toLowerCase().includes('video')) { // As media has *TWO* rel entries per item, check for first one, if found add second rel with alt style if (strXml.includes(' Target="' + rel.Target + '"')) { strXml += '' } else { strXml += '' } } else if (rel.type.toLowerCase().includes('online')) { // As media has *TWO* rel entries per item, check for first one, if found add second rel with alt style if (strXml.includes(' Target="' + rel.Target + '"')) { strXml += '' } else { strXml += '' } } }) // STEP 2: Add default rels defaultRels.forEach((rel, idx) => { strXml += `` }) strXml += '' return strXml } /** * Generate XML Paragraph Properties * @param {ISlideObject|TextProps} textObj - text object * @param {boolean} isDefault - array of default relations * @return {string} XML */ function genXmlParagraphProperties (textObj: ISlideObject | TextProps, isDefault: boolean): string { let strXmlBullet = '' let strXmlLnSpc = '' let strXmlParaSpc = '' let strXmlTabStops = '' const tag = isDefault ? 'a:lvl1pPr' : 'a:pPr' let bulletMarL = valToPts(DEF_BULLET_MARGIN) let paragraphPropXml = `<${tag}${textObj.options.rtlMode ? ' rtl="1" ' : ''}` // A: Build paragraphProperties { // OPTION: align if (textObj.options.align) { switch (textObj.options.align) { case 'left': paragraphPropXml += ' algn="l"' break case 'right': paragraphPropXml += ' algn="r"' break case 'center': paragraphPropXml += ' algn="ctr"' break case 'justify': paragraphPropXml += ' algn="just"' break default: paragraphPropXml += '' break } } if (textObj.options.lineSpacing) { strXmlLnSpc = `` } else if (textObj.options.lineSpacingMultiple) { strXmlLnSpc = `` } // OPTION: indent if (textObj.options.indentLevel && !isNaN(Number(textObj.options.indentLevel)) && textObj.options.indentLevel > 0) { paragraphPropXml += ` lvl="${textObj.options.indentLevel}"` } // OPTION: Paragraph Spacing: Before/After if (textObj.options.paraSpaceBefore && !isNaN(Number(textObj.options.paraSpaceBefore)) && textObj.options.paraSpaceBefore > 0) { strXmlParaSpc += `` } if (textObj.options.paraSpaceAfter && !isNaN(Number(textObj.options.paraSpaceAfter)) && textObj.options.paraSpaceAfter > 0) { strXmlParaSpc += `` } // OPTION: bullet // NOTE: OOXML uses the unicode character set for Bullets // EX: Unicode Character 'BULLET' (U+2022) ==> '' if (typeof textObj.options.bullet === 'object') { if (textObj?.options?.bullet?.indent) bulletMarL = valToPts(textObj.options.bullet.indent) if (textObj.options.bullet.type) { if (textObj.options.bullet.type.toString().toLowerCase() === 'number') { paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL }" indent="-${bulletMarL}"` strXmlBullet = `` } } else if (textObj.options.bullet.characterCode) { let bulletCode = `&#x${textObj.options.bullet.characterCode};` // Check value for hex-ness (s/b 4 char hex) if (!/^[0-9A-Fa-f]{4}$/.test(textObj.options.bullet.characterCode)) { console.warn('Warning: `bullet.characterCode should be a 4-digit unicode charatcer (ex: 22AB)`!') bulletCode = BULLET_TYPES.DEFAULT } paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL }" indent="-${bulletMarL}"` strXmlBullet = '' } else if (textObj.options.bullet.code) { // @deprecated `bullet.code` v3.3.0 let bulletCode = `&#x${textObj.options.bullet.code};` // Check value for hex-ness (s/b 4 char hex) if (!/^[0-9A-Fa-f]{4}$/.test(textObj.options.bullet.code)) { console.warn('Warning: `bullet.code should be a 4-digit hex code (ex: 22AB)`!') bulletCode = BULLET_TYPES.DEFAULT } paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL }" indent="-${bulletMarL}"` strXmlBullet = '' } else { paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL }" indent="-${bulletMarL}"` strXmlBullet = `` } } else if (textObj.options.bullet) { paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL }" indent="-${bulletMarL}"` strXmlBullet = `` } else if (!textObj.options.bullet) { // We only add this when the user explicitely asks for no bullet, otherwise, it can override the master defaults! paragraphPropXml += ' indent="0" marL="0"' // FIX: ISSUE#589 - specify zero indent and marL or default will be hanging paragraph strXmlBullet = '' } // OPTION: tabStops if (textObj.options.tabStops && Array.isArray(textObj.options.tabStops)) { const tabStopsXml = textObj.options.tabStops.map(stop => ``).join('') strXmlTabStops = `${tabStopsXml}` } // B: Close Paragraph-Properties // IMPORTANT: strXmlLnSpc, strXmlParaSpc, and strXmlBullet require strict ordering - anything out of order is ignored. (PPT-Online, PPT for Mac) paragraphPropXml += '>' + strXmlLnSpc + strXmlParaSpc + strXmlBullet + strXmlTabStops if (isDefault) paragraphPropXml += genXmlTextRunProperties(textObj.options, true) paragraphPropXml += '' } return paragraphPropXml } /** * Generate XML Text Run Properties (`a:rPr`) * @param {ObjectOptions|TextPropsOptions} opts - text options * @param {boolean} isDefault - whether these are the default text run properties * @return {string} XML */ function genXmlTextRunProperties (opts: ObjectOptions | TextPropsOptions, isDefault: boolean): string { let runProps = '' const runPropsTag = isDefault ? 'a:defRPr' : 'a:rPr' // BEGIN runProperties (ex: ``) runProps += '<' + runPropsTag + ' lang="' + (opts.lang ? opts.lang : 'en-US') + '"' + (opts.lang ? ' altLang="en-US"' : '') runProps += opts.fontSize ? ` sz="${Math.round(opts.fontSize * 100)}"` : '' // NOTE: Use round so sizes like '7.5' wont cause corrupt presentations runProps += opts?.bold ? ` b="${opts.bold ? '1' : '0'}"` : '' runProps += opts?.italic ? ` i="${opts.italic ? '1' : '0'}"` : '' runProps += opts?.strike ? ` strike="${typeof opts.strike === 'string' ? opts.strike : 'sngStrike'}"` : '' if (typeof opts.underline === 'object' && opts.underline?.style) { runProps += ` u="${opts.underline.style}"` } else if (typeof opts.underline === 'string') { // DEPRECATED: opts.underline is an object as of v3.5.0 runProps += ` u="${String(opts.underline)}"` } else if (opts.hyperlink) { runProps += ' u="sng"' } if (opts.baseline) { runProps += ` baseline="${Math.round(opts.baseline * 50)}"` } else if (opts.subscript) { runProps += ' baseline="-40000"' } else if (opts.superscript) { runProps += ' baseline="30000"' } runProps += opts.charSpacing ? ` spc="${Math.round(opts.charSpacing * 100)}" kern="0"` : '' // IMPORTANT: Also disable kerning; otherwise text won't actually expand runProps += ' dirty="0">' // Color / Font / Highlight / Outline are children of , so add them now before closing the runProperties tag if (opts.color || opts.fontFace || opts.outline || (typeof opts.underline === 'object' && opts.underline.color)) { if (opts.outline && typeof opts.outline === 'object') { runProps += `${genXmlColorSelection(opts.outline.color || 'FFFFFF')}` } if (opts.color) runProps += genXmlColorSelection({ color: opts.color, transparency: opts.transparency }) if (opts.highlight) runProps += `${createColorElement(opts.highlight)}` if (typeof opts.underline === 'object' && opts.underline.color) runProps += `${genXmlColorSelection(opts.underline.color)}` if (opts.glow) runProps += `${createGlowElement(opts.glow, DEF_TEXT_GLOW)}` if (opts.fontFace) { // NOTE: 'cs' = Complex Script, 'ea' = East Asian (use "-120" instead of "0" - per Issue #174); ea must come first (Issue #174) runProps += `` } } // Hyperlink support if (opts.hyperlink) { if (typeof opts.hyperlink !== 'object') throw new Error('ERROR: text `hyperlink` option should be an object. Ex: `hyperlink:{url:\'https://github.com\'}` ') else if (!opts.hyperlink.url && !opts.hyperlink.slide) throw new Error('ERROR: \'hyperlink requires either `url` or `slide`\'') else if (opts.hyperlink.url) { // runProps += ''+ genXmlColorSelection('0000FF') +''; // Breaks PPT2010! (Issue#74) runProps += `' : '/>'}` } else if (opts.hyperlink.slide) { runProps += `' : '/>'}` } if (opts.color) { runProps += ' ' runProps += ' ' runProps += ' ' runProps += ' ' runProps += ' ' runProps += '' } } // END runProperties runProps += `` return runProps } /** * Build textBody text runs [``] for paragraphs [``] * @param {TextProps} textObj - Text object * @return {string} XML string */ function genXmlTextRun (textObj: TextProps): string { // NOTE: Dont create full rPr runProps for empty [lineBreak] runs // Why? The size of the lineBreak wont match (eg: below it will be 18px instead of the correct 36px) // Do this: /* */ // NOT this: /* */ // Return paragraph with text run return textObj.text ? `${genXmlTextRunProperties(textObj.options, false)}${encodeXmlEntities(textObj.text)}` : '' } /** * Builds `` tag for "genXmlTextBody()" * @param {ISlideObject | TableCell} slideObject - various options * @return {string} XML string */ function genXmlBodyProperties (slideObject: ISlideObject | TableCell): string { let bodyProperties = ' // A: Enable or disable textwrapping none or square bodyProperties += slideObject.options._bodyProp.wrap ? ' wrap="square"' : ' wrap="none"' // B: Textbox margins [padding] if (slideObject.options._bodyProp.lIns || slideObject.options._bodyProp.lIns === 0) bodyProperties += ` lIns="${slideObject.options._bodyProp.lIns}"` if (slideObject.options._bodyProp.tIns || slideObject.options._bodyProp.tIns === 0) bodyProperties += ` tIns="${slideObject.options._bodyProp.tIns}"` if (slideObject.options._bodyProp.rIns || slideObject.options._bodyProp.rIns === 0) bodyProperties += ` rIns="${slideObject.options._bodyProp.rIns}"` if (slideObject.options._bodyProp.bIns || slideObject.options._bodyProp.bIns === 0) bodyProperties += ` bIns="${slideObject.options._bodyProp.bIns}"` // C: Add rtl after margins bodyProperties += ' rtlCol="0"' // D: Add anchorPoints if (slideObject.options._bodyProp.anchor) bodyProperties += ' anchor="' + slideObject.options._bodyProp.anchor + '"' // VALS: [t,ctr,b] if (slideObject.options._bodyProp.vert) bodyProperties += ' vert="' + slideObject.options._bodyProp.vert + '"' // VALS: [eaVert,horz,mongolianVert,vert,vert270,wordArtVert,wordArtVertRtl] // E: Close ' instead of '' causes issues in PPT-2013! if (slideObject.options.fit === 'none') bodyProperties += '' // NOTE: Shrink does not work automatically - PowerPoint calculates the `fontScale` value dynamically upon resize // else if (slideObject.options.fit === 'shrink') bodyProperties += '' // MS-PPT > Format shape > Text Options: "Shrink text on overflow" else if (slideObject.options.fit === 'shrink') bodyProperties += '' else if (slideObject.options.fit === 'resize') bodyProperties += '' } // // DEPRECATED: below (@deprecated v3.3.0) if (slideObject.options.shrinkText) bodyProperties += '' // MS-PPT > Format shape > Text Options: "Shrink text on overflow" /* DEPRECATED: below (@deprecated v3.3.0) * MS-PPT > Format shape > Text Options: "Resize shape to fit text" [spAutoFit] * NOTE: Use of '' in lieu of '' below causes issues in PPT-2013 */ bodyProperties += slideObject.options._bodyProp.autoFit ? '' : '' // LAST: Close _bodyProp bodyProperties += '' } else { // DEFAULT: bodyProperties += ' wrap="square" rtlCol="0">' bodyProperties += '' } // LAST: Return Close _bodyProp return slideObject._type === SLIDE_OBJECT_TYPES.tablecell ? '' : bodyProperties } /** * Generate the XML for text and its options (bold, bullet, etc) including text runs (word-level formatting) * @param {ISlideObject|TableCell} slideObj - slideObj or tableCell * @note PPT text lines [lines followed by line-breaks] are created using

-aragraph's * @note Bullets are a paragragh-level formatting device * @template * * * * * * * * * * textbox text * * * * * @returns XML containing the param object's text and formatting */ export function genXmlTextBody (slideObj: ISlideObject | TableCell): string { const opts: ObjectOptions = slideObj.options || {} let tmpTextObjects: TextProps[] = [] const arrTextObjects: TextProps[] = [] // FIRST: Shapes without text, etc. may be sent here during build, but have no text to render so return an empty string if (opts && slideObj._type !== SLIDE_OBJECT_TYPES.tablecell && (typeof slideObj.text === 'undefined' || slideObj.text === null)) return '' // STEP 1: Start textBody let strSlideXml = slideObj._type === SLIDE_OBJECT_TYPES.tablecell ? '' : '' // STEP 2: Add bodyProperties { // A: 'bodyPr' strSlideXml += genXmlBodyProperties(slideObj) // B: 'lstStyle' // NOTE: shape type 'LINE' has different text align needs (a lstStyle.lvl1pPr between bodyPr and p) // FIXME: LINE horiz-align doesnt work (text is always to the left inside line) (FYI: the PPT code diff is substantial!) if (opts.h === 0 && opts.line && opts.align) strSlideXml += '' else if (slideObj._type === 'placeholder') strSlideXml += `${genXmlParagraphProperties(slideObj, true)}` else strSlideXml += '' } /* STEP 3: Modify slideObj.text to array CASES: addText( 'string' ) // string addText( 'line1\n line2' ) // string with lineBreak addText( {text:'word1'} ) // TextProps object addText( ['barry','allen'] ) // array of strings addText( [{text:'word1'}, {text:'word2'}] ) // TextProps object array addText( [{text:'line1\n line2'}, {text:'end word'}] ) // TextProps object array with lineBreak */ if (typeof slideObj.text === 'string' || typeof slideObj.text === 'number') { // Handle cases 1,2 tmpTextObjects.push({ text: slideObj.text.toString(), options: opts || {} }) } else if (slideObj.text && !Array.isArray(slideObj.text) && typeof slideObj.text === 'object' && Object.keys(slideObj.text).includes('text')) { // } else if (!Array.isArray(slideObj.text) && slideObj.text!.hasOwnProperty('text')) { // 20210706: replaced with below as ts compiler rejected it // Handle case 3 tmpTextObjects.push({ text: slideObj.text || '', options: slideObj.options || {} }) } else if (Array.isArray(slideObj.text)) { // Handle cases 4,5,6 // NOTE: use cast as text is TextProps[]|TableCell[] and their `options` dont overlap (they share the same TextBaseProps though) tmpTextObjects = (slideObj.text as TextProps[]).map(item => ({ text: item.text, options: item.options })) } // STEP 4: Iterate over text objects, set text/options, break into pieces if '\n'/breakLine found tmpTextObjects.forEach((itext, idx) => { if (!itext.text) itext.text = '' // A: Set options itext.options = itext.options || opts || {} if (idx === 0 && itext.options && !itext.options.bullet && opts.bullet) itext.options.bullet = opts.bullet // B: Cast to text-object and fix line-breaks (if needed) if (typeof itext.text === 'string' || typeof itext.text === 'number') { // 1: Convert "\n" or any variation into CRLF itext.text = itext.text.toString().replace(/\r*\n/g, CRLF) } // C: If text string has line-breaks, then create a separate text-object for each (much easier than dealing with split inside a loop below) // NOTE: Filter for trailing lineBreak prevents the creation of an empty textObj as the last item if (itext.text.includes(CRLF) && itext.text.match(/\n$/g) === null) { itext.text.split(CRLF).forEach(line => { itext.options.breakLine = true arrTextObjects.push({ text: line, options: itext.options }) }) } else { arrTextObjects.push(itext) } }) // STEP 5: Group textObj into lines by checking for lineBreak, bullets, alignment change, etc. const arrLines: TextProps[][] = [] let arrTexts: TextProps[] = [] arrTextObjects.forEach((textObj, idx) => { // A: Align or Bullet trigger new line if (arrTexts.length > 0 && (textObj.options.align || opts.align)) { // Only start a new paragraph when align *changes* if (textObj.options.align !== arrTextObjects[idx - 1].options.align) { arrLines.push(arrTexts) arrTexts = [] } } else if (arrTexts.length > 0 && textObj.options.bullet && arrTexts.length > 0) { arrLines.push(arrTexts) arrTexts = [] textObj.options.breakLine = false // For cases with both `bullet` and `brekaLine` - prevent double lineBreak } // B: Add this text to current line arrTexts.push(textObj) // C: BreakLine begins new line **after** adding current text if (arrTexts.length > 0 && textObj.options.breakLine) { // Avoid starting a para right as loop is exhausted if (idx + 1 < arrTextObjects.length) { arrLines.push(arrTexts) arrTexts = [] } } // D: Flush buffer if (idx + 1 === arrTextObjects.length) arrLines.push(arrTexts) }) // STEP 6: Loop over each line and create paragraph props, text run, etc. arrLines.forEach(line => { let reqsClosingFontSize = false // A: Start paragraph, add paraProps strSlideXml += '' // NOTE: `rtlMode` is like other opts, its propagated up to each text:options, so just check the 1st one let paragraphPropXml = ` { // A: Set line index textObj.options._lineIdx = idx // A.1: Add soft break if not the first run of the line. if (idx > 0 && textObj.options.softBreakBefore) { strSlideXml += '' } // B: Inherit pPr-type options from parent shape's `options` textObj.options.align = textObj.options.align || opts.align textObj.options.lineSpacing = textObj.options.lineSpacing || opts.lineSpacing textObj.options.lineSpacingMultiple = textObj.options.lineSpacingMultiple || opts.lineSpacingMultiple textObj.options.indentLevel = textObj.options.indentLevel || opts.indentLevel textObj.options.paraSpaceBefore = textObj.options.paraSpaceBefore || opts.paraSpaceBefore textObj.options.paraSpaceAfter = textObj.options.paraSpaceAfter || opts.paraSpaceAfter paragraphPropXml = genXmlParagraphProperties(textObj, false) strSlideXml += paragraphPropXml.replace('', '') // IMPORTANT: Empty "pPr" blocks will generate needs-repair/corrupt msg // C: Inherit any main options (color, fontSize, etc.) // NOTE: We only pass the text.options to genXmlTextRun (not the Slide.options), // so the run building function cant just fallback to Slide.color, therefore, we need to do that here before passing options below. // FILTER RULE: Hyperlinks should not inherit `color` from main options (let PPT default to local color, eg: blue on MacOS) Object.entries(opts).filter(([key]) => !(textObj.options.hyperlink && key === 'color')).forEach(([key, val]) => { // if (textObj.options.hyperlink && key === 'color') null // NOTE: This loop will pick up unecessary keys (`x`, etc.), but it doesnt hurt anything if (key !== 'bullet' && !textObj.options[key]) textObj.options[key] = val }) // D: Add formatted textrun strSlideXml += genXmlTextRun(textObj) // E: Flag close fontSize for empty [lineBreak] elements if ((!textObj.text && opts.fontSize) || textObj.options.fontSize) { reqsClosingFontSize = true opts.fontSize = opts.fontSize || textObj.options.fontSize } }) /* C: Append 'endParaRPr' (when needed) and close current open paragraph * NOTE: (ISSUE#20, ISSUE#193): Add 'endParaRPr' with font/size props or PPT default (Arial/18pt en-us) is used making row "too tall"/not honoring options */ if (slideObj._type === SLIDE_OBJECT_TYPES.tablecell && (opts.fontSize || opts.fontFace)) { if (opts.fontFace) { strSlideXml += `' strSlideXml += `` strSlideXml += `` strSlideXml += `` strSlideXml += '' } else { strSlideXml += `' } } else if (reqsClosingFontSize) { // Empty [lineBreak] lines should not contain runProp, however, they need to specify fontSize in `endParaRPr` strSlideXml += `' } else { strSlideXml += `` // Added 20180101 to address PPT-2007 issues } // D: End paragraph strSlideXml += '' }) // IMPORTANT: An empty txBody will cause "needs repair" error! Add

content if missing. // [FIXED in v3.13.0]: This fixes issue with table auto-paging where some cells w/b empty on subsequent pages. /* */ if (strSlideXml.indexOf('') === -1) { strSlideXml += '' } // STEP 7: Close the textBody strSlideXml += slideObj._type === SLIDE_OBJECT_TYPES.tablecell ? '' : '' // LAST: Return XML return strSlideXml } /** * Generate an XML Placeholder * @param {ISlideObject} placeholderObj * @returns XML */ export function genXmlPlaceholder (placeholderObj: ISlideObject): string { if (!placeholderObj) return '' const placeholderIdx = placeholderObj.options?._placeholderIdx ? placeholderObj.options._placeholderIdx : '' const placeholderTyp = placeholderObj.options?._placeholderType ? placeholderObj.options._placeholderType : '' const placeholderType: string = placeholderTyp && PLACEHOLDER_TYPES[placeholderTyp] ? (PLACEHOLDER_TYPES[placeholderTyp]).toString() : '' return ` 0 ? ' hasCustomPrompt="1"' : ''} />` } // XML-GEN: First 6 functions create the base /ppt files /** * Generate XML ContentType * @param {PresSlide[]} slides - slides * @param {SlideLayout[]} slideLayouts - slide layouts * @param {PresSlide} masterSlide - master slide * @returns XML */ export function makeXmlContTypes (slides: PresSlide[], slideLayouts: SlideLayout[], masterSlide?: PresSlide): string { let strXml = '' + CRLF strXml += '' strXml += '' strXml += '' strXml += '' strXml += '' strXml += '' // STEP 1: Add standard/any media types used in Presentation strXml += '' strXml += '' strXml += '' // NOTE: Hard-Code this extension as it wont be created in loop below (as extn !== type) strXml += '' // NOTE: Hard-Code this extension as it wont be created in loop below (as extn !== type) slides.forEach(slide => { (slide._relsMedia || []).forEach(rel => { if (rel.type !== 'image' && rel.type !== 'online' && rel.type !== 'chart' && rel.extn !== 'm4v' && !strXml.includes(rel.type)) { strXml += '' } }) }) strXml += '' strXml += '' // STEP 2: Add presentation and slide master(s)/slide(s) strXml += '' strXml += '' slides.forEach((slide, idx) => { strXml += `` strXml += `` // Add charts if any slide._relsChart.forEach(rel => { strXml += `` }) }) // STEP 3: Core PPT strXml += '' strXml += '' strXml += '' strXml += '' // STEP 4: Add Slide Layouts slideLayouts.forEach((layout, idx) => { strXml += `` ; (layout._relsChart || []).forEach(rel => { strXml += ' ' }) }) // STEP 5: Add notes slide(s) slides.forEach((_slide, idx) => { strXml += `` }) // STEP 6: Add rels masterSlide._relsChart.forEach(rel => { strXml += ' ' }) masterSlide._relsMedia.forEach(rel => { if (rel.type !== 'image' && rel.type !== 'online' && rel.type !== 'chart' && rel.extn !== 'm4v' && !strXml.includes(rel.type)) { strXml += ' ' } }) // LAST: Finish XML (Resume core) strXml += ' ' strXml += ' ' strXml += '' return strXml } /** * Creates `_rels/.rels` * @returns XML */ export function makeXmlRootRels (): string { return `${CRLF} ` } /** * Creates `docProps/app.xml` * @param {PresSlide[]} slides - Presenation Slides * @param {string} company - "Company" metadata * @returns XML */ export function makeXmlApp (slides: PresSlide[], company: string): string { return `${CRLF} 0 0 Microsoft Office PowerPoint On-screen Show (16:9) 0 ${slides.length} ${slides.length} 0 0 false Fonts Used 2 Theme 1 Slide Titles ${slides.length} Arial Calibri Office Theme ${slides.map((_slideObj, idx) => `Slide ${idx + 1}`).join('')} ${company} false false false 16.0000 ` } /** * Creates `docProps/core.xml` * @param {string} title - metadata data * @param {string} subject - metadata data * @param {string} author - metadata value * @param {string} revision - metadata value * @returns XML */ export function makeXmlCore (title: string, subject: string, author: string, revision: string): string { return ` ${encodeXmlEntities(title)} ${encodeXmlEntities(subject)} ${encodeXmlEntities(author)} ${encodeXmlEntities(author)} ${revision} ${new Date().toISOString().replace(/\.\d\d\dZ/, 'Z')} ${new Date().toISOString().replace(/\.\d\d\dZ/, 'Z')} ` } /** * Creates `ppt/_rels/presentation.xml.rels` * @param {PresSlide[]} slides - Presenation Slides * @returns XML */ export function makeXmlPresentationRels (slides: PresSlide[]): string { let intRelNum = 1 let strXml = '' + CRLF strXml += '' strXml += '' for (let idx = 1; idx <= slides.length; idx++) { strXml += `` } intRelNum++ strXml += `` + `` + `` + `` + `` + '' return strXml } // XML-GEN: Functions that run 1-N times (once for each Slide) /** * Generates XML for the slide file (`ppt/slides/slide1.xml`) * @param {PresSlide} slide - the slide object to transform into XML * @return {string} XML */ export function makeXmlSlide (slide: PresSlide): string { return ( `${CRLF}` + '` + `${slideObjectToXml(slide)}` + '' ) } /** * Get text content of Notes from Slide * @param {PresSlide} slide - the slide object to transform into XML * @return {string} notes text */ export function getNotesFromSlide (slide: PresSlide): string { let notesText = '' slide._slideObjects.forEach(data => { if (data._type === SLIDE_OBJECT_TYPES.notes) notesText += data?.text && data.text[0] ? data.text[0].text : '' }) return notesText.replace(/\r*\n/g, CRLF) } /** * Generate XML for Notes Master (notesMaster1.xml) * @returns {string} XML */ export function makeXmlNotesMaster (): string { return `${CRLF}7/23/19Click to edit Master text stylesSecond levelThird levelFourth levelFifth level‹#›` } /** * Creates Notes Slide (`ppt/notesSlides/notesSlide1.xml`) * @param {PresSlide} slide - the slide object to transform into XML * @return {string} XML */ export function makeXmlNotesSlide (slide: PresSlide): string { return ( `${CRLF}${encodeXmlEntities(getNotesFromSlide(slide))}${slide._slideNum}` ) } /** * Generates the XML layout resource from a layout object * @param {SlideLayout} layout - slide layout (master) * @return {string} XML */ export function makeXmlLayout (layout: SlideLayout): string { return ` ${slideObjectToXml(layout)} ` } /** * Creates Slide Master 1 (`ppt/slideMasters/slideMaster1.xml`) * @param {PresSlide} slide - slide object that represents master slide layout * @param {SlideLayout[]} layouts - slide layouts * @return {string} XML */ export function makeXmlMaster (slide: PresSlide, layouts: SlideLayout[]): string { // NOTE: Pass layouts as static rels because they are not referenced any time const layoutDefs = layouts.map((_layoutDef, idx) => ``) let strXml = '' + CRLF strXml += '' strXml += slideObjectToXml(slide) strXml += '' strXml += '' + layoutDefs.join('') + '' strXml += '' strXml += '' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '' strXml += '' return strXml } /** * Generates XML string for a slide layout relation file * @param {number} layoutNumber - 1-indexed number of a layout that relations are generated for * @param {SlideLayout[]} slideLayouts - Slide Layouts * @return {string} XML */ export function makeXmlSlideLayoutRel (layoutNumber: number, slideLayouts: SlideLayout[]): string { return slideObjectRelationsToXml(slideLayouts[layoutNumber - 1], [ { target: '../slideMasters/slideMaster1.xml', type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster', }, ]) } /** * Creates `ppt/_rels/slide*.xml.rels` * @param {PresSlide[]} slides * @param {SlideLayout[]} slideLayouts - Slide Layout(s) * @param {number} `slideNumber` 1-indexed number of a layout that relations are generated for * @return {string} XML */ export function makeXmlSlideRel (slides: PresSlide[], slideLayouts: SlideLayout[], slideNumber: number): string { return slideObjectRelationsToXml(slides[slideNumber - 1], [ { target: `../slideLayouts/slideLayout${getLayoutIdxForSlide(slides, slideLayouts, slideNumber)}.xml`, type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout', }, { target: `../notesSlides/notesSlide${slideNumber}.xml`, type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide', }, ]) } /** * Generates XML string for a slide relation file. * @param {number} slideNumber - 1-indexed number of a layout that relations are generated for * @return {string} XML */ export function makeXmlNotesSlideRel (slideNumber: number): string { return ` ` } /** * Creates `ppt/slideMasters/_rels/slideMaster1.xml.rels` * @param {PresSlide} masterSlide - Slide object * @param {SlideLayout[]} slideLayouts - Slide Layouts * @return {string} XML */ export function makeXmlMasterRel (masterSlide: PresSlide, slideLayouts: SlideLayout[]): string { const defaultRels = slideLayouts.map((_layoutDef, idx) => ({ target: `../slideLayouts/slideLayout${idx + 1}.xml`, type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout', })) defaultRels.push({ target: '../theme/theme1.xml', type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme' }) return slideObjectRelationsToXml(masterSlide, defaultRels) } /** * Creates `ppt/notesMasters/_rels/notesMaster1.xml.rels` * @return {string} XML */ export function makeXmlNotesMasterRel (): string { return `${CRLF} ` } /** * For the passed slide number, resolves name of a layout that is used for. * @param {PresSlide[]} slides - srray of slides * @param {SlideLayout[]} slideLayouts - array of slideLayouts * @param {number} slideNumber * @return {number} slide number */ function getLayoutIdxForSlide (slides: PresSlide[], slideLayouts: SlideLayout[], slideNumber: number): number { for (let i = 0; i < slideLayouts.length; i++) { if (slideLayouts[i]._name === slides[slideNumber - 1]._slideLayout._name) { return i + 1 } } // IMPORTANT: Return 1 (for `slideLayout1.xml`) when no def is found // So all objects are in Layout1 and every slide that references it uses this layout. return 1 } // XML-GEN: Last 5 functions create root /ppt files /** * Creates `ppt/theme/theme1.xml` * @return {string} XML */ export function makeXmlTheme (pres: IPresentationProps): string { const majorFont = pres.theme?.headFontFace ? `` : '' const minorFont = pres.theme?.bodyFontFace ? `` : '' return `${majorFont}${minorFont}` } /** * Create presentation file (`ppt/presentation.xml`) * @see https://docs.microsoft.com/en-us/office/open-xml/structure-of-a-presentationml-document * @see http://www.datypic.com/sc/ooxml/t-p_CT_Presentation.html * @param {IPresentationProps} pres - presentation * @return {string} XML */ export function makeXmlPresentation (pres: IPresentationProps): string { let strXml = `${CRLF}` + '` // STEP 1: Add slide master (SPEC: tag 1 under ) strXml += '' // STEP 2: Add all Slides (SPEC: tag 3 under ) strXml += '' pres.slides.forEach(slide => (strXml += ``)) strXml += '' // STEP 3: Add Notes Master (SPEC: tag 2 under ) // (NOTE: length+2 is from `presentation.xml.rels` func (since we have to match this rId, we just use same logic)) // IMPORTANT: In this order (matches PPT2019) PPT will give corruption message on open! // IMPORTANT: Placing this before `` causes warning in modern powerpoint! // IMPORTANT: Presentations open without warning Without this line, however, the pres isnt preview in Finder anymore or viewable in iOS! strXml += `` // STEP 4: Add sizes strXml += `` strXml += `` // STEP 5: Add text styles strXml += '' for (let idy = 1; idy < 10; idy++) { strXml += `` + '' + `` } strXml += '' // STEP 6: Add Sections (if any) if (pres.sections && pres.sections.length > 0) { strXml += '' strXml += '' pres.sections.forEach(sect => { strXml += `` sect._slides.forEach(slide => (strXml += ``)) strXml += '' }) strXml += '' strXml += '' strXml += '' } // Done strXml += '' return strXml } /** * Create `ppt/presProps.xml` * @return {string} XML */ export function makeXmlPresProps (): string { return `${CRLF}` } /** * Create `ppt/tableStyles.xml` * @see: http://openxmldeveloper.org/discussions/formats/f/13/p/2398/8107.aspx * @return {string} XML */ export function makeXmlTableStyles (): string { return `${CRLF}` } /** * Creates `ppt/viewProps.xml` * @return {string} XML */ export function makeXmlViewProps (): string { return `${CRLF}` } /** * Checks shadow options passed by user and performs corrections if needed. * @param {ShadowProps} shadowProps - shadow options */ export function correctShadowOptions (shadowProps: ShadowProps): void { if (!shadowProps || typeof shadowProps !== 'object') { // console.warn("`shadow` options must be an object. Ex: `{shadow: {type:'none'}}`") return } // OPT: `type` if (shadowProps.type !== 'outer' && shadowProps.type !== 'inner' && shadowProps.type !== 'none') { console.warn('Warning: shadow.type options are `outer`, `inner` or `none`.') shadowProps.type = 'outer' } // OPT: `angle` if (shadowProps.angle) { // A: REALITY-CHECK if (isNaN(Number(shadowProps.angle)) || shadowProps.angle < 0 || shadowProps.angle > 359) { console.warn('Warning: shadow.angle can only be 0-359') shadowProps.angle = 270 } // B: ROBUST: Cast any type of valid arg to int: '12', 12.3, etc. -> 12 shadowProps.angle = Math.round(Number(shadowProps.angle)) } // OPT: `opacity` if (shadowProps.opacity) { // A: REALITY-CHECK if (isNaN(Number(shadowProps.opacity)) || shadowProps.opacity < 0 || shadowProps.opacity > 1) { console.warn('Warning: shadow.opacity can only be 0-1') shadowProps.opacity = 0.75 } // B: ROBUST: Cast any type of valid arg to int: '12', 12.3, etc. -> 12 shadowProps.opacity = Number(shadowProps.opacity) } }