UNPKG

pptx-automizer

Version:

A template based pptx generator

672 lines 27.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.XmlSlideHelper = exports.mapUriType = exports.nsMain = void 0; const xml_helper_1 = require("./xml-helper"); const xml_template_helper_1 = require("./xml-template-helper"); const xml_placeholder_helper_1 = __importDefault(require("./xml-placeholder-helper")); exports.nsMain = 'http://schemas.openxmlformats.org/presentationml/2006/main'; exports.mapUriType = { 'http://schemas.openxmlformats.org/drawingml/2006/table': 'table', 'http://schemas.openxmlformats.org/drawingml/2006/chart': 'chart', 'http://schemas.microsoft.com/office/drawing/2014/chartex': 'chartEx', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject': 'oleObject', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink': 'hyperlink', }; /** * Class that represents an XML slide helper */ class XmlSlideHelper { /** * Constructor for the XmlSlideHelper class. * @param {XmlDocument} slideXml - The slide XML document to be used by the helper. * @param params */ constructor(slideXml, params) { /** * Fetches an XML file from the given path and extracts the dimensions. * * @param {string} path - The path of the XML file. * @returns {Promise<{ width: number; height: number } | null>} - A promise that resolves with an object containing the width and height, or `null` if there was an error. */ this.getAndExtractDimensions = (path) => __awaiter(this, void 0, void 0, function* () { try { const xml = yield xml_helper_1.XmlHelper.getXmlFromArchive(this.sourceArchive, path); if (!xml) return null; const sldSz = xml.getElementsByTagName('p:sldSz')[0]; if (sldSz) { const width = XmlSlideHelper.parseCoordinate(sldSz, 'cx'); const height = XmlSlideHelper.parseCoordinate(sldSz, 'cy'); return { width, height }; } return null; } catch (error) { console.warn(`Error while fetching XML from path ${path}: ${error}`); return null; } }); if (!slideXml) { throw Error('Slide XML is not defined'); } this.slideXml = slideXml; this.sourceArchive = params.sourceArchive; this.slideNumber = params.slideNumber; this.sourceLayoutId = params.sourceLayoutId; } getSlideCreationId() { const creationIdItem = this.slideXml .getElementsByTagName('p14:creationId') .item(0); if (!creationIdItem) { return; } const creationIdSlide = creationIdItem.getAttribute('val'); if (!creationIdSlide) { return; } return Number(creationIdSlide); } getSlideLayout() { return __awaiter(this, void 0, void 0, function* () { const xml = yield this.getSlideLayoutXml(this.sourceLayoutId); return xml_template_helper_1.XmlTemplateHelper.getLayoutInfo(xml); }); } /** * Get an ElementInfo object for the target element on the slide. * @param selector */ getElement(selector) { return __awaiter(this, void 0, void 0, function* () { const shapeNode = xml_helper_1.XmlHelper.isElementCreationId(selector) ? xml_helper_1.XmlHelper.findByCreationId(this.slideXml, selector) : xml_helper_1.XmlHelper.findByName(this.slideXml, selector); return XmlSlideHelper.getElementInfo(shapeNode); }); } /** * Get an array of ElementInfo objects for all named elements on a slide. * @param filterTags Use an array of strings to filter the output array * @param layoutPlaceholders */ getAllElements(filterTags, layoutPlaceholders) { const elementInfo = []; try { const shapeNodes = this.getNamedElements(filterTags); shapeNodes.forEach((shapeNode) => { elementInfo.push(XmlSlideHelper.getElementInfo(shapeNode, layoutPlaceholders)); }); } catch (error) { console.error(error); throw new Error(`Failed to retrieve elements: ${error.message}`); } return elementInfo; } /** * Get all text element IDs from the slide. * @return {string[]} An array of text element IDs. */ getAllTextElementIds(useCreationIds) { const elementIds = []; try { elementIds.push(...this.getAllElements(['sp']) .filter((element) => element.hasTextBody) .map((element) => (useCreationIds ? element.id : element.name))); } catch (error) { console.error(error); throw new Error(`Failed to retrieve text element IDs: ${error.message}`); } return elementIds; } static getElementInfo(slideElement, layoutPlaceholders) { const selector = this.getSelector(slideElement); const position = XmlSlideHelper.parseShapeCoordinates(slideElement); const type = XmlSlideHelper.getElementType(slideElement); const visualType = XmlSlideHelper.getElementVisualType(slideElement); return { name: selector.name, id: selector.creationId, creationId: selector.creationId, nameIdx: selector.nameIdx, type, visualType, position, getPlaceholderInfo: () => { return xml_placeholder_helper_1.default.getPlaceholderInfo(slideElement, layoutPlaceholders); }, hasTextBody: !!XmlSlideHelper.getTextBody(slideElement), getXmlElement: () => slideElement, getText: () => XmlSlideHelper.parseTextFragments(slideElement), getParagraphs: () => XmlSlideHelper.parseTextParagraphs(slideElement), getParagraphGroups: () => XmlSlideHelper.parseParagraphGroups(slideElement), getTableInfo: () => XmlSlideHelper.readTableInfo(slideElement), getAltText: () => XmlSlideHelper.getImageAltText(slideElement), getGroupInfo: () => XmlSlideHelper.parseGroupInfo(slideElement), }; } /** * Retrieves a list of all named elements on a slide. Automation requires at least a name. * @param filterTags Use an array of strings to filter the output array */ getNamedElements(filterTags) { const skipTags = ['spTree']; const nvPrs = this.slideXml.getElementsByTagNameNS(exports.nsMain, 'cNvPr'); const namedElements = []; xml_helper_1.XmlHelper.modifyCollection(nvPrs, (nvPr) => { var _a; const parentNode = (_a = nvPr === null || nvPr === void 0 ? void 0 : nvPr.parentNode) === null || _a === void 0 ? void 0 : _a.parentNode; const parentTag = parentNode === null || parentNode === void 0 ? void 0 : parentNode.localName; if (!skipTags.includes(parentTag) && (!(filterTags === null || filterTags === void 0 ? void 0 : filterTags.length) || filterTags.includes(parentTag))) { namedElements.push(parentNode); } }); return namedElements; } static getTextBody(shapeNode) { return shapeNode.getElementsByTagNameNS(exports.nsMain, 'txBody').item(0); } static parseTextFragments(shapeNode) { const txBody = XmlSlideHelper.getTextBody(shapeNode); const textFragments = []; if (!txBody) { return textFragments; } const texts = txBody.getElementsByTagName('a:t'); for (let t = 0; t < texts.length; t++) { const text = texts.item(t); textFragments.push(text.textContent); } return textFragments; } static parseParagraphGroups(shapeNode) { const rawParagraphs = XmlSlideHelper.parseTextParagraphs(shapeNode); return XmlSlideHelper.groupSimilarParagraphs(rawParagraphs); } static parseTextParagraphs(shapeNode) { const textParagraphs = []; // Find txBody element first const txBody = shapeNode.getElementsByTagName('p:txBody')[0] || shapeNode.getElementsByTagName('a:txBody')[0]; if (!txBody) return textParagraphs; // Get all paragraph elements const paragraphs = txBody.getElementsByTagName('a:p'); for (const p of Array.from(paragraphs)) { const paragraph = { texts: [] }; // Check for paragraph properties (indent and bullet) const pPr = p.getElementsByTagName('a:pPr')[0]; if (pPr) { XmlSlideHelper.setParagraphProperties(pPr, paragraph); } // Get all text runs in the paragraph const runs = p.getElementsByTagName('a:r'); const texts = []; for (const run of Array.from(runs)) { XmlSlideHelper.setTextProperties(run, paragraph); // Get text content const textElements = run.getElementsByTagName('a:t'); for (const textElement of Array.from(textElements)) { texts.push(textElement.textContent || ''); } // Check if the next sibling after rPr is a line break const nextSibling = run.nextSibling; if (nextSibling && nextSibling.nodeName === 'a:br') { texts.push(`\n`); } } // Only add paragraphs that have text content if (texts.length > 0) { paragraph.texts = texts; textParagraphs.push(paragraph); } } return textParagraphs; } static setTextProperties(run, paragraph) { const rPr = run.getElementsByTagName('a:rPr')[0]; if (rPr) { const isBold = rPr.getAttribute('b') === '1'; const isUnderlined = rPr.getAttribute('u'); const isItalic = rPr.getAttribute('i') === '1'; const fontSize = parseInt(rPr.getAttribute('sz') || '0') / 100; // Convert to points if (isBold) paragraph.isBold = true; if (isItalic) paragraph.isItalic = true; if (isUnderlined) paragraph.isUnderlined = true; if (fontSize) paragraph.fontSize = fontSize; } } static setParagraphProperties(pPr, paragraph) { const marL = pPr.getAttribute('marL'); if (marL) { paragraph.indent = parseInt(marL); } const buChar = pPr.getElementsByTagName('a:buChar')[0]; if (buChar) { paragraph.bullet = buChar.getAttribute('char'); } // Check for numbered list const buAutoNum = pPr.getElementsByTagName('a:buAutoNum')[0]; if (buAutoNum) { paragraph.isNumbered = true; paragraph.numberingType = buAutoNum.getAttribute('type') || undefined; paragraph.startAt = buAutoNum.getAttribute('startAt') || undefined; } // Check for alignment const algn = pPr.getAttribute('algn'); if (algn) { paragraph.align = algn; } } static groupSimilarParagraphs(paragraphs) { const groups = []; let currentGroup = null; const getDefinedProperties = (paragraph) => { const properties = {}; const propertyKeys = [ 'fontSize', 'isBold', 'isItalic', 'isUnderlined', // 'indent', 'align', 'isNumbered', 'numberingType', 'bullet', 'startAt', 'breaks', ]; for (const key of propertyKeys) { if (paragraph[key] !== undefined) { properties[key] = paragraph[key]; } } return properties; }; for (const paragraph of paragraphs) { const properties = getDefinedProperties(paragraph); // Helper function to check if properties match const propertiesMatch = (a, b) => { return JSON.stringify(a) === JSON.stringify(b); }; // If we have no current group or properties don't match, create new group if (!currentGroup || !propertiesMatch(currentGroup.properties, properties)) { currentGroup = { properties, texts: [], }; groups.push(currentGroup); } // Add text to current group currentGroup.texts.push(paragraph.texts.join('')); } return groups; } static getNonVisibleProperties(shapeNode) { return shapeNode.getElementsByTagNameNS(exports.nsMain, 'cNvPr').item(0); } static getImageAltText(slideElement) { const cNvPr = XmlSlideHelper.getNonVisibleProperties(slideElement); if (cNvPr) { return cNvPr.getAttribute('descr'); } } static getElementName(slideElement) { const cNvPr = XmlSlideHelper.getNonVisibleProperties(slideElement); if (cNvPr) { return cNvPr.getAttribute('name'); } } static getElementCreationId(slideElement, stripBrackets) { const cNvPr = XmlSlideHelper.getNonVisibleProperties(slideElement); if (cNvPr) { const creationIdElement = cNvPr .getElementsByTagName('a16:creationId') .item(0); if (creationIdElement) { const id = creationIdElement.getAttribute('id'); if (stripBrackets) return id.replace('{', '').replace('}', ''); return id; } } } static getElementNameIdx(slideElement) { const elementName = XmlSlideHelper.getElementName(slideElement); if (!elementName) { return 0; } // Find the parent slide element (spTree) to search all elements on the slide const currentNode = xml_helper_1.XmlHelper.getClosestParent('p:spTree', slideElement); if (!currentNode) { return 0; // Unable to find slide parent } const spTree = currentNode; // Get all named elements from the slide const namedElements = []; const nvPrs = spTree.getElementsByTagNameNS(exports.nsMain, 'cNvPr'); xml_helper_1.XmlHelper.modifyCollection(nvPrs, (nvPr) => { const parentNode = nvPr.parentNode.parentNode; const name = nvPr.getAttribute('name'); if (name === elementName) { namedElements.push(parentNode); } }); // Find the index of the current element in the array of elements with the same name for (let i = 0; i < namedElements.length; i++) { if (namedElements[i] === slideElement) { return i; } } return 0; } /** * Parses local tag name to specify element type in case it is a 'graphicFrame'. * @param slideElementParent */ static getElementType(slideElementParent) { let type = slideElementParent === null || slideElementParent === void 0 ? void 0 : slideElementParent.localName; const getUri = () => { const graphicData = slideElementParent.getElementsByTagName('a:graphicData')[0]; return graphicData.getAttribute('uri'); }; switch (type) { case 'graphicFrame': type = exports.mapUriType[getUri()] || type; break; case 'oleObj': type = 'OLEObject'; break; } // Check for hyperlinks const hasHyperlink = slideElementParent.getElementsByTagName('a:hlinkClick'); if (hasHyperlink.length > 0) { type = 'Hyperlink'; } return type; } static parseShapeCoordinates(slideElementParent, returnDefaults) { const xFrmsA = slideElementParent.getElementsByTagName('a:xfrm'); const xFrmsP = slideElementParent.getElementsByTagName('p:xfrm'); const xFrms = xFrmsP.item(0) ? xFrmsP : xFrmsA; const position = { x: 0, y: 0, cx: 0, cy: 0, rot: 0, }; if (!xFrms.item(0)) { if (returnDefaults === false) { return null; } return position; } const xFrm = xFrms.item(0); const Off = xFrm.getElementsByTagName('a:off').item(0); const Ext = xFrm.getElementsByTagName('a:ext').item(0); position.x = XmlSlideHelper.parseCoordinate(Off, 'x'); position.y = XmlSlideHelper.parseCoordinate(Off, 'y'); position.cx = XmlSlideHelper.parseCoordinate(Ext, 'cx'); position.cy = XmlSlideHelper.parseCoordinate(Ext, 'cy'); if (xFrm.getAttribute('rot')) { position.rot = parseInt(xFrm.getAttribute('rot')); } return position; } getSlideLayoutXml(layoutId) { return __awaiter(this, void 0, void 0, function* () { return XmlSlideHelper.getSlideLayoutXml(this.sourceArchive, layoutId); }); } static getSlideLayoutXml(sourceArchive, layoutId) { return __awaiter(this, void 0, void 0, function* () { const layoutPath = 'ppt/slideLayouts/slideLayout' + layoutId + '.xml'; const layoutXml = yield xml_helper_1.XmlHelper.getXmlFromArchive(sourceArchive, layoutPath); if (layoutXml) { return layoutXml; } }); } /** * Asynchronously retrieves the dimensions of a slide. * Tries to find the dimensions from the slide XML, then from the layout, master, and presentation XMLs in order. * * @returns {Promise<{ width: number, height: number }>} The dimensions of the slide. * @throws Error if unable to determine dimensions. */ getDimensions() { return __awaiter(this, void 0, void 0, function* () { try { const dimensions = yield this.getAndExtractDimensions('ppt/presentation.xml'); if (dimensions) return dimensions; } catch (error) { console.error(`Error while fetching slide dimensions: ${error}`); throw error; } }); } /** * Reconstruct the complete selector for a given xmlElement, including the * n-th occurance of the shape name on the current slide * @param slideElement */ static getSelector(slideElement) { const creationId = XmlSlideHelper.getElementCreationId(slideElement, true); const nameIdx = !creationId ? XmlSlideHelper.getElementNameIdx(slideElement) : 0; return { name: XmlSlideHelper.getElementName(slideElement), creationId, nameIdx, }; } /** * Determines the type of visual element in PowerPoint * @param element The XML element to check * @returns A string identifying the element type */ static getElementVisualType(element) { if (XmlSlideHelper.getElementType(element) === 'grpSp') { return 'group'; } // Check for graphicFrame elements (charts, SmartArt, tables, etc.) if (element.tagName === 'p:graphicFrame') { const graphicData = xml_helper_1.XmlHelper.findElement(element, 'a:graphicData'); if (graphicData) { const uri = graphicData.getAttribute('uri').toLowerCase(); // Check for specific URIs that identify element types if (uri && uri.includes('chart')) { return 'chart'; } else if (uri && (uri.includes('smartart') || uri.includes('diagram'))) { return 'smartArt'; } else if (uri && uri.includes('table')) { return 'table'; } } return 'graphicFrame'; } // Check for tables - also check direct table elements if (xml_helper_1.XmlHelper.findElement(element, 'a:tbl') || element.getElementsByTagName('a:tbl')[0]) { return 'table'; } // Check for SVG Images if (xml_helper_1.XmlHelper.findElement(element, 'a:svgBlip') || xml_helper_1.XmlHelper.findElement(element, 'asvg:svgBlip')) { return 'svgImage'; } // Check for pictures/photos const hasPicPr = !!element.getElementsByTagName('p:nvPicPr')[0]; if (hasPicPr) { return 'picture'; } // Check for image fills const hasBlipFill = !!xml_helper_1.XmlHelper.findElement(element, 'a:blipFill'); if (hasBlipFill) { return 'imageFilledShape'; } const hasGeometry = !!xml_helper_1.XmlHelper.findElement(element, 'a:prstGeom') || !!xml_helper_1.XmlHelper.findElement(element, 'a:custGeom'); if (hasGeometry) { // Check if it's a line shape specifically const prstGeomElement = xml_helper_1.XmlHelper.findElement(element, 'a:prstGeom'); if (prstGeomElement) { const prst = prstGeomElement.getAttribute('prst'); // Common line presets in PowerPoint const linePresets = [ 'line', 'lineInv', 'straightConnector1', 'bentConnector2', 'bentConnector3', 'bentConnector4', 'bentConnector5', 'curvedConnector2', 'curvedConnector3', 'curvedConnector4', 'curvedConnector5', 'callout1', 'callout2', 'callout3', 'accentCallout1', 'accentCallout2', 'accentCallout3', 'borderCallout1', 'borderCallout2', 'borderCallout3', 'accentBorderCallout1', 'accentBorderCallout2', 'accentBorderCallout3', ]; if (prst && linePresets.includes(prst)) { return 'vectorLine'; } if (prst && prst === 'rect') { const cNvSpPr = xml_helper_1.XmlHelper.findElement(element, 'p:cNvSpPr'); if (cNvSpPr && cNvSpPr.getAttribute('txBox') === '1') { return 'textBox'; } return 'rectangle'; } } return 'vectorShape'; } if (element.tagName === 'p:sp') { const txBody = xml_helper_1.XmlHelper.findElement(element, 'p:txBody'); if (txBody) { return 'textBox'; } } // Check for 3D models if (xml_helper_1.XmlHelper.findElement(element, 'a:scene3d')) { return '3dModel'; } // Default case return 'unknown'; } } exports.XmlSlideHelper = XmlSlideHelper; XmlSlideHelper.parseCoordinate = (element, attributeName) => { return parseInt(element.getAttribute(attributeName), 10); }; XmlSlideHelper.parseGroupInfo = (element) => { if (!(element === null || element === void 0 ? void 0 : element.parentNode)) { return; } // Check if element is a child of a group // Look for a parent node that is a group (grpSp) const isChild = element.parentNode && (element.parentNode.nodeName === 'grpSp' || element.parentNode.nodeName.includes('grpSp')); // Check if element is a group parent itself const isParent = element.localName === 'grpSp' || element.nodeName.includes('grpSp'); // Function to get the parent group if element is a child const getParent = () => { if (isChild && element.parentNode) { return element.parentNode; } return null; }; // Function to get children if element is a group parent const getChildren = () => { if (isParent) { // Get all children that are not group properties or group metadata const children = Array.from(element.childNodes).filter((node) => { if (node.nodeType !== 1) return false; // Skip non-element nodes const nodeName = node.localName || node.nodeName; // Skip group property elements return (!nodeName.includes('nvGrpSpPr') && !nodeName.includes('grpSpPr')); }); return children; } return []; }; return { isChild, isParent, getParent, getChildren, }; }; XmlSlideHelper.readTableInfo = (element) => { const info = []; const rows = element.getElementsByTagName('a:tr'); if (!rows) { console.error("Can't find a table row."); return info; } for (let r = 0; r < rows.length; r++) { const row = rows.item(r); const columns = row.getElementsByTagName('a:tc'); for (let c = 0; c < columns.length; c++) { const cell = columns.item(c); const gridSpan = cell.getAttribute('gridSpan'); const hMerge = cell.getAttribute('hMerge'); const texts = cell.getElementsByTagName('a:t'); const text = []; for (let t = 0; t < texts.length; t++) { text.push(texts.item(t).textContent); } info.push({ row: r, column: c, rowXml: row, columnXml: cell, text: text, textContent: text.join(''), gridSpan: Number(gridSpan), hMerge: Number(hMerge), }); } } return info; }; //# sourceMappingURL=xml-slide-helper.js.map